[Anfänger] Gültigkeitsbereich von lokaler Variable



  • Hallo,

    ich bin gerade dabei von C# auf C++ umzusteigen und habe ein kleines Logikproblem mit dem gültigkeitsbereich von lokalen Variablen. Um es besser erklären zu können hier mal etwas Code vorab:

    #include <iostream>
    
    using namespace std;
    
    class MyTestClass
    {
        public:
        MyTestClass();
        ~MyTestClass();
    };
    
    MyTestClass::MyTestClass()
    {
        cout << "MyTestClass::ctor()" << endl;
    }
    
    MyTestClass::~MyTestClass()
    {
        cout << "MyTestClass::dtor()" << endl;
    }
    
    MyTestClass GetInstance()
    {
        MyTestClass mt;
        cout << "GetInstance(): Address of mt: " << hex << &mt << endl;
    
        return mt;
    }
    
    int main( int argc, char **argv )
    {
        MyTestClass mt = GetInstance();
    
        cout << "main(): Address of mt: " << hex << &mt << endl;
    }
    

    In der Funktion "GetInstance" wird eine Instanz der Klasse "MyTestClass" am Stack erzeugt und dann an main zurück gegeben. Warum funktioniert das? Ich dachte der gültigkeitsbereich von Stack basierten Datenstrukturen endet mit dem verlassen es Blocks in dem diese erzeugt wurden?

    Folgende Lösungen hätte ich selbst erarbeitet, wiedersprechen sich aber zum teil oder sind unlogisch:

    1.) Durch das return Statement wird eine Bitweise Kopie des lokalen Objekts erstellt und auf den Stack gepusht. Der Caller benutzt nun dieses "neue" Objekt - dagegen spricht allerdings dass die beiden Objekte die gleiche Speicheradresse haben?
    2.) Es ist das gleiche Objekt. Durch die Ausgaben im Konstruktor/Destruktor wird ersichtlich daß der Konstruktor in der Funktion "GetInstance" beim anlegen des Objekts ausgeführt wird, der Destruktor aber erst nachdem sich main funktion verlassen wird.

    Kann mir bitte jemand eine erklärung anbieten?

    Danke und Grüße,
    Egon


  • Mod

    Es ist eine Kopie.

    Würde deine Methode stattdessen ein Objekt vom Typ MyTestClass& zurückgeben, dann wäre es das gleiche Objekt und würde beim Verlassen tatsächlich seine Gültigkeit verlieren.

    edit: Das kannst du beispielsweise überprüfen, indem du die Konstruktoren und Destruktoren eine Meldung ausgeben lässt:

    MyTestClass()
    {
        std::cout<<"MyTestClass erzeugt"<<std::endl;
    }
    ~MyTestClass();
    {
        std::cout<<"MyTestClass zerstört"<<std::endl;
    }
    


  • Das ist hier, weil das Objekt per Kopie zurückgegeben wird. Das heisst, dass ein neues Objekt mit dem Kopierkonstruktor erzeugt wird.

    Füg das mal hinzu:

    MyTestClass::MyTestClass( const MyTestClass & rhs) //Kopierkonstruktor
    {
        cout << "MyTestClass::copy-ctor()" << endl;
    }
    


  • Und damit es sich so verhält wie in C# (oder Java) musst Du einen Pointer erzeugen und zurückgeben.

    MyTestClass* GetInstance() 
    { 
        MyTestClass* mt = new MyTestClass; 
        cout << "GetInstance(): Address of mt: " << hex << mt << endl; 
        return mt;
    }
    

    Du musst Dich dann nur noch selber um das "zerstören" des pointers kümmern.

    int main( int argc, char **argv )
    {
        MyTestClass* mt = GetInstance();
        cout << "main(): Address of mt: " << hex << mt << endl;
        delete mt;
        return 0;
    }
    

    Das zerstören übernimmt in C#/Java dann die GarbageCollection, aber auch dafür gibt es Umsetzungen in C++.

    Gut Schuß
    VuuRWerK 😉



  • Hallo,

    irgendwie eigenartig: Wenn ich in der Klasse einen Kopierkonstruktor einfüge, wird dieser NICHT aufgerufen:

    Output des Testprogramms:

    MyTestClass::ctor()
    GetInstance(): Address of mt: 0xbfccd13b
    main(): Address of mt: 0xbfccd13b
    MyTestClass::dtor()
    

    Code des Testprogramms:

    #include <iostream>
    
    using namespace std;
    
    class MyTestClass
    {
        public:
        MyTestClass( const MyTestClass & rhs );
        MyTestClass();
        ~MyTestClass();
    };
    
    MyTestClass::MyTestClass( const MyTestClass & rhs )
    {
        cout << "MyTestClass::ctor(const MyTestClass &)" << endl;
    }
    
    MyTestClass::MyTestClass()
    {
        cout << "MyTestClass::ctor()" << endl;
    }
    
    MyTestClass::~MyTestClass()
    {
        cout << "MyTestClass::dtor()" << endl;
    }
    
    MyTestClass GetInstance()
    {
        MyTestClass mt;
        cout << "GetInstance(): Address of mt: " << hex << &mt << endl;
    
        return mt;
    }
    
    int main( int argc, char **argv )
    {
        MyTestClass mt = GetInstance();
    
        cout << "main(): Address of mt: " << hex << &mt << endl;
    }
    

    Was mich ja auch etwas stutzig macht bezüglich dem dass es sich um eine Kopie handelt: Warum wird der Destruktor des originals nach beendigung von "GetInstance" nicht aufgerufen?

    Grüße,
    Egon

    /edit: So ich hab jetzt noch mal in meinem schlauen Lehrbuch nachgelesen. Es ist tatsächlich eine Kopie. Der Compiler legt beim verlassen eine Kopie des Objekts auf den Stack welche dann vom Aufrufer benutzt wird. Der Destruktor wird in diesem Fall nicht aufgerufen da dadurch die Kopie zerstört werden könnte wenn beispielsweise Zeiger freigegeben werden (und es sich ja nicht um eine Deep sondern eine Flache Kopie handelt) - was nicht behandelt wurde: Warum wir der eigens definierte Copy Construktor nicht benutzt? Kann das sein dass dies in diesem bestimmten Fall sogar so im Standard vorgesehen ist? Fragen über Fragen....



  • Du bist da gleich auf 2 Optimierungen deines Compilers gestossen. (ich nehme mal an, dass du die Optimierungen einstellt hast, was bei MSVC++ /O2 bedeutet.

    Das erste nennt sich NRVO (name return value optimization) und kommt bei der Rückgabe eines Objektes per Kopie zum tragen.

    Und das zweite kommt bei der Definition von mt zum tragen, wo anstatt ein Objekt erstellt und dann per Kopierkonstruktor kopiert wird gleich das ganze Objekt erstezt wird.

    Vergleich die Ausgabe mal, wenn du das hier änderst:

    MyTestClass mt = GetInstance();
    

    zu:

    MyTestClass mt;
    mt = GetInstance();
    

    Am besten machst du auch gleich noch den assign-Operator rein, dann siehst du alles.

    MyTestClass& MyTextClass::operator = ( const MyTestClass & rhs )
    {
        cout << "assign-operator" << endl;
    }
    

    Und das ganze kannst du dann noch mit dem unoptimierten Output vergleichen. (ist dann ganz schön viel :))

    Hier sei noch angemerkt, dass diese Optimierung durchaus Seiteneffekte haben kann und optimierter Code sich anders verhalten kann, wie unoptimierter. Das wird aber im Artikel auch erlärt.



  • Ganz allgemein nennt man das was da (nicht) passiert "copy (constructor) elision".

    Also das "übergehen" des copy-ctors, weil er dort "nicht nötig ist", und der C++ Standard es an der Stelle (und einigen anderen) erlaubt ihn wegzulassen.



  • Hallo,

    danke für eure Antworten, nun ist mir die ganze Sache klar geworden!

    Grüße,
    Egon


Anmelden zum Antworten