© APSIS GmbH extern.gif (1249 Byte)

Errata

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


Inhaltlich relevante Fehler:

Seite 91: Programmfehler
Seiten 173-175: Programmfehler
Seite 203: Programmfehler
Seite 266: Programmfehler
Seite 273: Programmfehler
Seite 282: Programmfehler

Ergänzungen und Verbesserungen:

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

Formatierung- und Schreibfehler:

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.


Seite 40: Schreibfehler

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)


Seite 203: Programmfehler

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 extern.gif (1249 Byte) des Autors


© APSIS GmbH extern.gif (1249 Byte)