Kompaktkurs C++ Themen F 1 Bereits behandelt: Klassen, Konstruktoren, Destruktoren etc. Operatoren Heute: Namensräume Vererbung
Kompaktkurs C++ Namensräume F 2 Namensräume fassen Datentypen, Variablen und Funktionen lose zu einer Einheit zusammen: namespace myspace int i; void func(); class A void elemfunc(); ; void myspace::func() i = 0; void myspace::a::elemfunc() namespace myspace void neufunc(); namespace morespace int i;
Kompaktkurs C++ Namensräume F 3 Im Gegensatz zu Klassen kann man Namensräume nachträglich erweitern. Namensräume sind keine Datentypen und haben keine Zugriffskontrolle. Um auf Elemente eines Namensraums von außerhalb zuzugreifen, stellt man ihrem Namen (analog zu Klassen) Namensraum:: voran. Namensräume können geschachtelt werden. Namensräume werden z. B. verwendet, um Namenskonflikte zwischen verschiedenen Bibliotheken zu vermeiden. Die meisten Namen aus der C++-Standard-Bibliothek sind im Namensraum std zu finden (wie z. B. std::cout, std::sqrt). Der globale Namensraum umfasst alle anderen Namensräume. Auf seine Elemente kann man mittels :: zugreifen: int i; void f() int i; i = ::i;
Kompaktkurs C++ using F 4 Durch using namespace Namensraum; kann man alle Namen eines Namensraums in einen anderen importieren: #include <iostream> using namespace std; // importiert std in den globalen Namensraum void func() cout << "Hello World!" << endl; Das ist zwar kurz, sollte aber vermieden werden, weil man damit Namenskonflikte mit der C++-Standard-Bibliothek riskiert. Alternativ kann man auch einzelne Typen, Variablen oder Funktionen importieren: #include <iostream> using std::cout; // importiert std::cout using std::endl; // und std::endl void func() cout << "Hello World!" << endl;
Kompaktkurs C++ Vererbung (inheritance) F 5 Oft besitzen verschiedene Datentypen einen gemeinsamen Kern: Kreis und Rechteck sind geometrische Figuren; sie haben eine Kantenzahl, eine Fläche, einen Umfang etc. Den gemeinsamen Kern kann man in einer Basisklasse Figur abbilden. Die abgeleiteten Klassen Kreis und Rechteck erben die Elemente des Basisklasse und verfügen über zusätzliche Eigenschaften (Seitenlänge, Radius etc.).
Kompaktkurs C++ Beispiel: Basisklasse F 6 #include <iostream> #include <cmath> class Figur protected: int Kanten_; double Flaeche_, Umfang_; public: Figur(int k, double fl, double u) : Kanten_(k), Flaeche_(fl), Umfang_(u) ; int Kanten() const return Kanten_; double Flaeche() const return Flaeche_; double Umfang() const return Umfang_; const char* Name() const return "Figur"; std::ostream& operator << (std::ostream &os, const Figur &f) return os << "Die Figur hat " << f.kanten() << " Kanten, eine Flaeche von " << f.flaeche() << ", einen Umfang von " << f.umfang() << "\n";
Kompaktkurs C++ Beispiel: abgeleitete Klassen F 7 class Kreis : public Figur protected: double Radius_; public: Kreis (double r) : Figur(0, M_PI*r*r, 2*M_PI*r), Radius_(r) ; double Radius() const return Radius_; const char* Name() const return "Kreis"; class Rechteck : public Figur protected: double Seite_[2]; public: Rechteck (double s0, double s1) : Figur(4, s0*s1, 2*(s0+s1)) Seite_[0]=s0; Seite_[1]=s1; ; double Seite(int i) const return Seite_[i]; const char* Name() const return "Rechteck";
Kompaktkurs C++ Beispiel: Hauptprogramm F 8 int main() Figur mykreis(0, M_PI, 2*M_PI); Figur myquadrat(4,1,4); Kreis kreis(1); Rechteck quadrat(1,1); std::cout << mykreis << "und ist ein(e) " << mykreis.name() << ".\n" << myquadrat << "und ist ein(e) " << myquadrat.name() << ".\n" << kreis << "und ist ein(e) " << kreis.name() << ".\n" << quadrat << "und ist ein(e) " << quadrat.name() << "." << std::endl; return 0; Die Figur hat 0 Kanten, eine Flaeche von 3.14159, einen Umfang von 6.28319 und ist ein(e) Figur. Die Figur hat 4 Kanten, eine Flaeche von 1, einen Umfang von 4 und ist ein(e) Figur. Die Figur hat 0 Kanten, eine Flaeche von 3.14159, einen Umfang von 6.28319 und ist ein(e) Kreis. Die Figur hat 4 Kanten, eine Flaeche von 1, einen Umfang von 4 und ist ein(e) Rechteck.
Kompaktkurs C++ Vererbung F 9 Um eine Klasse B von einer Basisklasse A abzuleiten, verwendet man die Definition class B : public A... ; Die Basisklasse A kann in mancherlei Hinsicht als Element der abgeleiteten Klasse B aufgefasst werden. Im Konstruktor wird sie beispielsweise wie ein Element angesprochen. Die Basisklasse wird somit quasi in die abgeleitete Klasse eingebettet. Dennoch besteht ein großer Unterschied zu class C A a;... ; Klasse B kann in eine Klasse A konvertiert werden, bei C funktionert das nicht (B ist ein A, C hat ein A). Von A abgeleitete Klassen dürfen auf die protected Elemente von A zugreifen, nicht aber auf die private Elemente. Das Schlüsselwort public bei der Ableitung bewirkt, dass die Basisklasse A öffentlich ist. Damit sind alle öffentlichen Daten von A auch in B öffentlich, und jede Funktion kann B*/B& in A*/A& konvertieren. Wird stattdessen private verwendet, dann dürfen nur noch die Elementfunktionen und Friends von B die öffentlichen Daten von A benutzen, und nur diese dürfen B*/B& in A*/A& konvertieren.
Kompaktkurs C++ Vererbung F 10 Vererbung kann sich über mehrere Generationen erstrecken, d. h., abgeleitete Klassen können selbst wieder als Basisklassen anderer Klassen dienen. Die Klasse Rechteck, abgeleitet von Figur, könnte z. B. die Basisklasse einer Klasse Quadrat sein: struct Figur... ; struct Rechteck : Figur... ; struct Quadrat : Rechteck... ; Mehrfachvererbung ist ebenfalls möglich, d. h., Klassen können von mehr als einer Basisklasse abgeleitet werden. Eine Klasse Wasserflugzeug könnte z. B. von den Klassen Boot und Flugzeug abgeleitet werden durch: class Wasserflugzeug : public Boot, public Flugzeug... ; Sie erbt dann die Daten und Funktionen beider Basisklassen.
Kompaktkurs C++ Überladen und Überschatten von Funktionsnamen F 11 struct A void f(); void f(int,int); ; struct B : A void g() f(); A::f(); // OK ; struct C : A void f(int); void g() f(); void h() A::f(); ; struct D : A using A::f; void f(int); void g() f(); f(0); ; // Die Funktionen f aus A werden ueberschattet // Fehler // OK // Alle Funktionen f aus A werden importiert // und mit einer weiteren Funktion ueberladen // OK
Kompaktkurs C++ Typkonvertierung zur Basisklasse F 12 Leider liefert folgendes Programm nicht die erhoffte Ausgabe: void print_name (const Figur &f) std::cout << "Die Figur ist ein(e) " << f.name() << ".\n"; int main() Figur mykreis(0, M_PI, 2*M_PI); Kreis kreis(1); Rechteck quadrat(1,1); print_name(mykreis); print_name(kreis); print_name(quadrat); return 0; Die Figur ist ein(e) Figur. Die Figur ist ein(e) Figur. Die Figur ist ein(e) Figur.
Kompaktkurs C++ Virtuelle Funktionen F 13 Dass bei der abgeleiteten Klasse Kreis die eigene Funktion Name und nicht die der Basisklasse Figur aufgerufen wird, selbst wenn Kreis über const Figur& angesprochen wird, kann man durch virtuelle Funktionen erreichen. Dazu muss die Funktion Name in Figur deklariert werden als: virtual const char* Name() const; Der Übersicht wegen kann man in Kreis auch virtual hinzufügen. Die Ausgabe des Programms ist nun Die Figur ist ein(e) Figur. Die Figur ist ein(e) Kreis. Die Figur ist ein(e) Rechteck. Hat eine Klasse virtuelle Funktionen, so sollte man den Destruktur ebenfalls als virtual deklarieren.
Kompaktkurs C++ Virtuelle Funktionen F 14 Eine virtuelle Funktion muss in der abgeleiteten Klasse dieselbe Signatur (Argumente und Rückgabetyp) wie in der Basisklasse haben. Eine Ausnahme bilden die kovarianten Rückgabetypen: Gibt eine virtuelle Funktion in der Basisklasse einen Zeiger oder eine Referenz auf die Basisklasse zurück, dann darf sie in der abgeleiteten Klasse eine einen Zeiger oder eine Referenz auf die abgeleitete Klasse zurückgeben: class A virtual A* func(); ; class B : A virtual B* func(); ; Passt die Signatur der Funktion in der abgeleiteten Klasse nicht, dann wird nicht die virtuelle Funktion der Basisklasse überschrieben, sondern ihr Name wird durch die neue Funktion überschattet. Die Funktion aus der Basisklasse ist dann nicht sichtbar: class A virtual void f(); ; class B : A virtual void f(int); B() f(); // Fehler! ;
Kompaktkurs C++ Vtable F 15 Dass die richtigen Funktionen aufgerufen werden, wird erreicht, indem bei jeder Klasse eine Tabelle (Vtable) mit den Adressen der anzusprechenden Funktionen mitgeführt wird. Jede Instanz der Klasse enthält einen Zeiger auf diese Tabelle. Beim Aufuf einer virtuellen Funktion wird über diesen Zeiger der Vtable ausgewählt, wo dann die Adresse der aufzurufenden Funktion zu finden ist. Da dies Speicherplatz und Rechenzeit kostet, ist dieses Verhalten nicht der Default, sondern muss durch virtual aktiviert werden. Durch explizite Angabe der Klasse kann man erzwingen, dass der Vtable übergangen wird: Figur::Name() ruft die Funktion der Basisklasse Figur auf.
Kompaktkurs C++ Vtable F 16 #include<iostream> struct A int* p; ; struct A1 : A int* q; ; struct B int* p; ~B(); ; struct B1 : B int* q; ~B1(); ; struct C int *p; virtual ~C(); ; struct C1 : C int *q; virtual ~C1(); ; int main() std::cout << sizeof(a) << << sizeof(a1) << ", " << sizeof(b) << << sizeof(b1) << ", " << sizeof(c) << << sizeof(c1) << std::endl; return 0; // Ausgabe auf 64bit-Rechner: 8 16, 8 16, 16 24
Kompaktkurs C++ Abstrakte Funktionen und Klassen F 17 Anstatt die Daten Flaeche_ und Umfang_ zu speichern, könnte man sie in Flaeche() und Umfang() berechnen. Da diese Berechnung nur mit den Daten aus den abgeleiteten Klassen möglich ist, müsste man dazu Flaeche() und Umfang() zu virtuellen Funktionen machen. Für die Basisklasse alleine könnte man aber keine sinnvollen Definitionen der beiden Funktionen angeben. Durch Anhängen von =0 deklariert man daher die Funktion als abstrakt. Eine Klasse, die eine abstrakte Funktion enthält, ist selbst abstrakt. Man kann keine Instanzen einer abstrakten Klasse direkt erzeugen, da ihr Vtable nicht vollständig ist. Eine solche Klasse dient nur als Basisklasse. Eine von ihr abgeleitete Klasse ist nicht mehr abstrakt, wenn sie die fehlenden Funktionen definiert.
Kompaktkurs C++ Beispiel: Basisklasse F 18 #include <iostream> #include <cmath> class Figur protected: int Kanten_; public: Figur(int k) : Kanten_(k) virtual ~Figur() ; int Kanten() const return Kanten_; virtual double Flaeche() const = 0; virtual double Umfang() const = 0; virtual const char* Name() const return "Figur"; std::ostream& operator << (std::ostream &os, const Figur &f) return os << "Die " << f.figur::name() << " " << f.name() << " hat " << f.kanten() << " Kanten, eine Flaeche von " << f.flaeche() << ", einen Umfang von " << f.umfang() << "\n";
Kompaktkurs C++ Beispiel: abgeleitete Klassen F 19 class Kreis : public Figur protected: double Radius_; public: Kreis (double r) : Figur(0), Radius_(r) double Radius() const return Radius_; virtual double Flaeche() const return M_PI*Radius_*Radius_; virtual double Umfang() const return 2*M_PI*Radius_; virtual const char* Name() const return "Kreis"; ; class Rechteck : public Figur protected: double Seite_[2]; public: Rechteck (double s0, double s1) : Figur(4) Seite_[0]=s0; Seite_[1]=s1; double Seite(int i) const return Seite_[i]; virtual double Flaeche() const return Seite_[0]*Seite_[1]; virtual double Umfang() const return 2*(Seite_[0]+Seite_[1]); virtual const char* Name() const return "Rechteck"; ;
Kompaktkurs C++ Beispiel: Hauptprogramm F 20 int main() Kreis kreis(1); Rechteck quadrat(1,1); std::cout << kreis << quadrat; return 0; Die Figur Kreis hat 0 Kanten, eine Flaeche von 3.14159, einen Umfang von 6.28319 Die Figur Rechteck hat 4 Kanten, eine Flaeche von 1, einen Umfang von 4
Kompaktkurs C++ Konstruktoren F 21 Konstruktoren können nicht virtuell sein. Im Normalfall ist das sinnvoll, da man ja erst ein Objekt haben muss, bevor man es über eine Referenz auf eine Basisklasse ansprechen kann. Beim Kopier-Konstruktor ist die Situation allerdings eine andere: Man hat bereits ein Objekt und will eine genaue Kopie davon anlegen. Spricht man das Objekt über eine Referenz auf eine Basisklasse an, dann wird leider nur der Basisteil kopiert. Hier hilft ein Trick: In der abstrakten Basisklasse deklarieren wir virtual Figur* Copy() const = 0; In den anderen Klassen (hier für Kreis) definieren wir Kreis* Copy() const return new Kreis(*this); Die virtuelle Funktion Copy liefert jetzt einen Zeiger auf eine exakte Kopie zurück. Leider muss man diese Kopie wieder mit delete zerstören.
Kompaktkurs C++ Casts F 22 Neben den bereits kennengelernten Typkonvertierungen wie (double)3 und double(3) gibt es 4 weitere Typumwandlungen in C++: const int *p; int *q = const_cast<int*>(p); ändert const-qualifizierer. Kreis *p; Figur *q = static_cast<figur*>(p); führt eine Typumwandlung durch, wobei ein Zeiger eventuell neu ausgerichtet wird. Figur *p; Kreis *q = dynamic_cast<kreis*>(p); testet die Typinformation zur Laufzeit, bevor eine Umwandlung wie bei static_cast durchgeführt wird. Figur *p; Kreis *q = reinterpret_cast<kreis*>(p); wandelt den Zeiger um, ohne ihn neu auszurichten.
Kompaktkurs C++ Casts: const_cast, static_cast F 23 void func(const int *p) int i = int(3.5); // C-Notation int j = static_cast<int>(3.5); // C++-Notation (genauer) int k = const_cast<int>(3.5); // Fehler, kein Zeiger int *q = (int*)p; // C-Notation int *r = const_cast<int*>(p); // C++-Notation (genauer) int *s = static_cast<int*>(p); // Fehler, const ignoriert
Kompaktkurs C++ Casts: Neuausrichtung von Zeigern F 24 #include<iostream> A B struct A int i; ; struct B int j; ; struct C : A, B int k; ; i j k int main() C C c; B* p = static_cast<b*>(&c); B* q = reinterpret_cast<b*>(&c); B* r = (B*)&c; std::cout << &c << << p << << q << << r << std::endl; // 0x7fffffc2a970 0x7fffffc2a974 0x7fffffc2a970 0x7fffffc2a974 c.i = 1; c.j = 2; c.k = 3; std::cout << c.i << << c.j << << c.k << std::endl; // 1 2 3 p->j = 4; std::cout << c.i << << c.j << << c.k << std::endl; // 1 4 3 q->j = 7; std::cout << c.i << << c.j << << c.k << std::endl; // 7 4 3 return 0;
Kompaktkurs C++ Casts: Dynamische Typinformation F 25 #include <iostream> struct Figur virtual ~Figur() ; struct Kreis : public Figur ; void DynCast (Figur *p) Kreis *q = dynamic_cast<kreis*>(p); if (q) std::cout << "Das ist ein Kreis!" << std::endl; else std::cout << "Das ist kein Kreis!" << std::endl; int main() Kreis kr; Figur fig; DynCast(&kr); // Das ist ein Kreis! DynCast(&fig); // Das ist kein Kreis! return 0;
Kompaktkurs C++ Typinformation: typeid, type_info F 26 Der Operator typeid liefert Informationen über einen Typ. Dazu gibt er eine Referenz auf eine Instanz der Klasse type_info zurück, die im Header <typeinfo> definiert ist. Die Klasse type_info enthält eine Element-Funktion name, die den (vom Compiler benutzten) Namen des Typs zurückgibt. Man kann zwei Instanzen der Klasse type_info auch auf Gleichheit testen. #include<typeinfo> #include<iostream> struct A a1, a2; struct B b; int main() std::cout << typeid(a1).name() << \n // 1A << (typeid(a1)==typeid(a2)) << \n // 1 << (typeid(a1)==typeid(b)) << std::endl; // 0 return 0;
Kompaktkurs C++ Typinformation: typeid, type_info F 27 #include<typeinfo> #include<iostream> void func(int,double); struct A ; struct X virtual ~X() ; struct B : A b; struct Y : X y; struct S struct T ; ; namespace M struct A ; int main() std::cout << typeid(0).name() << \n // i << typeid(0.0).name() << \n // d << typeid(func).name() << \n // FvidE << typeid(a).name() << \n // 1A << typeid(s::t).name() << \n // N1S1TE << typeid(m::a).name() << \n // N1M1AE << typeid(b).name() << \n // 1B << typeid(*(a*)&b).name() << \n // 1A << typeid(*(x*)&y).name() << std::endl; // 1Y return 0;