A. Solymosi: Objektorientiertes Plug and Play
Letzter Eintrag: 17. Februar 1998
Fehlerhafte Seitenzahlen im Sachwortverzeichnis
Das aktualisierte Sachwortverzeichnis ist auf der Begleitdiskette
als komprimierte Datei in MS-Word'97-Format zu finden
Seite 91: Programmfehler
Seiten 173-175: Programmfehler
Seite 203: Programmfehler
Seite 266: Programmfehler
Seite 273: Programmfehler
Seite 282: Programmfehler
Seite XIV: Ergänzung
Seite 54 und 143: Ergänzung
Seite 126: Programmverbesserung
Seite 126: Programmverbesserung
Seite 202: Programmverbesserung
Seite 277: Programmverbesserung
Seite 280: Verbesserte Lösung
Seite 290: Ergänzungen
Seite 291: Ergänzung
Seite 40: Schreibfehler
Seite 49: Formatierungsverbesserung
Seite 126: Formatierungsverbesserung
Seite 186: Tippfehler
Seite 271: Schreibfehler
Seite 285: Schreibfehler
Ergänzung auf der Seite XIV: Neuer Satz zwischen den 1. und 2. Sätzen (vor "Meine Diplomanden ...")
Der schlagkräftige Titel "Objektorientiertes Plug and Play" stammt von meiner TFH-Kollegin Prof. Dr. Fanny-Michaela Reisin.
Unten auf der Seite im Programmbeispiel des Abschnitts 2.8.4. (Passende Datentypen) sollen die Kommentare mit // (statt mit --) gekennzeichnet werden. Somit ist das Programmbeispiel richtig:
datentyp_1 objekt_1; datentyp_2 objekt_2; operation_fuer_datentyp_1(objekt_1); // paßt operation_fuer_datentyp_1(objekt_2); // Typfehler operation_fuer_datentyp_2(objekt_1); // Typfehler
und nicht (wie im Buch):
datentyp_1 objekt_1; datentyp_2 objekt_2; operation_fuer_datentyp_1(objekt_1); -- paßt operation_fuer_datentyp_1(objekt_2); -- Typfehler operation_fuer_datentyp_2(objekt_1); -- Typfehler
Formatierungsverbesserung auf den Seiten 40, 49, 65, 67 und 157:
In Referenzparameter-Vereinbarungen soll (konsequenterweise) zwischen dem Typnamen und dem Referenzzeichen & kein Zwischenraum geschrieben werden:
Seite 40:
void fuellen_und_entleeren(TEimer& e) { // (2.23) // Definition der lokalen Prozedur; ihr Parameter ist e vom Typ TEimer fuellen(e); entleeren(e); }
Seite 49:
typedef ... TEimer // der exportierte ADT; seine Definition ist nicht Teil der Schnittstelle (2.28) void fuellen(TEimer&) throw(EEimer_voll); // Eimer wird gefüllt, wenn leer void entleeren(TEimer&) throw(EEimer_leer); // Eimer wird entleert, wenn voll // Reihenfolgebedingungen wie bei M1EIMER void anzeigen(TEimer&);
Seite 65:
void fuellen_nach_entleeren(TEimer& quelle, TEimer& ziel) { // (3.10) // Der Eimer ziel wird mit dem Inhalt des Eimers quelle gefüllt, nachdem er entleert wurde. entleeren(ziel); fuellen(ziel, inhalt(quelle)); }
Seite 65:
void fuellen_nach_entleeren(const TEimer& quelle, TEimer& ziel) { ... void fuellen_nach_entleeren(TEimer quelle, TEimer& ziel) { ...
Seite 67:
void fuellen_nach_entleeren(const TEimer& quelle, TEimer& ziel); ... // weitere Vereinbarungen void fuellen_nach_entleeren(const TEimer& quelle, TEimer& ziel) { ... // die Definition der lokalen Prozedur
void fuellen_nach_entleeren(const TEimer&, TEimer&);
Seite 157:
void suchen(const TElement&) throw(ENicht_vorhanden);
Ergänzung auf der Seite 54: Neue Übung:
12a. Übung: Entwickeln Sie eine Klasse CGarantierter_Eimer mit Hilfe der Klasse CEimer (exportiert vom Modul CEIMER). Diese Klasse soll über die üblichen Operationen Fuellen und Entleeren hinaus auch zwei neue Methoden garantierter_fuellen und garantierter_entleeren exportieren; sie lösen keine Ausnahmen aus, sondern füllen bzw. entleeren den Eimer unabhängig von seinem vorherigen Zustand.
Schreiben Sie auch ein Testprogramm für Ihre Klasse, in dem Sie alle ihre Methoden aufrufen.
Programmfehler auf der Seite 91:
Im Programm (4.13) sind die Namen der Zeigerobjekte nicht links und rechts, sondern linker_zeiger und rechter_zeiger :
anzeigen(*linker_zeiger); // Eimerobjekt wird erreicht und angezeigt anzeigen(*rechter_zeiger); menue(links, rechts, fuellen, entleeren); delete linker_zeiger; // die zwei dynamischen Objekte werden gelöscht delete rechter_zeiger;
und nicht (wie im Buch);
anzeigen(*links); // Eimerobjekt wird erreicht und angezeigt anzeigen(*rechts); menue(links, rechts, fuellen, entleeren); delete rechts; // die zwei dynamische Objekte werden gelöscht delete links;
Auch der Schreibfehler im Kommentar soll korrigiert werden:
// die zwei dynamischen Objekte werden gelöscht
Somit lautet die gesamte Prozedur:
void aktion_nach_vorwahl_zeiger() { linker_zeiger = new TEimer; // zwei Objekte dynamisch angelegt rechter_zeiger = new TEimer; aktuell = linker_zeiger; // Startwert, um null_pointer_reference vorzubeugen anzeigen(*linker_zeiger); // Eimerobjekt wird erreicht und angezeigt anzeigen(*rechter_zeiger); menue(links, rechts, fuellen, entleeren); delete rechts; // die zwei dynamischen Objekte werden gelöscht delete links; }
Formatierungsverbesserung auf der Seite 126:
In der 1. Zeile auf der Seite gehört zwischen "gegenüber" und "(2.29)" ein Zwischenraum:
class CEimer { public: // Schnittstelle ergänzt gegenüber (2.29)
Programmverbesserung auf der Seite 126:
Die 5. Zeile auf der Seite ist besser:
TGetraenk inhalt() throw(EEimer_leer) const;
anstelle von (wie im Buch):
TGetraenk inhalt() throw(EEimer_leer);
Wie alle Informatoren, kann (und soll) auch ninhalt() als const-Methode vereinbart werden (ähnlich wie gefuellt()).
Programmkorrektur auf der Seite 126:
Die 9. Zeile des Programms (6.11) ist - ähnlich wie im Programm (6.9) - besser:
eimer_gefuellt ++; // Zugriff auf die Klassenkomponente
anstelle von (wie im Buch):
eimer_gefuellt = eimer_gefuellt ++; // Zugriff auf die Klassenkomponente
Die Zuweisung ist überflüssig
Ergänzung auf der Seite 143: Neue Übung:
32a. Übung: In der 12a. Übung haben Sie die Klasse CGarantierter_Eimer mit Hilfe der Klasse CEimer entwickelt. Programmieren Sie jetzt diese Klasse als Erbe der Klasse CEimer.Verwenden Sie Ihr Testprogramm aus der 12a. Übung unverändert..
Programmfehler auf den Seiten 173, 174 und 175:
In den Programmen (8.5) und (8.6) M1STAPEL.CPP und CSTAPEL.CPP soll anstelle des Aufzählungstyps TIndex die Klasse CIndex benutzt werden, damit die Operatoren ++ und -- eine Ausnahme auslösen. Die 9. Zeile des Programms (8.5) lautet somit richtig:
CIndex spitze = null; // zeigt auf den letzten belegten Platz im stapel; zu Anfang leer
anstelle von (wie im Buch):
TIndex spitze = null; // zeigt auf den letzten belegten Platz im stapel; zu Anfang leer
Ähnlich soll in der 4. Zeile des Methodenrumpfs lesen() auf der Seite 174
CIndex i = spitze;
geschrieben werden, und nicht wie im Buch
TIndex i = spitze;
In der 6. Zeile der Seite 175 soll analog
CIndex spitze; // zeigt auf den letzten belegten Platz im stapel; zu Anfang leer
stehen und nicht (wie im Buch)
TIndex spitze; // zeigt auf den letzten belegten Platz im stapel; zu Anfang leer
Seite 186: Tippfeher im Programm CEIMER.CPP in der 14. Zeile von unten (Rumpf des Operators ==): Zwischen rechts. und eimer_inhalt gehört - konventionsgemäß - kein Zwischenraum. Somit lautet die Zeile richtig
eimer_inhalt == rechts.eimer_inhalt); // gleicher Inhalt
und nicht wie im Buch
eimer_inhalt == rechts. eimer_inhalt); // gleicher Inhalt
Seite 202: Programmverbesserung
Im Programm (9.3) ist der 1. Parameter des Konstruktors CKnoten kann als const vereinbart werden. Somit lautet die 11. Programmzeile richtig:
CKnoten(const CKnoten* v, const TElement& e): wert(e), verbindung(v) {};
und nicht wie im Buch
CKnoten(CKnoten* v, const TElement& e): wert(e), verbindung(v) {};
Die 10. Programmzeile
public:
kann gestrichen werden (da die Datenelemente zuvor auch public sind). Somit lautet die geschachtelte Klassenvereinbarung:
class CKnoten { public: // geschachtelte Klasse TElement wert; // Ausprägungstyp CKnoten* verbindung; // Rückwärtsverbindung CKnoten(const CKnoten* v, const TElement& e): wert(e), verbindung(v) {}; ~CKnoten() { if (verbindung != NULL) delete verbindung; }; };
Programmverbesserungen auf den Seiten 202-203:
In der Implementierung des Destruktors der Klasse GLStapel kann die if-Abfrage gestrichen werden. Somit lautet die 5. Programmzeile der Implementierung richtig:
entleeren();
und nicht wie im Buch
if (anker != NULL) entleeren(); // wieder Vorgriff auf das Kapitel 12.1.
Ähnlich kann in der Implementierung der Methode entleeren die if-Abfrage gestrichen werden. Somit lautet die 1. Programmzeile der Implementierung richtig:
delete anker; // CKnoten-Destruktor wird aufgerufen
und nicht wie im Buch
if (anker != NULL) delete anker; // CKnoten-Destruktor wird aufgerufen
(delete auf einen NULL-Zeiger hat keine Wirkung)
Die 2. Zeile lautet
return anker -> wert; // das Spitzenelement
und nicht wie im Buch
return anker -> wert(); // das Spitzenelement
(wert ist eine Datenkomponente der Klasse CKnoten, keine Methode)
Die 2. Zeile lautet
anker = anker -> verbindung; // das Spitzenelement wird ausgekettet
und nicht wie im Buch
anker = anker -> aushaengen(); // das Spitzenelement wird ausgekettet
(die Klasse CKnoten hat keine Methode aushaengen, es gibt aber einen direkten Zugriff auf die Datenkomponente verbindung)
Die beiden Fehler stammen aus einer früheren Version von CKnoten, in der die Datenkomponenten noch gekapselt (protected) waren und Methoden für ihre Manipulation exportiert wurden. Dies ist nicht nötig, da die Klasse CKnoten selber in protected-Teil der Klase GLStapel vereinbart wurde, daher sind auch ihre öffentlichen Komponenten geschützt.
Somit lauten die korrigierten Methoden der Klasse GLStapel richtig:
template <class TElement> GLStapel<TElement>::~GLStapel() { entleeren(); }
template <class TElement> TElement GLStapel<TElement>::lesen() const throw(EStapel_leer) { try { return anker -> wert; // das Spitzenelement } catch(...) { // wenn anker == NULL // wenn der Compiler keine Ausnahme auslöst, dann if (anker == NULL) throw EStapel_leer(); } }
template <class TElement> void GLStapel<TElement>::entfernen() throw(EStapel_leer) { try { const CKnoten* freizugebender_knoten = anker; anker = anker -> verbindung; // das Spitzenelement wird ausgekettet freizugebender_knoten -> verbindung = NULL; delete freizugebender_knoten; // CKnoten-Destruktor wird aufgerufen } catch(...) { // wenn anker == NULL // wenn der Compiler keine Ausnahme auslöst, dann if (anker == NULL) throw EStapel_leer(); } }
template <class TElement> void GLStapel<TElement>::entleeren() { delete anker; // CKnoten-Destruktor wird aufgerufen anker = NULL; }
Programmfehler auf der Seite 266 in den Prozeduren inorder und postorder: Die rekursiven Aufrufe beziehen sich selbstverstänldlich jeweils auf dieselben Prozeduren inorder und postorder und nicht auf die Prozedur preorder wie im Buch. Somit lauten die drei Prozeduren richtig:
void preorder(TBaum& baum) { if (baum != NULL) { operation (baum); preorder(baum -> links); preorder(baum -> rechts); } }void inorder(TBaum& baum) { if (baum != NULL) { inorder(baum -> links); operation(baum); inorder(baum -> rechts); } }void postorder(TBaum& baum) { if (baum != NULL) { postorder(baum -> links); postorder(baum -> rechts); operation(baum); } }
Schreibfehler auf der Seite 273. Der Absatz lautet:
84. Übung: Implementieren Sie die Klasse CRund_Quadrat aus der obigen Klassenhierarchie. Testen Sie sie mit einem Testtreiber. Testen Sie das Verhalten der Methode CRund_Quadrat::springe für den Fall, wo erscheine und verschwinde als statische nichtvirtuelle Methoden vereinbart wurden.
Programmfehler auf der Seite 271 im Programm (13.1) in der Methode speichern: Die 5. Zeile lautet richtig
datei << spitze << endl;
anstelle von (wie im Buch)
datei << speicher << endl;
Die ganze Methode lautet somit:
void CStapel::speichern (const char* dateiname) const { // Persistenzoperationen ostream datei; // fstream::ostream datei.open(dateiname); datei << max << endl; datei << spitze << endl; for (int i=1; i<=spitze; i++) // Elementweise ausgeben datei << speicher[i] << endl; // << für int datei.close(); }
Programmverbesserung auf der Seite 277 im Programm (13.6): Der Klassenname mit dem Bereichsoperator CSummen_Stapel_TypErb:: kann vor der Konstruktorvereinbarung CSummen_Stapel_TypErb(int) weggelassen werden. Somit lautet die3. Zeile der Klassenvereinbarung
CSummen_Stapel_TypErb(int);
und nicht wie im Buch
CSummen_Stapel_TypErb::CSummen_Stapel_TypErb(int);
Die gesamte Klassenvereinbarung lautet:
class CSummen_Stapel_TypErb : public CStapel { public: // (13.6) // Mutatoren (nicht-const-Methoden) werden überladen: CSummen_Stapel_TypErb(int); void eintragen(const TElement& element) throw(EStapel_voll); void entfernen() throw(EStapel_leer); void entleeren() throw(EStapel_leer); void operator = (const CSummen_Stapel_TypErb& rechts); // neue Methode: TElement summe() const; // alles andere (die const-Methoden) wird durch die Modulvererbung // automatisch exportiert und ist für CSummen_Stapel_TypErb gültig protected: // geerbte Komponenten, und noch: TElement s; // neue Komponente: Typvererbung };
Eine bessere Lösung auf der Seite 280 im Programm (13.18) für den Konstruktor und den Destruktor von CPerson_Element ist:
CPerson_Element(const TPerson& x) { wert = new TPerson; wert -> name = new char [strlen (x.name)]; wert -> vorname = new char [strlen (x.vorname)]; strcpy (wert.name, x.name); strcpy (wert.vorname, x.name); wert -> alter = x.alter; }~CPerson_Element(){ delete wert.name; // Zeichenketten werden gelöscht delete wert.vorname; delete wert; // Verbund wird gelöscht }
Im Gegensatz zur Lösung im Buch werden hier die Zeichenkettenkomponenten des TPerson-Verbunds automatisch gelöscht. Dadurch besteht keine Notwendigkeit, diese Zeichenkettenkomponenten im Benutzerprogramm - wie im Buch vorgeschlagen - mit new anzulegen (die dann evtl. schwer freizugeben sind). Die Rückrufprozedur person_eingeben wird einfacher:
void person_eingeben() { cout << "Bitte Person eingeben" << endl; char name[max_laenge], vorname[max_laenge]; cout << "Name (maximal " << max_laenge << " Zeichen): "; cin >> name; cout << "Vorname (maximal " << max_laenge << " Zeichen): "; cin >> vorname; TPerson person; person.name = &name; person.vorname = &vorname; cout << "Alter (Ganzzahl): "; cin >> person.alter; stapel.person_eintragen(person); }
Programmfehler auf der Seite 282 im Programm (13.18) in der Prozedur main: Ihr einziger Aufruf lautet richtig
menue(ganzzahl_eingeben, zeichenkette_eingeben, person_eingeben, ausgeben); // generiert für 4 Menüpunkte
anstelle von (wie im Buch)
menue(ganzzahl_eingeben, zeichenkette_eingeben, zeichenkette_eingeben, ausgeben); // generiert für 4 Menüpunkte
(Die dritte Rückrufprozedur wurde falsch angegeben.)
Schreibfehler auf der Seite 285: Der 2. Satz der 84. Übung lautet richtig:
Testen Sie sie mit einem Testtreiber.
Seite 290: Ergänzungen im letzten Absatz:
Die Basisklasse selbst enthält keine Daten, nur die Verkettung. Dies ist eine Klasse, von der (zumindest direkt*) keine Objekte angelegt werden, da eine verkettete Liste ohne Daten keinen Sinn ergibt. Solche Klassen nennen wir aufgeschoben oder abstrakt.
* Manchmal wird es so gesehen, daß ein Objekt der Sohnklasse ein Subobjekt der Vaterklasse (in diesem Fall der Basisklasse) enthält.
Seite 291: Ergänzung im 1. Satz des 4. Absatzes:
Solche Datentypen können aufgeschobene oder, wie es in C++ heißt, abstrakte oder rein virtuelle Methoden haben.
Leitseite des Buchs
Leitseite des Autors