Geltungsbereich von Variablen



  • Hi,

    ich bin gerade am C++ lernen und habe gerade ein kleines Denkproblem.

    Und zwar hatte ich gelesen und kenne es auch so von Java, dass Variablen, die in einer Methode deklariert werden, halt nur in der Methode gültig sind. (in Java werden aber die Variablen nicht wieder freigegeben, wenn noch irgendwo eine Variable darauf zeigt. Also in Java hab ich das folgende Problem nicht)

    Und soweit wie ich das jetzt verstanden habe, sind Pointer, auch wenn sie in einer Methode erstellt wurden, auch danach noch gültig bzw. der Speicher muss mit delete freigemacht werden. Korrigiert mich bitte wenn ich Müll erzähle 😃

    Jetzt hatte ich vorhin folgende Methode:

    int* test() {
    	int bla[10];
    	return bla;
    }
    

    Da hatte der Compiler dann als Warnung ausgegeben, dass ich eine Adresse einer lokalen Variable zurückgebe. Ist ja auch klar, da bla am Ende der Methode ja wieder freigegeben wird und der Zeiger dann auf freigegeben Speicher zeigt.

    Ich hatte dass dann folgendermaßen umgeschrieben:

    int* test() {
    	int* bla = new int[10];
    	return bla;
    }
    

    und die zurückgegebene Variable dann später im Code mit delete[] bla; wieder freigegeben.
    Hat auch alles geklappt.

    Aber jetzt kommt meine Frage:
    Ich habe jetzt noch z.B. folgende Methode:

    void DEA::addConnection(string startState, string endState, string input) {
    	Connection conn(startState, endState, input);
    	connections.push_back(conn);
    }
    

    Warum wird da der Speicher von Connection nicht automatisch wieder freigegeben? Oder wird er das und ich hatte bisher nur Glück dass es bei mir immer geklappt hat? Oder merkt C++ dass die Variable noch in einer Liste ist und behält sie deswegen?
    Oder sollte ich da auch lieber einen pointer auf die neue Connection verwenden?

    Wie ihr bestimmt merkt ist bei mir da noch bisschen Unklarheit. 🙂
    Ich hoffe ihr könnt mir bisschen Klarheit verschaffen mit den Pointern und Gültigkeitsbereich.

    Gruß,
    Dennis

    P.S.: Hab gerade noch meinen ersten Post hier im Forum gelesen 😃
    Ist ja schon ein Weilchen her und wenn ich den jetzt so lese hätte ich selber nicht gedacht dass der Typ es schafft irgendwas mit Programmieren auf die Beine zu stellen.
    Aber hab es inzwischen geschafft paar Spiele zu programmieren und auch eins das Mario ähnlich ist:D (Mein neuestes Spiel kommt übrigens bald raus: Marble Mania) Die sind zwar alle in Java geschrieben, aber bin derzeit wieder am C++ lernen für die Uni.


  • Mod

    Creativ schrieb:

    Jetzt hatte ich vorhin folgende Methode:

    int* test() {
    	int bla[10];
    	return bla;
    }
    

    Da hatte der Compiler dann als Warnung ausgegeben, dass ich eine Adresse einer lokalen Variable zurückgebe. Ist ja auch klar, da bla am Ende der Methode ja wieder freigegeben wird und der Zeiger dann auf freigegeben Speicher zeigt.

    Richtig. Automatische Variablen (also Sachen die du nicht mit new angelegt hast) werden beim Verlassen ihres Gültigkeitsbereichs (d.h. die geschweiften Klammern zwischen denen sie stehen) zerstört.

    Ich hatte dass dann folgendermaßen umgeschrieben:

    int* test() {
    	int* bla = new int[10];
    	return bla;
    }
    

    und die zurückgegebene Variable dann später im Code mit delete[] bla; wieder freigegeben.
    Hat auch alles geklappt.

    Auch richtig. Wobei man merkt, dass du von Java kommt. Normalerweise würde man nämlich das new tunlichst vermeiden und stattdessen zum Beispiel einen vector<int> (eine kopierbare, dynamische Array-Klasse, die sich komplett selber um ihre Speicherverwaltung kümmert) benutzen.

    Aber jetzt kommt meine Frage:
    Ich habe jetzt noch z.B. folgende Methode:

    void DEA::addConnection(string startState, string endState, string input) {
    	Connection conn(startState, endState, input);
    	connections.push_back(conn);
    }
    

    Warum wird da der Speicher von Connection nicht automatisch wieder freigegeben? Oder wird er das und ich hatte bisher nur Glück dass es bei mir immer geklappt hat? Oder merkt C++ dass die Variable noch in einer Liste ist und behält sie deswegen?

    Dein Beispiel ist leider fehlerhaft, deshalb ist die Frage nicht beantwortbar. Was soll denn Connection sein? Das sehe ich nirgends im Code. Und was ist connections?



  • Das heißt bei einem vector brauch ich mich überhaupt nicht um Speicherverwaltung kümmern?
    Also könnte ich dann einfach den Vector anlegen mit
    vector<int> bla;
    und den als rückgabewert benutzen und der bleibt danach weiterhin erhalten, ohne dass er automatisch zerstört wird?

    Kann ich dass dann so verstehen, dass meistens in C++ statt Arrays einfach vector benutzt werden?

    Sorry, hatte ich vergessen zu erwähnen.
    Connection ist eine Klasse von mir die derzeit einfach nur 3 String beinhaltet:

    class Connection {
    public:
    	Connection(string start, string end, string input);
    	~Connection();
    
    	string getInput();
    	string getStart();
    	string getEnd();
    private:
    	string start;
    	string end;
    	string input;
    };
    

    Und connections ist einfach ein vector:
    vector<Connection> connections;


  • Mod

    Creativ schrieb:

    Das heißt bei einem vector brauch ich mich überhaupt nicht um Speicherverwaltung kümmern?
    Also könnte ich dann einfach den Vector anlegen mit
    vector<int> bla;
    und den als rückgabewert benutzen und der bleibt danach weiterhin erhalten, ohne dass er automatisch zerstört wird?

    In dem Falle würde eine Kopie gemacht*. Ansonsten gelten für alle Datentypen die gleichen Regeln für den Gültigkeitsbereich, das heißt das Original würde beim Verlassen der Funktion zerstört.

    Kann ich dass dann so verstehen, dass meistens in C++ statt Arrays einfach vector benutzt werden?

    Ja. Und selbst wenn man wirklich die Eigenschaften eines statischen Arrays braucht, dann sollte man dieses trotzdem in einer Klasse kapseln.

    Sorry, hatte ich vergessen zu erwähnen.
    Connection ist eine Klasse von mir die derzeit einfach nur 3 String beinhaltet:

    class Connection {
    public:
    	Connection(string start, string end, string input);
    	~Connection();
    
    	string getInput();
    	string getStart();
    	string getEnd();
    private:
    	string start;
    	string end;
    	string input;
    };
    

    Und connections ist einfach ein vector:
    vector<Connection> connections;

    Also connections ist global außerhalb der Funktion? Wenn ja, dann passiert folgendes: Du fügst mit push_back eine Kopie von conn an connections an. Die Variable conn wird danach beim verlassen der Funktion zerstört. Die Kopie bleibt aber erhalten.

    *: Es ist dem Compiler ausdrücklich erlaubt diese (ziemlich unnötige) Kopieraktion weg zu optimieren und gleich das Original weiterzubenutzen. das nennt man dann 'return value optimization'. Die Vorstellung mit der Kopie und der Zerstörung des Originals ist nur wie es in der Theorie abläuft.



  • Danke erstmal für deine Erklärungen 🙂

    Hab jetzt aber gerade noch ein für mich merkwürdes Verhalten entdeckt.

    Und zwar hab ich einen vector der Zeiger auf eine Klasse enthält:

    vector<Particle*> particles
    

    Jetzt iteriere ich die Liste durch:

    void Test::update1() {
    for(vector<Particle*>::iterator it = particles.begin(); it != particles.end(); it++) {
    	Particle p = **it;
    	p.update();
    }
    }
    

    Der Compiler meckert dabei auch nicht und es scheint auch alles zu funktionieren, allerdings sind die Werte, die in der update-Methode des Particle verändert werden, nachdem update1() durchgelaufen ist, wieder auf dem Wert von vorher. Das heißt wenn ich die Liste danach nochmal durchiteriere hab ich wieder die gleichen Werte wie am Anfang.
    Ich hoffe ihr versteht was ich meine.
    Ansonsten hier nochmal bisschen Pseudocode der das vielleicht besser erklärt:

    vector erstellen und mit particle-Zeigern füllen
    durchlaufen => z.B. die variable int test von Particle ist 0
    update1() <- für jedes particle wird in der update() test um 1 erhöht
    durchlaufen => die variable test von Particle ist wieder 0

    Wenn ich jetzt aber beim durchiterieren statt Particle p = *it; den Zeiger speicher:
    Particle
    p = *it;
    und den dann benutze und davon die update()-Methode aufrufe, funktioniert alles ganz normal.

    Jetzt hab ich die Vermutung dass wenn ich Particle p = **it; benutze, in p eine Kopie vom eigentlichen Objekt gespeichert wird und dadurch die Änderungen halt nicht beim Original übernommen werden.
    Aber warum?
    Nach meiner Logik würde ich davon ausgehen, dass ich ja nur den Inhalt vom Zeiger hole und den bearbeite und daher müsste das ja auch gespeichert werden. Aber scheinbar ist dem ja nicht so.

    Kann mir das vielleicht wer erklären warum C++ da scheinbar eine Kopie benutzt?



  • Das verhält sich wie eine Kopie, weil es eine Kopie ist. Mit

    Particle p = **it;
    

    erzeugst du ein neues Particle-Objekt aus dem Inhalt von **it, in diesem Fall also anhand Particles Kopierkonstruktors. Wenn du eine Referenz nehmen willst, nimm eine Referenz:

    Particle &p = **it;
    

    Klassen sind in C++ keine Referenztypen wie in Java.



  • Creativ schrieb:

    Und zwar hab ich einen vector der Zeiger auf eine Klasse enthält:

    vector<Particle*> particles
    

    Warum nicht std::vector<Particle> ?

    Creativ schrieb:

    for(vector<Particle*>::iterator it = particles.begin(); it != particles.end(); it++)
    

    Warum nicht ++it ?



  • Creativ schrieb:

    vector erstellen und mit particle-Zeigern füllen
    durchlaufen => z.B. die variable int test von Particle ist 0
    update1() <- für jedes particle wird in der update() test um 1 erhöht
    durchlaufen => die variable test von Particle ist wieder 0

    Also fangen wir mal ganz an Anfang an:
    C++ benutzt bei Funktionen die Übergabe by value, d.h. es werden immer Kopien erstellt. Das gilt auch für alle Containerklassen der C++ Standard Library. Will man keine Kopien haben, muß man explizit Objekte via Referenz übergeben.

    Beispiel

    #include <vector>
    #include <iostream>
    
    std::vector<int> func1 (std::vector<int> vec) {
        vec.clear();
        vec.push_back(5);
    
        return vec;
    }
    
    void func2 (std::vector<int>& vec) {
        vec.clear();
        vec.push_back(5);
    
        return;
    }
    
    int main () {
        std::vector<int> v, w;
    
        v.push_back(1);
        v.push_back(3);
    
        w = func1(v);
    
        std::cout << "vector v\n";
        for (std::vector<int>::iterator it = v.begin(), en = v.end(); it != en; ++it) {
            std::cout << *it << "\n";
        }
    
        std::cout << "\nvector w\n";
        for (std::vector<int>::iterator it = w.begin(), en = w.end(); it != en; ++it) {
            std::cout << *it << "\n";
        }
        std::cout << "\n";
    
        func2(v);
    
        std::cout << "vector v\n";
        for (std::vector<int>::iterator it = v.begin(), en = v.end(); it != en; ++it) {
            std::cout << *it << "\n";
        }
    }
    

    Desweiteren muß man anmerken, daß es in der Regel nicht sinnvoll ist Zeiger in Standard Container zu speichern, da diese die Objekte "by value" verwalten. Wird ein Objekt in einem Container gelöscht, wird nur dessen Destruktor aufgerufen. Für normale Zeiger heißt dies, daß die Objekte auf denen die Zeiger verweisen nicht gelöscht werden. Das kann manchmal so gewollt sein.

    Falls man Zeiger in Container braucht, sollte man sich die boost SmartPointer Klassen anschauen, diese bieten dieses Verhalten. Wahlweise mit deep oder swallow copies.



  • Swallow copy 🤡
    War nicht vielleicht shallow copy gemeint? 🙂



  • ~john schrieb:

    Falls man Zeiger in Container braucht, sollte man sich die boost SmartPointer Klassen anschauen, diese bieten dieses Verhalten. Wahlweise mit deep oder swallow copies.

    Lieber gleich die Pointer-Container. Es sei denn, man benötigt tatsächlich Shared-Ownership-Semantik.


Anmelden zum Antworten