(Standard- vs. Kopier-) Konstruktor



  • Hallo ihr Lieben,

    mit dem Wunsch die Vererbung zu verstehen versuche ich die verschiedenen Konstruktoren und den Sprachgebrauch zu verstehen.

    Wie der Titel sagt, gibt es verschiedene Arten eines Konstruktors. Doch scheinbar gibt es keine Namen für einen Konstruktor, der keinem der drei im Titel genannten Bezeichnungen gerecht wird?

    Wenn ich das recht verstehe, dann besitzt die folgende Klasse einen Standardkonstruktor

    class Klasse
    {
        Klasse();
    };
    

    Diese Deklaration könnte ich mir auch sparen, weil C++ im Zweifelsfall ohnehin einen Standardkonstruktor erstellt?

    Wie sieht es aber aus, wenn ich im Konstruktor nichts übergebe (also nichts kopiere oder zueweise), aber z.B. eine Initialisierungsliste verwende.

    class Klasse
    {
        Klasse();
    
        private:
          int val;
    };
    Klasse::Klasse():val(0) {}
    

    Das ist jetzt kein Standardkonstruktor, weil im Konstruktor nicht Nichts passiert? Komische Formulierung. 😃
    Und sobald ich im Konstruktor etwas definiere, also keinen Standarkonstruktor im Sinne von nicht Nichts habe, muss ich sofort die Regel der 3 beachten?

    Und jetzt gibt es noch den Kopierkonstruktor, dessen Bezeichnung ich nicht recht verstehe. Im Sinne von Funktionen existiert doch call by value und call by reference? Ich kann eine Variable a als Kopie übergeben

    void function(double a)
    

    oder mit der Referenz darauf arbeiten, wohl wissend dass jede Änderung an der Variablen auch stattfindet, da es sich nicht nur um eine lokale Kopie handelt, die den Gültigkeitsbereich nicht verlässt.

    void function(double & a)
    

    Vom Sprachgebrauch würde ich vermuten, dass der Kopierkonstruktor eine lokale Kopie des übergebenen Objekts anlegt und mit dieser arbeitet

    class Klasse
    {
        Klasse(double a);
    
        private:
          int val;
    };
    Klasse::Klasse():val(a) {}
    

    Wenn ich mir aber z.B. hier die Definition eines Kopierkonstruktors anschaue, dann ist das 'ganz anders'. Zum Einen wird der Punkt verwendet und zum Anderen zielt es auf die Kopie von Klassen ab? Außerdem scheint in diesem Syntax nichts kopiert zu werden, da der Referenzoperator verwendet wird: Es wird keine lokale Kopie angelegt sondern direkt mit den Werten der zu übergebenden Variable gearbeitet.
    Aber ich schätze der Sprachgebrauch soll dann bedeuten, dass der Inhalt der Variablen worauf die Referenz zeigt kopiert wird?

    Ist es in meinem einfach konstruierten Beispiel unnötig von Kopierkonstruktor zu sprechen, weil ich lediglich eine Variable übergebe und keine ganze Klasse?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Wenn ich das recht verstehe, dann besitzt die folgende Klasse einen Standardkonstruktor

    class Klasse
    {
        Klasse();
    };
    

    Diese Deklaration könnte ich mir auch sparen, weil C++ im Zweifelsfall ohnehin einen Standardkonstruktor erstellt?

    Ja.

    Wie sieht es aber aus, wenn ich im Konstruktor nichts übergebe (also nichts kopiere oder zueweise), aber z.B. eine Initialisierungsliste verwende.

    class Klasse
    {
        Klasse();
    
        private:
          int val;
    };
    Klasse::Klasse():val(0) {}
    

    Das ist jetzt kein Standardkonstruktor, weil im Konstruktor nicht Nichts passiert? Komische Formulierung. 😃

    Doch, das ist ein Standardkonstruktor, weil der standardmäßig aufgerufen wird.

    Und sobald ich im Konstruktor etwas definiere, also keinen Standarkonstruktor im Sinne von nicht Nichts habe, muss ich sofort die Regel der 3 beachten?

    Die Regel der Drei beinhaltet Destruktor, Kopierkonstruktor und Zuweisungsoperator.

    Ist es in meinem einfach konstruierten Beispiel unnötig von Kopierkonstruktor zu sprechen, weil ich lediglich eine Variable übergebe und keine ganze Klasse?

    Der Kopierkonstruktor einer Klasse T hat die Syntax.

    T(const T &other);
    

    Er wird benutzt, um Objekte zu kopieren. Dein Beispiel war kein Kopierkonstruktor, sondern einfach ein normaler Konstruktor (der nebenbeibemerkt auch benutzt werden kann um ein double automatisch in Klasse zu konvertieren).



  • Hallo Nathan,

    Nathan schrieb:

    Er wird benutzt, um Objekte zu kopieren. Dein Beispiel war kein Kopierkonstruktor, sondern einfach ein normaler Konstruktor (der nebenbeibemerkt auch benutzt werden kann um ein double automatisch in Klasse zu konvertieren).

    Also ich halte fest: Der Kopierkonstruktor handelt von dem Kopieren von Instanzen, d.h. Klassen (Strukturen).

    In dem folgenden Beispiel wird auch etwas kopiert, nämlich der Wert der übergebenen Referenz in die Variable var der Klasse. Aber da lediglich der Inhalt einer Variablen kopiert wird (und keiner Instanz) reden wir nicht von einem Kopierkonstruktor und müssen auch nicht die Regel der Großen Drei anweden.

    class Klasse
    {
    	public:
    		Klasse(double & a):var(a){}
    		double var;
    };
    

    Analog kann ich diesen Syntax auch für die Übergabe von Klassen verwenden

    class foo
    {
    	public:
    	foo():var(5){}
    	int var;
    };
    
    class bar
    {
    	public:
    		//bar(class foo f):var(f.var){}
    		//bar(class foo & f):var(f.var){}
    		bar(const foo& other):var(other.var){}
    		int var;
    };
    

    Dabei ist allerdings von all den drei präsentierten Möglichkeiten eines Konstruktors die dritte (nicht auskommentiert) ein Kopierkonstruktor. Die ersten beiden Möglichkeiten funktionieren auch, sind aber nicht zu empfehlen.

    Schließlich wundert mich noch ein Detail in dem angegebenem Wikieintrag:

    Ein besonderer und sehr hilfreicher Konstruktor ist der Kopierkonstruktor (copy constructor), mit dem Sie das Kopieren aus einer anderen Instanz der gleichen Klasse genau steuern können, falls Sie keine eigenen Regeln zur Kopie festlegen, liefert Ihnen der Compiler eine Definition in Form einer Eins-zu-eins-Kopie

    Wie jetzt? Ich will doch eine andere Klasse ( foo ) mittels Konstruktor an die Klasse bar übergeben. 😕

    Gruß,
    -- Klaus.



  • -Ein Standardkonstruktor ist jeder Konstruktor, der ohne Argumente aufgerufen werden kann. ( also z.B. auch T(int x=0) )
    -Ein Kopierkonstruktor ist jeder Konstruktor, der ein Argument T& will. ( also z.B. auch T(const T& ori, int x=0) )



  • out schrieb:

    -Ein Kopierkonstruktor ist jeder Konstruktor, der ein Argument T& will. ( also z.B. auch T(const T& ori, int x=0) )

    Ich wundere mich die ganze Zeit nur, warum in dem Beispiel in dem wiki-Eintrag oder auch der Syntax von Nathan stets die eigene Klasse im Rumpf des Kopierkonstruktors auftaucht.

    Aber ich denke: Dort soll es ja auch auch hinkopiert werden.

    Ist dann die Verwendung einer im Rumpf vewendeten Variable ein Umweg?

    Ich habe den Eindruck, dass bei folgendem Beispiel

    class Klasse
    {
    	public:
    		Klasse(double & a):var(a){}
    		double var;
    };
    
    int main()
    {
    double d = 10.0;
    
    Klasse K(d);
    
    return 0;
    }
    

    etwas umständlich dasteht

    Klasse.var( a ( d ) )
    

    Ich kopiere den Inhalt von d in die Referenz a und kopiere sie dann weiter in var der Klasse.

    Also kann ich mir den Umweg über die Referenz schenken und den Inhalt von d direkt in var kopieren.

    class Klasse
    {
    	public:
    		Klasse(const Klasse& other):var(other.var){}
    		double var;
    };
    
    int main()
    {
    double d = 10.0;
    
    Klasse K(d);
    
    return 0;
    }
    

    Stimmt diese Sichtweise einigermaßen?

    Die Notation mit Klasse(double & a) ist in Anlehnung an den Rumpf von Funktionen, die per se keine vorher deklarierten Variablen besitzen, wie es eben eine Klasse tut.

    Gruß,
    -- Klaus.



  • Hä. Du bist gerade auf Abwegen. Nochmal: Ein Kopierkonstruktor ist ein Konstruktor, der den Parameter T& hat, wobei T der Klassenname (in deinem Fall Klasse ) ist.

    Klasse(double & a):var(a){}
    

    Somit ist das also kein Kopierkonstruktor. Das hat überhaupt nichts mit einem zu tun. Ein Kopierkonstruktor erstellt ein neues Objekt anhand eines anderen Objekts der Klasse. Kopieren heißt hier, ich kopiere ein Objekt X meiner Klasse T, und nicht, ich kopiere ein Objekt X irgendeines Datentyps.



  • Klar kannst du ein Objekt auch mittels eines einfachen Konstruktors kopieren. Aber das ist doch total doof. Nicht umsonst gibt es den Kopierkonstruktor. Denn sowas

    class Klasse
    {
        public:
            Klasse(double & a):var(a){}
            double var;
    };
    int main()
    {
        Klasse a;
        Klasse b(a.var);
    }
    

    ist doch total doof. Stell dir mal vor, du hast 20 Member, dann viel Spaß. 😃



  • Ach soooo.

    Man langsam verstehe ich das ganze. Es geht nicht darum wie ich ein Objekt erstmalig initialisiere, sondern wie ich andere Objekte des gleichen Typs aus einem bereits bestehenden heraus erstelle!

    Dazu das folgende Minimalbeispiel

    #include <iostream>
    
    using namespace std;
    
    struct foo
    {
    	foo();
    	foo(const foo&);
    	foo& operator=(const foo&);
    
    	int var;
    };
    
    foo::foo():var(5)
    {
    	cout << "Constructor called!" << endl;
    }
    
    foo::foo(const foo& other):var(other.var)
    {
    	cout << "Copy construtor called!" << endl;
    }
    
    foo& foo::operator=(const foo& rhs)
    {
    	cout << "Assignment oprator called!" << endl;
    	var = rhs.var;
    	return *this;
    }
    
    int main()
    {
    
    foo f;
    
    foo f1(f);
    
    // foo f2 = f; <-- Error
    foo f2;
    
    f2 = f;
    
    return 0;
    }
    

    Mit der Ausgabe

    Constructor called!
    Copy construtor called!
    Constructor called!
    Assignment oprator called!
    

    In Zeile 34 wird das erste Objekt (die Instanz, weil von einer Klasse bzw. Struktur?) erstellt, das führt zur ersten Zeile der Ausgabe.
    Also nächstes erstelle ich in Zeile 36 die zweite Instanz von foo , durch das Kopieren von f. Das führt zur nächsten Zeile der Ausgabe.
    Schließlich möchte ich den Assignment operator verwenden, der lässt sich allerdings nur auf bereits bestehende Objekte anwenden, deshalb geht Zeile 38 vom Syntax her nicht.
    Ich muss ein neues foo zunächst in Zeile 39 erstellen und kann ihm dann den Inhalt in Zeile 41 zuordnen!

    Jetzt habe ich es doch, oder? 🙂

    Und eigentlich wäre ich jetzt fertig, nur in Zusammenhang mit STL Containern bricht das ganze wieder zusammen. Aus diesem Grund kommt die move semantik ins Spiel, ja?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Schließlich möchte ich den Assignment operator verwenden, der lässt sich allerdings nur auf bereits bestehende Objekte anwenden, deshalb geht Zeile 38 vom Syntax her nicht.

    Doch, das geht.
    Nennt sich Initialisierung, dabei wird der CopyCtor aufgerfuen.

    Und eigentlich wäre ich jetzt fertig, nur in Zusammenhang mit STL Containern bricht das ganze wieder zusammen. Aus diesem Grund kommt die move semantik ins Spiel, ja?

    Nee, move kommt aus einem anderen Grund ins Spiel.
    Nehmen wir mal an du hast eine Klasse, die irgendein HANDLE kapselt. Für dieses HANDLE fehlt aber eine Kopierfunktion (warum auch immer, und ja: kam bei mir schon mal vor).
    Die Frage ist, was machst du im CopyCtor?
    Das HANDLE kopieren? Wenn ja, wer gibt es dann frei?
    Man könnte gemeinsamen Besitz implementieren (s. shared_ptr).
    Verbietet man Kopie, hat man ein Problem mit den Container.
    Deshalb gibt es move seit C++11.
    Damit hat man das Problem nicht und auch die Container kommen damit klar.



  • Nathan schrieb:

    Klaus82 schrieb:

    Schließlich möchte ich den Assignment operator verwenden, der lässt sich allerdings nur auf bereits bestehende Objekte anwenden, deshalb geht Zeile 38 vom Syntax her nicht.

    Doch, das geht.
    Nennt sich Initialisierung, dabei wird der CopyCtor aufgerfuen.

    Ja, du hast recht. Ich war unpräzise. Ich meinte damit, dass der Assignment operator für diese Art der Initialisierung nicht benutzt wird, was per se auch nicht die Aufgabe des Assignment operator ist.
    Denn dieser dient dazu, Objekten etwas zuzweisen, die bereits initialisiert sind - so besser? 🙂

    Über den Rest muss ich erst wieder nachdenken. 🙂

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Denn dieser dient dazu, Objekten etwas zuzweisen, die bereits initialisiert sind - so besser? 🙂

    Ich würde es anders sagen:

    a = b;
    

    Kopierkonstruktor wird verwendet, wenn Objekt a nicht existiert.
    Assignment operator wird verwendet, wenn Objekt a bereits existiert.



  • out schrieb:

    a = b;
    

    Kopierkonstruktor wird verwendet, wenn Objekt a nicht existiert.
    Assignment operator wird verwendet, wenn Objekt a bereits existiert.

    Aber es geht doch speziell ums Initialisieren, d.h. in der einfachen Zeile von oben

    foo f2 = f
    

    könnt man doch vermuten, dass f2 schon existiert, wenn es f zugewiesen bekommt, d.h. analog zu

    foof f2;
    f2 = f;
    

    sei. Ist es aber nicht. 🙂
    Scheinbar ist der Initailisierungprozess (oder wie man das nennen will) stärker, d.h. die obige Gleichung ist analog zu

    foo f2(f);
    

    Und das ist anscheinend per Konvention einfach so.

    Oder es war eine Designentscheidung oder es ist einfach logisch, das die obige einfache Gleichung zwingend den Kopierkonstruktor erfordert. Aber dazu kenne ich eben C++ noch zu wenig, um im Detail zu verstehen was beim Initialisierungsprozess geschieht.

    Gruß,
    -- Klaus.



  • Okay,

    also dieser Standardkonstruktor hat mir einfach keine Ruhe gelassen. Ganz einfach weil ich zum Thema Vererbung z.B. wieder gelesen habe

    Ist in der Basisklasse der Standardkonstruktor vorhanden, so braucht sich die abgeleitete Klasse nicht um die Initialisierung der geerbten Elemente zu kümmern [..]

    Und ich dachte mir nach der ersten Antwort dieses Posts: Warum sollte ich überhaupt einen Standardkonstruktor definieren, wenn er eh nichts tut und vom Compiler automatisch erzeugt wird.
    Dann hätte eine Basisklasse niemals einen Standarkonstruktor, weil ich mir es immer spare ihn aufzuschreiben!

    Aber der kleine aber feine Unterschied ist die Initialisierungsliste! Ein Standardkonstruktor darf keine Parameter haben und es darf nichts in seinem Rumpf geschehen, aber ich darf über die Initialisierungsliste Variablen (standardmäßig) inititialiseren. D.h. folgendes Beispiel wäre eine Standardkonstruktor

    struct foo
    {
        foo():a(0),b(0){}
        int a;
        int b;
    };
    

    Ein Standarkonstruktor kann also jede Menge tun, nämlich die Variablen initialisieren - aber er darf keine Parameter haben und es darf nichts im Rumpf passieren.

    Jetzt habe ich es doch, oder? 🙂

    Gruß,
    -- Klaus.



  • Doch, es darf etwas im Rumpf geschehen.
    Und definierst du überhaupt keine eigenen Konstruktoren, erstellt der Compiler für dich den Defaultkonstruktor, der tut wirklich ncihts, außer die Defaultkonstruktoren der Member aufzurufen.



  • Klaus82 schrieb:

    Und ich dachte mir nach der ersten Antwort dieses Posts: Warum sollte ich überhaupt einen Standardkonstruktor definieren, wenn er eh nichts tut und vom Compiler automatisch erzeugt wird.

    Weil dein Standardkonstruktor vielleicht was tun will, was der vom Compiler generierte nicht tut.

    Da brauchst du gar nicht mit Initialisiererlisten oder Vererbung ankommen.

    Aber der kleine aber feine Unterschied ist die Initialisierungsliste! Ein Standardkonstruktor darf keine Parameter haben und es darf nichts in seinem Rumpf geschehen, aber ich darf über die Initialisierungsliste Variablen (standardmäßig) inititialiseren.

    Blödsinn, natürlich darf im Rumpf was geschehen. Ein Standardkonstruktor definiert sich -- das hat hier auch schonmal jemand geschrieben -- nur dadurch, dass er ohne Argumente aufgerufen werden kann.

    Eigentlich heißt das Teil auch nicht Standardkonstruktor, sondern Defaultkonstruktor, aber Default ist halt kein deutsches Wort...



  • Okay,
    ich glaube ich verstehe es langsam. Ich habe damit ein wenig herumgespielt und bin bei folgendem etwas verwirrt, wenn bar eine Instanz von foo bekommt:

    #include <iostream>
    
    using namespace std;
    
    struct foo
    {
    	foo();
    	foo(const foo&);
    	int var;
    };
    
    foo::foo():var(5){ cout << "foo defaultconstructor called!" << endl;	}
    
    foo::foo(const foo& other):var(other.var){ cout << "foo copy constructor called!" << endl; }
    
    struct bar
    {
    	bar(foo);
    	foo f;
    	int var;
    };
    
    bar::bar(foo f):f(f){	cout << "bar constructor called" << endl;}
    
    int main()
    {
    	foo f;
    
    	foo f1(f);
    
    	bar b(f);
    
    return 0;
    }
    

    Denn die Ausgabe ist

    foo defaultconstructor called!
    foo copy constructor called!
    foo copy constructor called!
    foo copy constructor called!
    bar constructor called
    

    Zunächst wird in Zeile 27 die Instanz f mittels des Defaultconstructors initialisiert. Dann wird in Zeile 29 eine zweite Instanz f2 durch f erzeugt, das realisiert der Kopierkonstruktor.
    Jetzt verstehe ich aber nicht warum zwei weitere Male der Kopierkonstruktor aufgerufen wird. In Zeile 31 erzeuge ich doch in bar eine Instanz bar.foo durch das Hineinkopieren von f. Also würde ich erwarten, dass nur ein Mal der Kopierkonstruktor aufgerufen wird.
    Oder liegt es daran, dass in der Deklaration des Konstruktors von bar (Zeile 18 bzw. 23) zunächst eine lokale Kopie von foo erzeugt wird? Falls ja, warum wird dazu nicht der Defaultkonstruktor verwendet, sondern auch der Kopierkonstruktor?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Okay,
    ich glaube ich verstehe es langsam. Ich habe damit ein wenig herumgespielt und bin bei folgendem etwas verwirrt, wenn bar eine Instanz von foo bekommt:

    #include <iostream>
    
    using namespace std;
    
    struct foo
    {
    	foo();
    	foo(const foo&);
    	int var;
    };
    
    foo::foo():var(5){ cout << "foo defaultconstructor called!" << endl;	}
    
    foo::foo(const foo& other):var(other.var){ cout << "foo copy constructor called!" << endl; }
    
    struct bar
    {
    	bar(foo);
    	foo f;
    	int var;
    };
    
    bar::bar(foo f/*foo copy constructor called!*/):f(f/*foo copy constructor called!*/){	cout << "bar constructor called" << endl;}
    
    int main()
    {
    	foo f /*foo defaultconstructor called!*/;
    
    	foo f1(f/*foo copy constructor called!*/);
    
    	bar b(f)/*bar constructor called*/;
    
    return 0;
    }
    


  • Klaus82 schrieb:

    Oder liegt es daran, dass in der Deklaration des Konstruktors von bar (Zeile 18 bzw. 23) zunächst eine lokale Kopie von foo erzeugt wird? Falls ja, warum wird dazu nicht der Defaultkonstruktor verwendet, sondern auch der Kopierkonstruktor

    😕 Das würde doch keinen Sinn machen. Dann wäre der Parameter ja immer ein Defaultobjekt. call-by-value ➡ Kopie erzeugen.



  • out schrieb:

    😕 Das würde doch keinen Sinn machen. Dann wäre der Parameter ja immer ein Defaultobjekt. call-by-value ➡ Kopie erzeugen.

    Mh, jetzt wo du es sagst ...

    Aber ist das nicht wieder ziemlich umständlich? In Zeile 23 erzeugt der Konstruktor von bar zunächst eine Instanz von foo und kopiert den Inhalt dessen hinein was er übergeben bekommt: Kopierkonstruktor.

    Anschließend in der Initialisierungsliste wird die Instanz bar.f erzeugt und bekommt den Inhalt hineinkopiert, den der Parameter f hat: Kopierkonstruktor.

    Eigentlich will dich doch direkt den übergebenen Inhalt in bar.f hineinkopieren?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Aber ist das nicht wieder ziemlich umständlich? In Zeile 23 erzeugt der Konstruktor von bar zunächst eine Instanz von foo und kopiert den Inhalt dessen hinein was er übergeben bekommt: Kopierkonstruktor.

    Anschließend in der Initialisierungsliste wird die Instanz bar.f erzeugt und bekommt den Inhalt hineinkopiert, den der Parameter f hat: Kopierkonstruktor.

    Eigentlich will dich doch direkt den übergebenen Inhalt in bar.f hineinkopieren?

    Darum nimmt man dafür ja auch call-by-reference. const foo& f


Anmelden zum Antworten