lernen:eigentum
no way to compare when less than two revisions
Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
— | lernen:eigentum [2020-07-27 09:22] (aktuell) – angelegt - Externe Bearbeitung 127.0.0.1 | ||
---|---|---|---|
Zeile 1: | Zeile 1: | ||
+ | ====== Eigentum verpflichtet - Der Umgang mit dynamischem Speicher in C++ ====== | ||
+ | |||
+ | > Eigentum verpflichtet. Sein Gebrauch soll zugleich dem Wohle der Allgemeinheit dienen. | ||
+ | >> | ||
+ | |||
+ | Zeiger und dynamischer Speicher erfordern solides Handwerk bei ihrem Einsatz. Zeiger in Prozeduren und in Objekten, auch unter Ausnahmebedingungen werden betrachtet. | ||
+ | |||
+ | ===== Sterne ===== | ||
+ | |||
+ | Es ist mal wieder Weihnachten. Zeit, Sterne zu betrachten. Der folgende Quelltextabschnitt enthält auch welche. Leider stürzt das Programm ab: | ||
+ | |||
+ | <code cpp> | ||
+ | /*1*/ int *p = new int(1); | ||
+ | /*2*/ int *q = new int(2); | ||
+ | /*3*/ p = q; | ||
+ | /*4*/ delete q; | ||
+ | /*5*/ std::cout << *p << ' | ||
+ | /*6*/ delete p; // spätestens hier. | ||
+ | </ | ||
+ | |||
+ | Wieviel Fehler sind im Quelltext? Wo? Haben Sie sie entdeckt? Falls nicht, hier die Fakten: | ||
+ | |||
+ | ===== Grundlagen: Zeiger und dynamischer Speicher ===== | ||
+ | |||
+ | Zeiger werden mit Sternen nach der Typangabe vereinbart. Sie sollten auf Speicher verweisen, wo die erwarteten Inhalte stehen und mit Sternen auf den Inhalt zugegriffen wird: | ||
+ | |||
+ | <code cpp> | ||
+ | std::cout << *p << ' | ||
+ | </ | ||
+ | |||
+ | Zeiger müssen dazu vorher eine gültige Speicheradresse erhalten, auf die zugegriffen werden kann. Herumirrende, | ||
+ | <code cpp> | ||
+ | /*7*/ q = p = NULL; // Vorschlag für Zeile 7 | ||
+ | </ | ||
+ | Leider nützt das nichts, das Programm ist ja vorher abgeschmiert. Nur wo, schon gefunden? | ||
+ | |||
+ | Bei Bedarf kann dynamisch Speicher mit dem Operator new vom System angefordert und nach Gebrauch wieder mit delete freigegeben werden. Die Zeilen 1 und 2 fordern jeweils eine Speichereinheit für einen int-Wert an und belegen diese mit den Werten 1 und 2. Preisfrage: Welcher Wert hätte in Zeile 5 ausgegeben werden sollen? Vermutlich 2? Warum? | ||
+ | |||
+ | Schon die Speicheranforderung kann scheitern. Je nach Compiler liefert new dann NULL, einen ungültigen Zeiger, oder in new wird eine bad_alloc-Ausnahme geworfen und der normale Programmlauf unterbrochen. (Mehr zu Ausnahmen weiter unten. Hier reicht zu wissen, dass dann folgende Anweisungen überhaupt nicht mehr abgearbeitet werden.) Eine Debugger-Ausschrift, | ||
+ | |||
+ | Na, dämmert es? Es können nur die ursprünglichen Zeilen 3 und 4 sein. Falls Sie den Fehler noch nicht gefunden haben, lesen Sie diese Seite noch mal aufmerksam (goto Grundlagen; | ||
+ | |||
+ | ===== Speicher: Lecks, Leichen und Korruption ===== | ||
+ | |||
+ | Jeder angeforderte Speicherplatz muss wieder freigegeben werden. Speicherbereiche, | ||
+ | |||
+ | Das Programmbeispiel stürzt aber viel schneller ab. Es enthält zwei new und zwei delete. Trotzdem reicht das nicht: Es ist genau der angeforderte Speicher freizugeben, | ||
+ | |||
+ | Sie haben den Fehler schon gefunden, sonst würden sie hier nicht lesen. Falls nicht, zurück auf Seite 1 (malen Sie sich auf, was passiert)! Wenn Sie die Lösung kennen, überspringen Sie den nächsten Absatz, denn er ist nur langweilig... | ||
+ | |||
+ | Für alle Undisziplinierten hier die Auflösung. In Zeile 3 sollte vielleicht der Wert 2 aus dem Speicher, auf den q zeigt, in den Speicher, auf den p zeigt, übertragen werden. Allerdings wurden die Sternchen vor dem p und dem q vergessen: *p = *q. Dieser Fehler ist hier offensichtlich, | ||
+ | |||
+ | * Fehler 1: Der Speicher mit der Zahl 1 kann nicht mehr angesprochen werden, weil seine Adresse überschrieben wurde, er wurde zur Speicherleiche. | ||
+ | * Fehler 2: Dafür wird der Speicher q doppelt referenziert. Zwischen einzelnen Programmteilen (zwischen Zeile 3 und 4) können jetzt Daten auf merkwürdige Weise hin und her wandern. Gewöhnlich fragt der Fehlersuchende dann: "Wieso steht hier ein anderer Wert drin? Den habe ich doch gar nicht angefasst!" | ||
+ | * Fehler 3 steckt in Zeile 4. Der doppelt referenzierte Speicher mit der 2, auf den q zeigt, wird gelöscht. Damit zeigt p auf einen nicht mehr gültigen Speicherbereich, | ||
+ | * Fehler 4 in Zeile 5: lesender Zugriff auf ungültige Speicheradresse. Ein ordentlicher Betriebssystem bemerkt den Fehler und beendet das Programm: Schutzverletzung / Segmentierungsfehler. Ein unordentliches System lässt uns weiter ins Verderben laufen. | ||
+ | * Fehler 5 in Zeile 6: Der selbe, schon zurückgegebene Speicher wird ein zweites Mal zurückgebracht. Ein guter Bibliothekar merkt, das etwas nicht stimmt, wenn Sie zweimal hintereinander dasselbe Buch zurückbringen, | ||
+ | |||
+ | Eine weitere Chance, den Hauptspeicher durcheinander zu bringen, gibt es beim Verwechseln der Speicherverwaltung für Einzelobjekte und Felder von Objekten. | ||
+ | <code cpp> | ||
+ | p = new int(3); // eine Ganzzahl mit Wert 3 | ||
+ | q = new int[4]; // ein Feld mit 4 int-Werten | ||
+ | // ... | ||
+ | delete p; // Freigabe Einzelobjekt | ||
+ | delete [] q; // Freigabe Feld ... nie verwechseln! | ||
+ | </ | ||
+ | |||
+ | Verwechslungen dabei sind schwierig zu entdecken, weil es sich nur um eine harmlose Klammer handelt. Auf die Gefahr, mich als unfähig bloßzustellen: | ||
+ | |||
+ | ===== Ausnahmezustand: | ||
+ | |||
+ | Komplizierter wird die Kiste, wenn in C++ auch noch Ausnahmen ins Spiel kommen. Wenn irgendwo in den Tiefen des Programms etwas nicht klappt, kann man eine Ausnahme werfen: | ||
+ | |||
+ | <code cpp> | ||
+ | if (nenner == 0) throw DivByZeroError(); | ||
+ | bruch = zaehler / nenner; | ||
+ | </ | ||
+ | |||
+ | Der normale Programmlauf wird abgebrochen, | ||
+ | <code cpp> | ||
+ | try | ||
+ | { | ||
+ | // unsicherer Abschnitt, | ||
+ | // enthält Unterprogrammrufe mit Fehlermöglichkeit | ||
+ | } | ||
+ | catch(DivByZeroError& | ||
+ | { | ||
+ | cerr << e.what() << endl; // Fehlerausschrift | ||
+ | } | ||
+ | // normal weiter | ||
+ | </ | ||
+ | Nur, was machen wir mit dem zwischendurch dynamisch angeforderten Hauptspeicher? | ||
+ | <code cpp> | ||
+ | q = new int[100]; | ||
+ | try | ||
+ | { | ||
+ | // unsicherer Programmteil | ||
+ | } | ||
+ | catch(...) | ||
+ | { | ||
+ | delete [] q; // Speicher zurückgeben | ||
+ | throw; | ||
+ | } | ||
+ | // ... | ||
+ | delete [] q; // Normalfall | ||
+ | </ | ||
+ | Der Quelltext wird unleserlich, | ||
+ | |||
+ | Programmierer, | ||
+ | |||
+ | ===== Müllabfuhr ===== | ||
+ | |||
+ | Ein Müllsammler (garbage collector) kümmert sich um nicht mehr benutzte Speicherbereiche und führt sie den System wieder zu. Das ist eine feine Sache, wenn sie funktioniert. In einigen Sprachen ist ein solches System eingebaut. Neben Vorteilen gibt es aber auch Nachteile. Der Müllsammler nimmt dem Programmierer einen Teil der Arbeit ab und bürdet sie der Laufzeit-Umgebung auf. Von Zeit zu Zeit wuselt im Hintergrund der Müllsammler und frisst Rechenzeit, gemeinerweise immer dann, wenn viel Arbeit zu erledigen ist und der Speicher heftig genutzt wird (nur dann wird er so knapp, dass der garbage collector aktiv wird). Echtzeitanwendungen sind damit schwer realisierbar: | ||
+ | |||
+ | Der Ansatz von C++ ist ein anderer. Der Programmierer weiß (hoffentlich) am besten, wann ein Objekt nicht mehr benutzt wird. Ein Gleichnis: Wer in die Sächsische Schweiz boofen (übernachten) geht, nimmt den Müll wieder mit, um nicht dem nächsten den Schlafplatz als Müllhalde zu hinterlassen. (Verlasse den Ort so, wie du ihn vorzufinden wünschst.) Systeme, die sich auf garbage collection verlassen, ähneln den Wiesen um die Elbbrücken nach dem Jahreswechsel. Die Stadtreinigung braucht ungefähr drei Wochen im Januar, um Sektflaschen, | ||
+ | |||
+ | XXX geht nicht in Sprache YYY, ist ein beliebtes Argument. Es zeigt meist nur die Inkompetenz derer, die sich streiten. Kaum einer kennt mehrere Systeme gleich gut, um ein faires Urteil zu treffen. Müllsammler gibt es als Bibliotheken sowohl für C als auch für C++. Man muss sie nur aus dem Internet besorgen und einbinden, wenn man das will. Einen fest eingebauten Müllsammler abschalten, scheint mir schwieriger (Beweis meiner Inkompetenz). | ||
+ | |||
+ | Ein Teil der Philosophie von C++ ist, den Programmierer von Routineaufgaben zu befreien. Mein Ziel ist in diesem Text, Techniken zu zeigen, die den Umgang mit dynamischem Speicher erleichtern und sicherer machen, die Lesbarkeit des Quelltextes erhöhen und auch beim Auftreten von Ausnahmen sicher funktionieren. | ||
+ | |||
+ | Grundidee aller Ansätze ist, die Zeiger in Objekte zu kapseln. Konstruktoren allokieren Speicher, der Destruktor gibt den Speicher frei, sobald das Objekt selbst vernichtet wird, also auch bei Ausnahmen. Einmal festgelegt, laufen die Prozesse automatisiert ab. Das Wunderbare daran ist, dass das mit wenigen Zeilen Quelltext erreichbar ist. | ||
+ | |||
+ | ==== Standardcontainer für Objektmengen ==== | ||
+ | |||
+ | Containerklassen vector< | ||
+ | <code cpp> | ||
+ | { | ||
+ | std:: | ||
+ | // ... Arbeit mit v[i], i = 0..anzahl | ||
+ | } // automatische Freigabe durch Destruktor | ||
+ | </ | ||
+ | |||
+ | ==== Einzelne Zeiger einkapseln ==== | ||
+ | |||
+ | Die in der Standard-Bibliothek < | ||
+ | std:: | ||
+ | Ein unique_ptr kümmert sich selbst um die Freigabe des gekapselten Zeigers auf ein einzelnes Objekt: | ||
+ | |||
+ | <code cpp> | ||
+ | { | ||
+ | std:: | ||
+ | std::cout << *p << std::endl; | ||
+ | } // automatische Freigabe, auch bei Ausnahmen zwischendrin | ||
+ | </ | ||
+ | |||
+ | ===== Eigentum verpflichtet ===== | ||
+ | |||
+ | Mit der Zuweisung des neu erschaffenen Objektes an einen unique_ptr geht dieses Objekt in den Besitz des unique_ptr über, damit ist dieser auch für die Speicherfreigabe verantwortlich. Das heißt auch, niemals die Adresse einer lokalen Variable einem unique_ptr zu übertragen! | ||
+ | |||
+ | Bei der Nutzung dynamischen Speichers muss immer klar sein, wer für die Freigabe des Speichers verantwortlich ist. Bei normalen Zeigern ist das der Programmierer, | ||
+ | |||
+ | ==== Bengel: Klassen mit Zeigern ==== | ||
+ | |||
+ | Zuerst ein Beispiel, an dem deutlich werden soll, was bei bestem Wollen schiefgehen kann. Die dynamisch angelegten Ganzzahlen vom Anfang werden gekapselt: | ||
+ | |||
+ | <code cpp> | ||
+ | class Bengel | ||
+ | { | ||
+ | public: | ||
+ | Bengel(int n) { p = new int(n); } // Konstruktor: | ||
+ | ~Bengel() | ||
+ | int wert() const { return *p; } // abfragen | ||
+ | private: | ||
+ | int* p; | ||
+ | }; | ||
+ | |||
+ | int main() | ||
+ | { | ||
+ | Bengel a(9); | ||
+ | Bengel b(10); | ||
+ | std::cout << a.wert() << ' | ||
+ | // ... | ||
+ | </ | ||
+ | |||
+ | Es ist alles in bester Ordnung, der erhaltene Speicher wird wieder zurückgegeben. Bis einer auf die Idee kommt, einen Bengel an eine Funktion zu übergeben, die mit einem Bengel umgehen kann oder eine Zu(recht)weisung versucht: | ||
+ | <code cpp> | ||
+ | |||
+ | baendige(a); | ||
+ | std::cout << a.wert() << ' | ||
+ | a = b; // Crash? | ||
+ | Bengel c = b; // noch eine Bengel-Kopie | ||
+ | // ... | ||
+ | return 0; | ||
+ | } // Crash! | ||
+ | </ | ||
+ | |||
+ | Was ist passiert? Beim Kopieren (Wertübergabe erfolgt durch Kopie) wie auch bei der Zuweisung wird der Inhalt des Originals bitweise in das andere Objekt geschrieben. Auch der Zeiger! Wir haben dasselbe Problem wie im Anfangsbeispiel, | ||
+ | |||
+ | Was sollten wir sinnvoll bei Kopien und Zuweisungen tun? Bei eingehender Überlegung gibt es keine allgemeingültige Antwort, keinen Standard. Statt dessen kommen mehrere Möglichkeiten in Betracht: | ||
+ | |||
+ | * Den alten Speicher wegwerfen und neuen an sich reißen? | ||
+ | * Eine tiefe Kopie anlegen? Solange der durch Zeiger referenzierte Typ bekannt ist, kein Problem. Bei polymorphen Zeigern wäre eine clone()-Methode nötig, wie bei Java-Objekten definierbar. (Alle Java-Klassen stammen von Object ab.) | ||
+ | * Zeiger gemeinsam nutzen? Nur, wer macht dann das Licht aus? Wie bestimmt man, wer der Letzte ist? Referenzzähler. | ||
+ | * Verzögertes Kopieren? Eine Kombination der vorigen beiden Methoden. Die Klasse std::string der Standard-Bibliothek nutzte bisher bei mehreren Kopien den Inhalt gemeinsam, solange sich am Inhalt nichts ändert. Erst beim Schreiben wird eine Kopie gezogen (copy on write). In neueren Implementierungen werden std:: | ||
+ | |||
+ | Die Varianten 1 und 4 sind in älteren Versionen von < | ||
+ | |||
+ | ==== Engel: Tiefes Kopieren ==== | ||
+ | |||
+ | Will man richtig mit Zeigern in einer Klasse umgehen, sind außer Konstruktor und Destruktor auch Kopierkonstruktor und Zuweisungsoperator besonders zu definieren. | ||
+ | |||
+ | <code cpp> | ||
+ | class Engel | ||
+ | { | ||
+ | public: | ||
+ | Engel(int n) { p = new int(n); } // Konstruktor: | ||
+ | ~Engel() | ||
+ | |||
+ | Engel(Engel const& original) | ||
+ | { | ||
+ | p = new int(original.wert()); | ||
+ | } | ||
+ | |||
+ | Engel& operator=(Engel const& rhs) // Zuweisung von rechter Seite (rhs) | ||
+ | { | ||
+ | if (this != & | ||
+ | { | ||
+ | int *temp = new int(rhs.wert()); | ||
+ | std:: | ||
+ | delete temp; // altes Objekt freigeben | ||
+ | } | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | int wert() const { return *p; } // abfragen | ||
+ | private: | ||
+ | int* p; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Der Kopierkonstruktor muss bei tiefer Kopie neuen Speicher besorgen und initialisieren. | ||
+ | |||
+ | Bei der Zuweisung sind ein paar Dinge mehr zu tun. Zumeist ist eine Zuweisung an sich selbst gefährlich, | ||
+ | |||
+ | Da C fortlaufende Zuweisungen a = b = c erlaubt, sollten neue Klassen dies auch erlauben (Prinzip der kleinsten Überraschung). Deswegen muss b sich selbst (*this) zuletzt an a weiterreichen. | ||
+ | |||
+ | Die Zuweisung lässt sich elegant und schmerzfrei als Kopier-Zuweisung (copy & swap) umsetzen, wobei der Kopierkonstruktor nachgenutzt wird: | ||
+ | |||
+ | <code cpp> | ||
+ | Engel& operator=(Engel const& rhs) // Zuweisung von rechter Seite (rhs) | ||
+ | { | ||
+ | Engel temp(rhs); | ||
+ | std:: | ||
+ | return *this; | ||
+ | } // Freigabe alter Daten im temp-Destruktor | ||
+ | </ | ||
+ | |||
+ | Anstelle einer const-Referenz kann der Zuweisungsoperator auch einen Wertparameter als Kopie übernehmen. Das spart eine weitere Zeile, sieht aber ungewöhnlich aus. An manchen Stellen kann der Compiler dadurch weitere temporäre Kopien einsparen, die er sonst erzeugen müsste, um eine const-Referenz zu bilden. | ||
+ | |||
+ | Diese Regeln stellen das allgemeine Schema dar, das für die meisten Klassen sinnvoll ist. Speziell bei diesem Engel wäre etwas weniger Aufwand ausreichend gewesen: | ||
+ | |||
+ | <code cpp> | ||
+ | Engel& operator=(Engel const& rhs) | ||
+ | { | ||
+ | *p = rhs.wert(); | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Das Löschen des Speichers und Wiederbesorgen kann sich sparen, wenn nur ein Wert zu übernehmen ist. Wichtig ist nur, sich zu überlegen, welches Verhalten Objekte mit Zeigern haben sollen. Lege ich das nicht fest, verhält sich das System trotzdem irgendwie (fehlerhaft), | ||
+ | |||
+ | ==== (Halb-)Dynamische Felder ==== | ||
+ | |||
+ | C++ bietet std:: | ||
+ | |||
+ | <code java> | ||
+ | Vector sheep = new Vector(); | ||
+ | sheep.addElement(new Sheep()); | ||
+ | sheep.addElement(new Sheep()); | ||
+ | sheep.addElement(new Wolf()); // kein Übersetzungsfehler in Java! | ||
+ | |||
+ | Sheep one_sheep = (Sheep) sheep.elementAt(2); | ||
+ | // wirft ClassCastException - zur Laufzeit | ||
+ | <code cpp> | ||
+ | |||
+ | In Javascript gibt es einen Array-Container, | ||
+ | |||
+ | <code cpp> | ||
+ | #include < | ||
+ | |||
+ | template <class T> | ||
+ | class Array | ||
+ | { | ||
+ | public: | ||
+ | Array(int size, T const& initvalue) | ||
+ | : size_( size ) | ||
+ | , data_( new T[size] ) | ||
+ | { | ||
+ | std:: | ||
+ | } | ||
+ | |||
+ | Array(const Array< | ||
+ | : size_( rhs.size_ ) | ||
+ | , data_( new T[size_] ) | ||
+ | { | ||
+ | std:: | ||
+ | } | ||
+ | |||
+ | Array& operator=(Array< | ||
+ | { | ||
+ | swap(rhs); | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | void swap(Array< | ||
+ | { | ||
+ | std:: | ||
+ | std:: | ||
+ | } | ||
+ | |||
+ | ~Array() { delete [] data_; } // Destruktor | ||
+ | |||
+ | // Größe, Elementzugriff | ||
+ | int size() const { return size_; } | ||
+ | T const& operator[](int index) const { return data_[safe(index)]; | ||
+ | T& operator[](int index) | ||
+ | private: | ||
+ | int size_; | ||
+ | T* data_; | ||
+ | int safe(int index) const; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Die Feldgrenzen lassen sich auch noch überwachen: | ||
+ | |||
+ | <code cpp> | ||
+ | class ArrayRangeError | ||
+ | { | ||
+ | public: | ||
+ | virtual char const* what() const { return "Array range error"; | ||
+ | }; | ||
+ | |||
+ | template <class T> | ||
+ | int Array< | ||
+ | { | ||
+ | if (index<0 || index> | ||
+ | return index; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Mit nur 50 Zeilen Quelltext lassen sich so nützliche Container bauen. Damit besteht keine Notwendigkeit mehr, gefährliche C-Felder mit ungeprüften Feldgrenzen einzubauen. | ||
+ | |||
+ | <code cpp> | ||
+ | Array< | ||
+ | double sum = 0; | ||
+ | for (int i = 0; i < arr.size(); i++) | ||
+ | { | ||
+ | sum += arr[i]; | ||
+ | } | ||
+ | std::cout << sum << std::endl; // Ergebnis: 31.4 | ||
+ | </ | ||
+ | |||
+ | Mit der Array-Klasse (auch mit std:: | ||
+ | |||
+ | <code cpp> | ||
+ | std::cout << " | ||
+ | std::cin >> anzahl; | ||
+ | int C_array[anzahl]; | ||
+ | // ... Datenerfassung | ||
+ | </ | ||
+ | |||
+ | Die Lösung erfordert dynamischen Speicher. Zu Fuß sieht die Lösung so aus: | ||
+ | |||
+ | <code cpp> | ||
+ | // ... | ||
+ | int *dyn_array = new int[anzahl]; | ||
+ | // ... Datenerfassung | ||
+ | delete [] dyn_array; | ||
+ | </ | ||
+ | |||
+ | Mit Containern besteht die Lösung in einer einzigen Zeile: | ||
+ | |||
+ | <code cpp> | ||
+ | // ... | ||
+ | Array< | ||
+ | // ... oder | ||
+ | std:: | ||
+ | // ... Datenerfassung | ||
+ | </ | ||
+ | |||
+ | Die Container können auch zur Datenhaltung in Objekten genutzt werden. Da die Container sich selbsttätig um Speicheranforderung, | ||
+ | |||
+ | <code cpp> | ||
+ | class Briefkasten | ||
+ | { | ||
+ | // ... | ||
+ | private: | ||
+ | std:: | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Merkregel: Wer direkt dynamischen Speicher anfordert und besitzt, muss ihn auch freigeben. Wer einen Destruktor definiert, um dynamischen Speicher freizugeben, | ||
+ | |||
+ | ==== Kopierschutz ==== | ||
+ | |||
+ | Sie möchten ihre Objekte auch kopierschützen? | ||
+ | |||
+ | <code cpp> | ||
+ | class Copyrighted | ||
+ | { // ... | ||
+ | private: | ||
+ | Copyrighted(Copyrighted const& | ||
+ | void operator=(Copyrighted const& | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | ==== Klonen ==== | ||
+ | |||
+ | Das Klonen von Menschen kommt gerade in Mode. Sollten wir unsere Gene kopierschützen lassen? Es würde mich nicht überraschen, | ||
+ | |||
+ | ...GGGAAATTT(C)GOD_DAY_SIX_VER_ADAM_GPL___GGGAAATTTGGGAAATTT... | ||
+ | |||
+ | Es gibt gewisse Sekten, die gerne Klone herstellen möchten, obwohl es angenehmere Arten der Vervielfältigung gibt. Warum einfach, wenn es auch umständlich geht. | ||
+ | |||
+ | Klonen in C++ (oder Java) setzt klonfähige Objekte voraus, Erben einer Klasse, die selbst (!) Klone erzeugt. Dann kann ein Besitzer der Klone diese zur Vervielfachung veranlassen: | ||
+ | |||
+ | <code cpp> | ||
+ | class Cloneable | ||
+ | { | ||
+ | public: | ||
+ | virtual ~Cloneable(); | ||
+ | virtual Cloneable* clone() const = 0; // wird überschrieben | ||
+ | }; | ||
+ | |||
+ | class Besitzer | ||
+ | { | ||
+ | Cloneable* p; // Objekt kann auch abgeleitet sein | ||
+ | public: | ||
+ | Besitzer(Besitzer const& original) | ||
+ | { | ||
+ | p = original.p-> | ||
+ | } | ||
+ | |||
+ | Besitzer& | ||
+ | { | ||
+ | swap(rhs); | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | void swap(Besitzer& | ||
+ | { | ||
+ | std:: | ||
+ | } | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | ==== Automatisch klonen ==== | ||
+ | |||
+ | So unangenehme Aufgaben wie das Klonen kann man delegieren: | ||
+ | |||
+ | <code cpp> | ||
+ | class CloneablePtr | ||
+ | { | ||
+ | public: | ||
+ | CloneablePtr(Cloneable* neuesObjekt) | ||
+ | : p(neuesObjekt) | ||
+ | { | ||
+ | } | ||
+ | CloneablePtr(CloneablePtr const& original) | ||
+ | { | ||
+ | p = original.p ? original.p-> | ||
+ | } | ||
+ | CloneablePtr& | ||
+ | { | ||
+ | if (this != & | ||
+ | { | ||
+ | delete p; | ||
+ | p = original.p ? original.p-> | ||
+ | } | ||
+ | return *this; | ||
+ | } | ||
+ | // Zeigerverhalten: | ||
+ | Cloneable& | ||
+ | Cloneable* operator-> | ||
+ | |||
+ | Cloneable const& operator*() const { return *safe_ptr (); } | ||
+ | Cloneable const* operator-> | ||
+ | |||
+ | operator bool() const { return p!=0; } | ||
+ | private: | ||
+ | Cloneable* p; | ||
+ | Cloneable* safe_ptr(); | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Der Zugriff auf den Zeiger sollte überwacht werden: | ||
+ | |||
+ | <code cpp> | ||
+ | class NullPointerException | ||
+ | { | ||
+ | public: | ||
+ | virtual char const* what() const { return " | ||
+ | }; | ||
+ | |||
+ | Cloneable* CloneablePtr:: | ||
+ | { | ||
+ | if (!p) throw NullPointerException(); | ||
+ | return p; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Besitzer solcher Zeiger brauchen sich um nichts zu kümmern außer: | ||
+ | |||
+ | <code cpp> | ||
+ | class Besitzer | ||
+ | { | ||
+ | // ... | ||
+ | private: | ||
+ | CloneablePtr p; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | ==== Gute Referenzen ==== | ||
+ | |||
+ | Besonders bequem ist der Umgang mit Zeigertypen, | ||
+ | |||
+ | <code cpp> | ||
+ | template <class T> | ||
+ | class RefPtr | ||
+ | { | ||
+ | public: | ||
+ | RefPtr(); | ||
+ | RefPtr(T* neuesObjekt); | ||
+ | RefPtr& operator=(RefPtr const& rhs); | ||
+ | template <class T2> | ||
+ | RefPtr(RefPtr const< | ||
+ | RefPtr(RefPtr const& rhs); | ||
+ | ~RefPtr(); | ||
+ | T& operator*(); | ||
+ | T const& operator*() const; | ||
+ | T* operator-> | ||
+ | T const* operator-> | ||
+ | bool operator==(RefPtr const& rhs) const; | ||
+ | bool operator!=(RefPtr const& rhs) const; | ||
+ | operator bool() const; | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Sie werden zwei Hürden zu überwinden haben: | ||
+ | |||
+ | * Wie ist der Standardkonstruktor zu implementieren, | ||
+ | * Wie können RefPtr auf abgeleitete Objekte auch von RefPtr auf die Basisklasse übernommen werden? Die Schwierigkeit: | ||
+ | |||
+ | Der Rest ist einfach, um nicht zu sagen, elementar. Wenn Sie es geschafft haben, können Sie auch in C++ folgendes schreiben: | ||
+ | |||
+ | <code cpp> | ||
+ | struct Punkt { int x, y; }; | ||
+ | |||
+ | void demo() | ||
+ | { | ||
+ | RefPtr< | ||
+ | RefPtr< | ||
+ | |||
+ | p->x = 1; | ||
+ | p->y = 2; | ||
+ | |||
+ | array[0] = array[1] = p; | ||
+ | if (!array[2]) | ||
+ | { | ||
+ | // std::cout << array[2]-> | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Noboby is perfect ==== | ||
+ | |||
+ | Schaffen Sie es nicht auf Anhieb, trösten Sie sich; Sie sind in guter Gesellschaft. Ein Beispiel. | ||
+ | |||
+ | Der inzwischen geächtete std:: | ||
+ | |||
+ | <code cpp> | ||
+ | /* | ||
+ | * Copyright (c) 1991, 1996 by Borland International | ||
+ | * Copyright (c) 1994 Hewlett-Packard Company | ||
+ | * (c) Copyright 1994, 1995 Rogue Wave Software, Inc. | ||
+ | * ALL RIGHTS RESERVED | ||
+ | | ||
+ | |||
+ | // ... leicht gekürzt | ||
+ | |||
+ | template< | ||
+ | { | ||
+ | public: | ||
+ | explicit auto_ptr (X* p = 0) : the_p(p) | ||
+ | auto_ptr (auto_ptr< | ||
+ | void operator= (auto_ptr< | ||
+ | ~auto_ptr () { delete the_p; } | ||
+ | |||
+ | X& operator* | ||
+ | X* operator-> | ||
+ | X* get () const { return the_p; | ||
+ | X* release | ||
+ | X* reset (X* p = 0) { X* tmp = the_p; the_p = p; return tmp; } | ||
+ | |||
+ | private: | ||
+ | X* the_p; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Im Zuweisungsoperator fehlt das Zauberwörtchen delete. Ausgerechnet die Klasse, die Speicherlecks verhindern soll, erzeugt eines. Das alte Objekt wird vor Schreck fallengelassen und nicht aufgeräumt. Will man das Objekt löschen, muss man sich aber auch um die Selbstzuweisung sorgen; ein Fehler kommt selten allein: | ||
+ | |||
+ | <code cpp> | ||
+ | void operator= (auto_ptr< | ||
+ | { | ||
+ | if (this != &rhs) delete reset(rhs.release()); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Über dem Quelltext befinden sich drei Copyrights, die ich hiermit gebrochen habe. Die Frage ist nur: Wer hat wann von wem falsch abgeschrieben, | ||
+ | |||
+ | Der Zuweisungsoperator des std:: | ||
+ | |||
+ | |||
+ | ==== Holmes: Ich kombiniere ... ==== | ||
+ | |||
+ | Smarte Pointer (Zeigerklassen) kümmern sich nur um Einzelobjekte, | ||
+ | |||
+ | <code cpp> | ||
+ | typedef RefPtr< | ||
+ | |||
+ | Shared_int_array shared = new Array< | ||
+ | (*shared)[1] = 2; | ||
+ | |||
+ | typedef Array< | ||
+ | |||
+ | Array_of_clones stammzellen(10, | ||
+ | |||
+ | for (int i = 0; i < stammzellen.size(); | ||
+ | { | ||
+ | stammzellen[3]-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Es ist möglich, ein Feld gemeinsam zu nutzen, über einen referenzzählenden Zeiger. Es ist auch möglich, in einem Feld Zeiger einzusetzen, | ||
+ | |||
+ | <code cpp> | ||
+ | Array< | ||
+ | fahrwerk[0] = new Rad(); // Ersatzrad | ||
+ | fahrwerk[1]-> | ||
+ | </ | ||
+ | |||
+ | ===== Schlussbemerkung ===== | ||
+ | |||
+ | Zeiger sind gefährlich. Vielleicht sollte man gar keine benutzen? Besser ist, sie in Klassen zu verstecken, die sich um die niederen Arbeiten der Speicherbereitstellung und -bereinigung kümmern. Hat man diese Klassen, ist der Umgang mit dynamischem Speicher einfacher. Allerdings reichen Programmier-Grundkenntnisse allein nicht, diese Klassen zu schaffen. Zum Glück gibt es für die grundlegenden Aufgaben schon Standard-Container und Standard-Algorithmen. Wer Klassen mit Zeigern baut, muss sich um die Speichernutzung kümmern. Mindestens einmal. Ist man über diesen Berg, wird es einfacher. Nur mit umfassendem Verständnis läßt sich C++ effizient programmieren. Nur, in welcher Sprache ist das nicht so? | ||
+ | |||
+ | Weiterführende Literatur: | ||
+ | |||
+ | * [Josuttis] Nikolai Josuttis: Die C++-Standardbibliothek. Addison-Wesley, | ||
+ | * [Effective C++] Scott D. Meyers: Effective C++. Addison-Wesley, | ||
+ | * [Horstmann] Cay S. Horstmann: Practical Object-Oriented Development in C++ and Java. Wiley, New York (1997). ISBN 0-471-14767-2 | ||
+ | |||
lernen/eigentum.txt · Zuletzt geändert: 2020-07-27 09:22 von 127.0.0.1