Grundbegriffe: Pods und Objects (class und struct) Pod = Plain Old Data: Es handelt sich dabei hauptsächlich um die schon in C eingebauten Datentypen wie z.b. Ganzzahltypen: char, short, int, long, long long (auch unsigned) Gleitkommatypen: float, double, long double bool alle Pointer C-Arrays von solchen Typen struct und class-typen, die nur Pod-Attribute enthalten und weder Konstruktoren, Destruktor oder Zuweisungsoperatoren haben Definiert man Variablen (Instanzen) von diesem Typ, erfolgt kein automatischer Aufruf einer Funktion. Lokale Pod-Variablen werden nicht implizit initialisiert (d.h. sie haben nach der Definition einen zufälligen Wert, sofern sie nicht explizit initialisiert werden) : int x; int y = 1; // x hat hier einen undefinierten Startwert // y wird hier explizit auf 1 initialisiert Werden Pods gelöscht (z.b. lokale Variablen beim Verlassen dieser Funktion), erfolgt ebenfalls kein automatischer Aufruf einer Funktion. Objekttypen = Mittels class oder struct definierte Datentypen, die sowohl Daten- Attribute als auch Funktionen (= Methoden) haben können. Ein Objekt ist kein Pod, wenn wenigstens ein Datenattribut kein Pod ist oder aber ein Konstruktor, Destruktor oder Zuweisungsoperator definiert ist. Definiert man Variablen von diesem Typ (man "instanziert" die Objekttypen), werden IMMER spezielle Funktionen aufgerufen (die Konstruktoren, constructors, oft kurz ctor genannt). Werden solche Variablen vernichtet, werden ebenfalls IMMER Funktionen aufgerufen (Destruktor, destructor oft kurz dtor genannt). Klasse und Struktur (class and struct) Implementiert man in C++ einen etwas komplexeren Datentyp, wählt man dafür zumeist eine Klasse (class), seltener eine Struktur (struct). Beide Varianten unterscheiden sich nur in der Voreinstellung der Zugriffsrechte (class: private, struct: public) class MyClass { // die Bestandteile der Klasse, default private:
}; // der Strichpunkt ist Wichtig!!!! struct MyStruct { // die Bestandteile der Struktur, default public: }; // der Strichpunkt ist Wichtig!!!! Bestandteile der Objekttypen können sein (nicht vollständig): Datenobjekte, sog. Attribute: z.b. int x, y; Es spielt keine Rolle, wie komplex die Attribute sind. Auch Instanzen anderer Klassen oder Arrays davon sind selbstverständlich möglich. Funktionsdeklarationen (= Klassenfunktionen = Methoden), z.b. int func1(int); // mit Strichpunkt am Ende inline-funktionsdefinitionen: d.h. inclusive Code der Methode int func1(int a) { return a+1; } // kein Strichpunkt am Ende nach } friend-funktionsdeklarationen: z.b. friend double f(double); friend-funktionsdefinitionen: definiert auch den Code dieser Funktion friend double f(double x) {...; return...} friend-klassen: z.b. friend class AnotherClass; (macht alle Methoden von AnotherClass zu friend-funktionen). Typdeklarationen mittels typedef oder using (diese gelten dann nur innerhalb dieser Objekttypen) Auf die Bestandteile des Objekts wird wie bei C-Strukturen zugegriffen: MyClass c; c.x = 1; c.func1(5); // Eine Objekt-Instanz // das Objekt c hat das Attribut x // das Objekt c hat die Methode func1(), // diese wird mit Argument 5 aufgerufen Zusätzlich kann/soll man noch den Zugriffsschutz der Objekt-Bestandteile explizit definieren. Alles nach public: in der Objektdefinition darf im gesamten Programm verwendet werden. Alles nach private: darf nur in den Objektmethoden und in friend-funktionen verwendet werden. Alles nach protected: darf nur in den Objektmethoden, in friend-funktionen und zusätzlich auch in Methoden der abgeleiteten Objekte verwendet werden (Vererbung!). Diese 3 Labels können in beliebiger Reihenfolge
und Anzahl in der Objektdefinition aufscheinen. Normalerweise verwendet man aber jedes Label höchstens einmal. Die oft empfohlene Reihenfolge ist public: vor protected: (dieses fehlt zumeist ganz) und private: Der Konstruktor, constructor, ctor: Konstruktoren sind Methoden, die gleich wie der Objekttyp heißen. Eine Klasse kann beliebig viele Konstruktoren besitzen, wenn diese sich in der Argumentliste unterscheiden (Überladen von Funktionen). Konstruktoren haben KEINEN Rückgabetyp (kein void!!!). Ein Konstruktor, der ohne Argumente aufrufbar ist, heißt "Standardkonstruktor" oder "default constructor". Er hat keine Argumente oder alle seine Argumente besitzen Defaultwerte. Konstruktoren werden eigentlich NIE explizit aufgerufen, ihr Aufruf erfolgt bei jeder Instanzierung von Objekten (= man definiert Variablen von diesem Objekttyp). Bei jeder Definition einer Objektinstanz wird ein Konstruktor aufgerufen, es handelt sich daher IMMER um ausführbaren Code. Es gibt mehrere zulässige Schreibweisen: Komplexe_Zahl z; Komplexe_Zahl z{}; -> Komplexe_Zahl() (= Default-K.) Komplexe_Zahl z(1.) Komplexe_Zahl z = 1. Komplexe_Zahl z{1.} Komplexe_Zahl z = {1.} -> Komplexe_Zahl(1.) Komplexe_Zahl z(1., 2.0) Komplexe_Zahl z{1., 2.} Komplexe_Zahl z = {1., 2.} -> Komplexe_Zahl(1., 2.) Komplexe_Zahl z{1., 2., 3.} -> Komplexe_Zahl(1., 2., 3.) 1) Auch Konstruktoren können public oder private oder protected sein. 2) Alle benötigten (= verwendeten) Konstruktoren müssen für das Objekt auch definiert sein, sonst is das ein Fehler. 3) Die von C++ empfohlene Aufrufart von Konstruktoren ist seit C++11 jene mit {}, die aber inkompatibel zu älteren Compilern ist. Bei STL-Containern ist der Aufruf mittels ( ) verschieden vom Aufruf mit { }. 4) Bei allen Konstruktoren (aber nur bei diesen) sind Initialisierungslisten für die Objektattribute zulässig und empfohlen. Mit diesen kann man Konstruktor-Aufrufe für die Attribute festlegen. Die Reihenfolge muss der Objektdefinition entsprechen.
Ein hier fehlendes Attribut (das selbst Objekt ist) wird mit seinem default constructor initialisiert. Ist in einem Objekttyp (class oder struct ist egal, aber es darf kein Pod sein!) kein Konstruktor definiert und es wird ein default constructor benötigt, fügt C++ zur Klasse einen trivialen public default constructor hinzu, dessen Code nur aus {} besteht. Schreibt man selbst einen Konstruktor für das Objekt, fügt C++ diesen Default Konstruktor nicht mehr automatisch dazu. Man kann diesen (wenn man will) selbst schreiben oder C++ auffordern, ihn trotzdem zu erzeugen: Komplexe_Zahl() = default; // Bitte liebes C++, generiere diesen D.K. Ein sinnvollerdefault- Konstruktor für komplexe Zahlen, der alle Fälle abdeckt, wäre Komplexe_Zahl(double x = 0., double y = 0.) : re{x}, im{y} // Initialisierungs-Liste, Konstruiere re aus x, im aus y {} // nichts mehr zu tun Komplexe_Zahl z; // z = 0 + 0*i Komplexe_Zahl z{1.}; // z = 1. + 0*i Komplexe_Zahl z{1., 2.} // z = 1.+ 2.*i Beachte, dass nach der Definition dieses neuen Konstruktors obige Objektdefinitionen immer Konstruktoraufrufe sind und nicht mehr Initialisierungen der struct-attribute. Der Kopier-Konstruktor (copy constructor) Der Kopierkonstruktor ist ein Konstruktor, der ein Objekt als Kopie eines anderen Objekts vom gleichen Typ initialisiert. Es gibt im Wesentlichen nur folgende erlaubte Signaturen: Komplexe_Zahl(const Komplexe_Zahl& original) // Übergabe per konstanter Referenz Komplexe_Zahl(Komplexe_Zahl& original) // Übergabe per Referenz Besitzt eine Klasse keinen Kopierkonstruktor und es wird einer benötigt, fügt C++ einen einfachen public: Kopierkonstruktor hinzu, der alle Teile des Originals kopiert (d.h. deren Kopierkonstruktoren aufruft). Eine (hier unnötige) Implementierung wäre: Komplexe_Zahl(const Komplexe_Zahl& original) : re{original.re}, im{original.im} {}
Aufrufe des Kopierkonstruktors: Komplexe_Zahl original{1., 2.}; Komplexe_Zahl z1(original); Komplexe_Zahl z2{original}; Komplexe_Zahl z3 = original; Komplexe_Zahl z3 = {original}; // definiere das original // prä-c++11 // C++11 // das ist keine Zuweisung!!!! // C++11 Der Zuweisungsoperator, copy-assignment operator (operator=) Der Zuweisungsoperator wird verwendet, um einer Instanz den Inhalt einer anderen zuzuweisen. Ein =-Zeichen in einer Definition ist KEINE Zuweisung sondern eine alternative Schreibweise für einen Konstruktor. Es gibt 2 erlaubte Signaturen (rhs steht für "right hand side"): Komplexe_Zahl& operator=(const Komplexe_Zahl& rhs) // Übergabe von rhs per konstanter Referenz Komplexe_Zahl& operator=(komplexe_zahl rhs) // Übergabe von rhs per Wert Da die Zuweisung bei Pods in C/C++ immer den zugewiesenen Wert als Ergebnis des Operators hat, ist das auch bei diesem Objekt-Zuweisungsoperator beizubehalten, d.h. der Rückgabewert ist eine Referenz auf das Referenzobjekt. Damit wäre eine sinnvolle (aber unnötige s.u.) vollständige Implementierung: Komplexe_Zahl& operator=(const Komplexe_Zahl& rhs) { re = rhs.re; im = rhs.im; // kopiere die Attibute return *this; // gib Referenz zurück } Komplexe_Zahl a, b, c; a = b = c = Complex{0, 1}; // setze alle 3 auf i Falls man einen Zuweisungsoperator schreibt, MUSS dieser auch bei Zuweisungen an sich selbst funktionieren, d.h. ein Statement wie z = z; darf nicht zu einem Fehler führen. Complex z1 = 5, z2 = z1; // entspricht z1{5}, z2{z1} KEINE Zuweisung z1 = z2; // Zuweisungsoperator Besitzt eine Klasse keinen Zuweisungsoperator und es wird einer benötigt, fügt C++ einen einfachen public: Zuweisungsoperator hinzu, der alle Teile des Originals an jene des Ziels zuweist.
Seit C++11 heißt dieser Zuweisungsoperator übrigens copy assignment operator, da es ab dann auch einen move assignment operator gibt/geben kann. Der Destruktor, destructor, dtor Er hat immer den Namen ~Objekttypname(), es darf nirgends void stehen. Deshalb kann es auch nur einen einzigen Destruktor pro Objekttyp geben. Er wird fast NIE explizit aufgerufen sondern wird von C++ vollautomatisch aufgerufen, falls eine Instanz vernichtet wird (lokale Objekte beim Ende des Blocks in { }, globale Objekte am Programmende nach main()). Sehr häufig macht man sich diese Automatik-Funktion zunutze, um automagisch Funktionen ohne expliziten Aufruf einzusetzen (z.b. std::lock_guard sperrt das Schloss auf, std::unique_ptr zerstört seinen Inhalt, std::fstream schließt sich vollautomatisch). Besitzt eine Klasse keinen Destruktor und es wird einer benötigt, fügt C++ einen einfachen public: Destruktor hinzu, der nur aus {} besteht. Der move constructor (C++11) Seit C++11 kann ein Objekttyp auch einen solchen Konstruktor besitzen, der eine Instanz als Kopie eines temporären Originals erstellt und dabei dessen Ressourcen "klaut", wann immer das möglich ist. Alle STL-Containerklassen besitzen einen solchen Konstruktor, ebenso die std::string Klasse. Dieser Konstruktor ist nur dann wirklich sinnvoll, wenn die Klassen über dynamische Inhalte verfügen, die vom Original zum Ziel verschoben werden können (deshalb: move). Obwohl das bei der Klasse Komplexe_Zahl nicht zutrifft, könnte man ihn trotzdem definieren: Komplexe_Zahl(Komplexe_Zahl&& temp) : re{temp.re}, im{temp.im} {} // Übergabe per rvalue-referenz Komplexe_Zahl z1{1, 2}; Komplexe_Zahl z2{sin(z1)}; // sin(z1) ist eine temporäre Komplexe_Zahl -Instanz Besitzt eine Klasse keinen move-constructor und es könnte einer eingesetzt werden, so fügt C++ unter bestimmten Voraussetzungen einen einfachen public: move constructor hinzu.
Der move assignment operator (C++11) Dieser Zuweisungsoperator wird verwendet, um einer Instanz den Inhalt einer temporären anderen zuzuweisen und dabei deren Ressourcen zu "klauen". Obwohl er für die Klasse Komplexe_Zahl keine Vorteile bietet, könnte man ihn wie folgt programmieren: Komplexe_Zahl& operator=(komplexe_zahl&& temp) { re = temp.re; im = temp.im; // kopiere die Attibute (keine Init-Liste!) return *this; // gib Referenz zurück } Komplexe_Zahl z1{1,2}, z2; z2 = sin(z1); // die rechte Seite ist ein temporary (rvalue) Besitzt eine Klasse keinen move assignment operator und es könnte einer eingesetzt werden, so fügt C++ unter bestimmten Voraussetzungen einen einfachen public: move assignment operator hinzu. Die Übergabe des Referenzobjekts Bei jedem expliziten oder impliziten Methodenaufruf gibt es eine Referenzinstanz: Komplexe_Zahl z{1}; // implizit: Aufruf von z.complex(1), z ist die Referenzinstanz z.abs(); // explizit: z die Referenzinstanz Die Referenzinstanz taucht nie in der Argumentliste der Methoden auf und wird auch NIE deklariert. Innerhalb jeder nichtstatischen Klassenmethode (nicht aber in friend- Funktionen) gibt es den Pointer this,der auf die Referenzinstanz zeigt. In den meisten Fällen darf man bei Zugriffen auf die Attribute oder Methoden des Referenzobjekts das this-> weglassen. Bei den komplexen Zahlen steht z.b. im Konstruktor einfach Komplexe_Zahl(double r = 0, double i = 0): re{r}, im{i} {} statt: Komplexe_Zahl(double r = 0, double i = 0) : this->re{r}, this->im{i} {}
const, aber richtig Es ist guter Stil in C++ (und manchmal auch Pflicht), alle Argumente, die per Pointer oder Referenz übergeben werden und die innerhalb einer Funktion nicht verändert werden (auch nicht auf dem Umweg über weitere Funktionen!) mit dem Zusatz const zu versehen. Wird die Referenzinstanz durch eine Methode nicht verändert, so nennt man diese eine konstante Methode und man schreibt das const nach die schließende Klammer ) vor dem ; oder { void print() const; // gib die komplexe Zahl (die Referenzinstanz) aus, diese bleibt dabei unverändert. C++ beachtet diese Zusätze genauestens. Unterscheidet sich die Funktionsdefinition auch nur in einem const-zusatz, ist es eine andere Funktion: void print() {} // eine andere Print-Funktion oder Methode Man kann Klassenattribute mit dem Schlüsselwort mutable kennzeichnen. Dann zählt eine Veränderung dieser Attribute nicht als Veränderung der Klasseninstanz und ist somit auch in konstanten Methoden erlaubt. Destruktoren, Konstruktoren und Initialisierungslisten, Typ-Umwandlung Jeder Konstruktor und Destruktor (auch wenn der Code nur{} ist) führt vollautomatisch weitere Dinge aus: Konstruktor: VOR dem eigenen Code (also vor den Statements zwischen { und }) ruft der Konstruktor für alle Basis-Objekttypen, von denen geerbt wurde, und für alle Attribute der Klasse, die selbst Objekte (keine Pods) sind, deren Standardkonstruktor auf, falls keine Initialisierungsliste vorhanden ist. Falls diese Teile selbst wieder Attribute besitzen, werden auch diese konstruiert usw. Gibt es eine Initialisierungsliste, wird anstelle des Standardkonstruktors der dort angegebene Konstruktor aufgerufen. Fehlen Attribute in der Initialisierungsliste, die selbst Objekte sind, so werden diese mit deren Default-Konstruktor erzeugt. Fehlen Attribute in der Initialisierungsliste, die Pods sind, so werden diese nicht initialisiert. Man kann diese jedoch alternativ mittels Zuweisungen im Programmteil des Konstruktors auf vernünftige Anfangswerte setzen. Destruktor: Nach dem eigenen Code ruft der Destruktor auch die Destruktoren aller Attribute, die selbst Objekte sind, und der Basis-Objekte, falls es welche gibt, auf.
Initialisierungsliste: Diese steht (bei allen Konstruktoren und nur bei diesen) optional zwischen den Funktionsklammern ( ) und dem Code { }. Nach einem Doppelpunkt folgen die Konstruktoraufrufe von einigen oder allen Basisklassen und Attributen. So kann man selbst festlegen, mit welchem Konstruktor die Attribute erzeugt werden. Eine Initialisierungsliste ist Pflicht, falls wenigstens eine Basisklasse oder ein Attribut, das Objekt ist, keinen Standardkonstruktor besitzt; falls ein konstantes Attribut initialisiert werden muss; wenn ein Attribut eine Referenz ist. Darüber hinaus ist es oft effizienter, den Konstruktor selbst auszuwählen. Die Reihenfolge der Konstruktoraufrufe MUSS der Reihenfolge in der Objekt-Definition entsprechen (sonst gibt es Compiler-Warnungen): korrekt: Komplexe_Zahl(double r = 0, double i = 0): re{r}, im{i} {} falsche Reihenfolge: Komplexe_Zahl(double r = 0, double i = 0): im{i}, re{r} { } Seit C++11 darf eine Initialisierungsliste auch einen anderen Konstruktor derselben Klasse aufrufen (constructor delegation). Eine nicht sehr elegante Version hier: Komplexe_Zahl(double r, double i) : Komplexe_Zahl{r} // verwendet diesen ctor { im = i; } // und setzt nur noch den Imaginärteil Typ-Umwandlung durch Konstruktor: Konstruktoren, die mit genau einem Argument eines anderen Typs aufgerufen werden können, werden von C++ auch als Typumwandlungs- Funktionen verwendet. Im obigen Fall würde der Konstruktor Komplexe_Zahl(double r) (ruft Komplexe_Zahl(r, 0.) auf) auch als Umwandlungsfunktion von double in Komplexe_Zahl verwendet werden, was mathematisch sogar Sinn macht. Also wird man diese Zusatzfunktion dort wohl beibehalten. Bei den Brüchen ist die Umwandlung von Integern bei Bedarf in Brüche mit Nenner 1 ebenfalls sinnvoll. Bei den 2d- Vektoren will man jedoch Skalare auf keinen Fall in Vektoren umwandeln: Will man einen Konstruktor NICHT als Umwandlungsfunktion einsetzen, schreibt man vor ihn das Schlüsselwort explicit: Komplexe_Zahl(double = 0., double = 0.) ist auch Typumwandlung explicit Komplexe_Zahl(double = 0., double = 0.) ist keine.
Die Regel der großen Drei Das ist eine Faustregel in C++, die fast immer stimmt. Die großen Drei sind der Destruktor, der Copy-Konstruktor und der Kopier-Zuweisungsoperator operator=. Alle 3 werden von C++ u.u. automatisch erzeugt, wenn der Programmierer diese Methoden nicht selbst programmiert hat. Die Regel lautet: Benötigt eine Klasse unbedingt eine selbstgeschriebene Version von EINEM der großen Drei, benötigt sie ziemlich sicher selbstgeschriebene Versionen von ALLEN 3. Der Grundsatz gilt natürlich nicht, wenn man z.b. den Destruktor nur für Debugging-Zwecke geschrieben hat, damit er Log-Einträge erzeugt.