Klassen erweitern die in C++ vordefinierten Fähigkeiten und unterstützen damit die Darstellung und Lösung komplexer Probleme in der Praxis.
Mittlerweile haben Sie eine Reihe von Variablentypen kennengelernt, einschließlich
vorzeichenloser ganzer Zahlen und Zeichen. Der Typ einer Variablen sagt eine Menge
über die Variable aus. Wenn Sie zum Beispiel Height
und Width
als ganze Zahlen des
Typs unsigned short
deklarieren, wissen Sie, daß jede dieser Variablen eine Zahl zwischen
0 und 65.535 aufnehmen kann (vorausgesetzt, daß unsigned short
eine Länge
von 2 Byte hat). Und genau das ist mit unsigned short int
gemeint. Irgend etwas anderes
in diesen Variablen abzulegen, führt zu einer Fehlermeldung. Es ist nicht möglich,
Ihren Namen in einer Integer-Variablen vom Typ unsigned short
unterzubringen,
und Sie sollten es am besten gar nicht erst versuchen.
Somit ergibt sich allein aus der Deklaration von Height
und Width
als unsigned short
int
, daß man Height
und Width
addieren und diese Zahl einer anderen Zahl zuweisen
kann.
Der Typ dieser Variablen sagt etwas aus über
Allgemein betrachtet, ist ein Typ eine Kategorie. Zu den alltäglichen Typen gehören Auto, Haus, Person, Frucht und Figur. Ein C++-Programmierer kann jeden benötigten Typ erzeugen, und jeder dieser neuen Typen kann über die gesamte Funktionalität und Leistungsfähigkeit der vordefinierten, elementaren Typen verfügen.
Programme schreibt man in der Regel, um praxisgerechte Probleme zu lösen, wie etwa die Erfassung von Mitarbeiterdaten oder die Simulation der Abläufe eines Heizungssystems. Obwohl man komplexe Probleme auch mit Programmen lösen kann, die nur mit ganzen Zahlen und Zeichen arbeiten, bekommt man große, komplexe Probleme weitaus einfacher in den Griff, wenn man die Objekte, mit denen man es zu tun hat, direkt präsentieren kann. Mit anderen Worten läßt sich die Simulation der Abläufe eines Heizungssystems leichter umsetzen, wenn man Variablen für die Repräsentation von Räumen, Wärmesensoren, Thermostaten und Dampfkesseln erzeugen kann. Je besser diese Variablen der Realität entsprechen, desto einfacher ist es, das Programm zu schreiben.
Einen neuen Typ erzeugt man durch die Deklaration einer Klasse. Eine Klasse ist einfach eine Sammlung von Variablen - häufig mit unterschiedlichen Typen - kombiniert mit einer Gruppe zugehöriger Funktionen.
Ein Auto kann man sich als Sammlung von Rädern, Türen, Sitzen, Fenstern usw. vorstellen. Eine andere Möglichkeit ist es, das Auto als Sammlung seiner Funktionen aufzufassen: Es fährt, beschleunigt, bremst, stoppt, parkt und so weiter. Mit einer Klasse können Sie diese verschiedenen Elemente und Funktionen kapseln beziehungsweise in einer Sammlung bündeln, die dann auch Objekt genannt wird.
Die Kapselung aller Informationen über ein Auto in einer Klasse bietet für den Programmierer eine Reihe von Vorteilen. Alles ist an einer Stelle zusammengefaßt, wodurch man die Daten leicht ansprechen, kopieren und manipulieren kann. Des weiteren können Klienten Ihrer Klasse - das sind Teile des Programms, die Ihre Klasse nutzen - das Objekt nutzen, ohne sich um seinen Inhalt oder seine Funktionsweise kümmern zu müssen.
Eine Klasse kann aus jeder Kombination von Variablentypen, einschließlich anderer
Klassentypen, bestehen. Variablen, die Teil einer Klasse sind, bezeichnet man als Elementvariablen
oder Datenelemente. Eine Klasse Auto
könnte beispielsweise über Elementvariablen
zur Darstellung von Sitzen, Radiotypen, Reifen usw. besitzen.
Elementvariablen oder Datenelemente sind die Variablen Ihrer Klasse. Sie gehören zu Ihrer Klasse genau wie Räder und Motor Teile Ihres Autos sind.
Normalerweise manipulieren die Funktionen der Klasse die Elementvariablen. Man
bezeichnet diese Funktionen als Elementfunktionen oder Methoden der Klasse. Methoden
der Klasse Auto
können zum Beispiel Starten()
und Bremsen()
sein. Eine Klasse
Cat
(Katze) könnte über Datenelemente verfügen, die das Alter und Gewicht repräsentieren,
als Methoden sind Sleep()
, Meow()
und ChaseMice()
vorstellbar.
Elementfunktionen - oder Methoden - sind die Funktionen in einer Klasse. Elementfunktionen gehören ebenso zur Klasse wie Elementvariablen und bestimmen, was Ihre Klasse leisten kann.
Um eine Klasse zu deklarieren, verwendet man das Schlüsselwort class
gefolgt von einer
öffnenden geschweiften Klammer und einer Liste der Datenelemente und Methoden
dieser Klasse. Den Abschluß der Deklaration bildet eine schließende geschweifte
Klammer und ein Semikolon. Die Deklaration einer Klasse namens Cat
(Katze) sieht
wie folgt aus:
class Cat
{
unsigned int itsAge;
unsigned int itsWeight;
Meow();
};
Die Deklaration dieser Klasse reserviert noch keinen Speicher für Cat
. Es ist nur eine
Mitteilung an den Compiler, was die Klasse Cat
darstellt, welche Daten sie enthält (itsAge
und itsWeight
) und was sie tun kann (Meow()
). Außerdem wird dem Compiler
mitgeteilt, wie groß Cat
ist - das heißt, wieviel Platz der Compiler für jedes erzeugte
Cat
-Objekt reservieren muß. In diesem Beispiel ist Cat
lediglich 8 Byte groß (vorausgesetzt,
daß für iNT 4
Byte reserviert werden): itsAge
mit 4 Byte und itsWeight
mit weiteren
4. Die Methode Meow()
belegt keinen Platz, da für Elementfunktionen (Methoden)
kein Speicher reserviert wird.
Als Programmierer muß man alle Elementvariablen, Elementfunktionen und Klassen
benennen. Wie Sie in Kapitel 3, »Variablen und Konstanten«, gelernt haben, sollten
diese Namen leicht verständlich und aussagekräftig sein. Katze
, Rechteck
und Angestellter
sind geeignete Klassennamen. Miau()
, MausJagen()
und MaschineAnhalten()
sind passende Funktionsnamen, da man daraus die Aufgabe der Funktionen ablesen
kann. Viele Programmierer benennen die Elementvariablen mit dem Präfix its
(zu
deutsch: ihr, bezieht sich auf Klasse) wie in itsAge
, itsWeight
und itsSpeed
(ihrAlter,
ihrGewicht, ihreGeschwindigkeit). Damit lassen sich Elementvariablen leichter von anderen
Variablen unterscheiden.
C++ unterscheidet Groß-/Kleinschreibung, und alle Klassennamen sollten demselben
Muster folgen. Auf diese Weise braucht man nie zu prüfen, wie man den Klassennamen
schreiben muß - war es Rechteck
, rechteck
oder RECHTECK
? Einige Programmierer
setzen vor den Klassennamen einen bestimmten Buchstaben (beispielsweise cKatze
oder cPerson
), während andere den Namen durchgängig groß oder klein schreiben.
Ich beginne alle Klassennamen mit einem Großbuchstaben wie in Cat
oder Person
.
In gleicher Weise beginnen viele Programmierer ihre Funktionen mit Großbuchstaben
und die Variablen mit Kleinbuchstaben. Zusammengesetzte Wörter werden durch einen
Unterstrich (wie in Maus_Jagen()
) oder durch Großschreibung der einzelnen Wörter
(wie in MausJagen()
oder KreisZeichnen()
) auseinandergehalten.
Wichtig ist vor allem, daß Sie sich einen Stil herausgreifen und ihn in jedem Programm konsequent anwenden. Mit der Zeit vervollkommnet sich dieser Stil und umfaßt nicht nur Namenskonventionen, sondern auch Einzüge, Ausrichtung von Klammern und Kommentare.
Software-Häuser pflegen in der Regel ihre eigenen, internen Standards festzulegen. Damit wird sichergestellt, daß die Entwickler untereinander ihren Code schnell und ohne Probleme lesen können.
Ein Objekt Ihres neuen Typs definieren Sie fast genauso wie eine Integer-Variable:
unsigned int GrossWeight; // eine unsigned Integer-Variable definieren
Cat Frisky; // ein Cat-Objekt definieren
Dieser Code definiert eine Variable namens GrossWeight
vom Typ unsigned int
. Die
zweite Zeile zeigt die Definition von Frisky
als Objekt, dessen Klasse (oder Typ) Cat
ist.
Die Definition einer Katze kann man nicht streicheln, man streichelt einzelne Katzen.
Man unterscheidet also zwischen der Vorstellung von einer Katze und der realen Katze,
die gerade durchs Wohnzimmer schleicht. Auf die gleiche Weise unterscheidet
C++ zwischen der Klasse Cat
als Vorstellung von einer Katze und jedem individuellen
Cat
-Objekt. Demzufolge ist Frisky
ein Objekt vom Typ Cat
, genauso wie GrossWeight
eine Variable vom Typ unsigned int
ist.
Ein Objekt ist einfach eine selbständige Instanz einer Klasse.
Nachdem man ein Cat
-Objekt definiert hat (zum Beispiel Frisky
), kann man mit dem
Punktoperator (.
) auf die Elemente dieses Objekts zugreifen. Zum Beispiel weist man
den Wert 50 der Elementvariable Weight
des Objekts Frisky
wie folgt zu:
Frisky.itsWeight = 50;
In gleicher Weise ruft man die Funktion Meow()
auf:
Frisky.Meow();
Wenn Sie die Methode einer Klasse verwenden, rufen Sie diese Methode auf. In diesem
Beispiel rufen Sie Meow()
für Frisky
auf.
In C++ weist man die Werte nicht den Typen, sondern den Variablen zu. Beispielsweise schreibt man niemals
int = 5; // falsch
Der Compiler moniert dies als Fehler, da man den Wert 5 nicht an einen Integer-Typ zuweisen kann. Statt dessen muß man eine Integer-Variable erzeugen und dieser Variablen den Wert 5 zuweisen:
int x; // x als int definieren
x = 5; // den Wert von x auf 5 setzen
Das ist eine Kurzform für die Anweisung »weise 5 der Variablen x
zu, die vom Typ int
ist«. In diesem Sinne schreibt man auch nicht
Cat.itsAge = 5; // falsch
Der Compiler würde dies als Fehler anmerken, da Sie der Cat
-Klasse kein Alter zuweisen
können. Man muß erst ein Cat
-Objekt definieren und dann diesem Objekt den
Wert 5 zuweisen:
Cat Frisky; // analog zum Beispiel int x;
Frisky.age = 5; // analog zum Beispiel x = 5;
Machen Sie mal folgenden Versuch: Gehen Sie auf ein dreijähriges Kind zu und zeigen Sie ihm eine Katze. Sagen Sie dann »Das ist Frisky, Frisky kennt einen Trick. Frisky bell' mal.« Das Kind wird sicherlich kichern und sagen »Nein, du Dummkopf, Katzen können doch nicht bellen.«
Cat Frisky; // erzeugt eine Katze namens Frisky
Frisky.Bark() // teilt Frisky mit, zu bellen
eingeben, würde der Compiler ebenfalls sagen »Nein, du Dummkopf, Katzen können
doch nicht bellen.« (Der genaue Wortlaut hängt von Ihrem Compiler ab.) Der Compiler
weiß, daß Frisky nicht bellen kann, da die Cat
-Klasse nicht über eine Bark()
-Funktion
verfügt. Der Compiler würde sogar Frisky das Miauen untersagen, wenn die
Meow()
-Funktion nicht vorher definiert wäre.
Bei der Deklaration einer Klasse kommen noch weitere Schlüsselwörter zum Einsatz.
Zwei der wichtigsten sind public
(öffentlich) und private
(privat)
Alle Elemente einer Klasse - Daten und Methoden - sind per Vorgabe privat. Auf private Elemente kann man nur innerhalb der Methoden der Klasse selbst zugreifen. Öffentliche Elemente sind über jedes Objekt der Klasse zugänglich. Der Unterschied zwischen beiden ist wichtig und verwirrend zugleich. Um es klarer herauszustellen, sehen wir uns ein früheres Beispiel dieses Kapitels an:
class Cat
{
unsigned int itsAge;
unsigned int itsWeight;
void Meow();
};
In dieser Deklaration sind itsAge
, itsWeight
und Meow()
privat, da alle Elemente einer
Klasse per Vorgabe privat sind. Solange man also nichts anderes festlegt, bleiben sie
privat.
Wenn man allerdings in main()
(zum Beispiel) schreibt
Cat Boots;
Boots.itsAge=5; // Fehler! Kann nicht auf private Daten zugreifen!
zeigt der Compiler einen Fehler an. Praktisch hat man dem Compiler gesagt: »Ich
werde auf itsAge
, itsWeight
und Meow()
nur innerhalb der Elementfunktionen der Cat
-
Klasse zugreifen.« Im Beispiel findet jedoch ein Zugriff auf die Elementvariable itsAge
des Boots
-Objekts von außerhalb einer Cat
-Methode statt. Nur weil Boots
ein Objekt
der Klasse Cat
ist, heißt das nicht, daß man auf die privaten Teile von Boots
zugreifen
kann.
Dies ist ein Auslöser von häufiger Verwirrung bei Programmierneulingen in C++. Ich höre Sie förmlich schreien »Hey, ich habe gerade festgelegt, daß Boots eine Katze ist. Warum kann Boots nicht auf sein eigenes Alter zugreifen?«
Die Antwort lautet schlicht, daß Boots
kann, Sie aber nicht. Boots
kann innerhalb seiner
eigenen Methoden auf alle seine Bestandteile - öffentlich oder privat - zugreifen.
Auch wenn Sie eine Cat
-Klasse erzeugt haben, bedeutet das nicht, daß Sie alle ihre
Komponenten, die privat sind, auch sehen oder ändern können.
Um auf die Datenelemente von Cat
zugreifen zu können, deklariert man einen Abschnitt
der Klasse Cat
als public
:
class Cat
{
public:
unsigned int itsAge;
unsigned int itsWeight;
Meow();
};
Nunmehr sind itsAge
, itsWeight
und Meow()
öffentlich (public
). Der Compiler hat
nichts mehr an Boots.itsAge=5
auszusetzen.
Listing 6.1 enthält die Deklaration einer Cat
-Klasse mit öffentlichen Elementvariablen.
Listing 6.1: Zugriff auf die öffentlichen Elemente einer einfachen Klasse
1: // Deklaration einer Klasse und
2: // Definition eines Objekts dieser Klasse,
3:
4: #include <iostream.h> // für cout
5:
6: class Cat // Deklariert das Klassenobjekt
7: {
8: public: // folgende Elemente sind public
9: int itsAge;
10: int itsWeight;
11: };
12:
13:
14: int main()
15: {
16: Cat Frisky;
17: Frisky.itsAge = 5; // Wertzuweisung an die Elementvariable
18: cout << "Frisky ist eine Katze, die ;
19: cout << Frisky.itsAge << " Jahre alt ist.\n";
20: return 0;
21: }
Frisky ist eine Katze, die 5 Jahre alt ist.
Zeile 6 enthält das Schlüsselwort class
. Damit wird dem Compiler mitgeteilt, daß jetzt
eine Deklaration folgt. Der Name der neuen Klasse steht direkt hinter dem Schlüsselwort
class
. In diesem Fall lautet er Cat
.
Der Rumpf der Deklaration beginnt mit der öffnenden geschweiften Klammer in Zeile
7 und endet mit der schließenden geschweiften Klammer und dem Semikolon in Zeile
11. Zeile 8 enthält das Schlüsselwort public
, das anzeigt, daß alles, was folgt, öffentlich
ist, bis der Compiler auf das Schlüsselwort private
trifft oder das Ende der Klassendeklaration
erreicht ist.
Die Zeilen 9 und 10 enthalten die Deklarationen der Datenelemente der Klasse: itsAge
und itsWeight
.
Zeile 14 beginnt mit der main()
-Funktion des Programms. In Zeile 16 wird Frisky
als
Instanz von Cat
definiert, das heißt als ein Cat
-Objekt. Zeile 17 setzt Friskys
Alter auf
5. Die Zeile 18 und 19 verwenden die Elementvariable itsAge
, um die Meldung von
Frisky
auszugeben.
Versuchen Sie einmal, die Zeile 8 auszukommentieren und den Quellcode dann neu zu kompilieren. Sie werden eine Fehlermeldung zu Zeile 17 erhalten, da auf
itsAge
nicht länger öffentlich zugegriffen werden kann. Standardmäßig sind alle Elemente einer Klasse privat.
Als allgemeine Entwurfsregel gilt, daß man die Datenelemente einer Klasse privat halten sollte. Daher muß man öffentliche Elementfunktionen, die sogenannten Zugriffsmethoden , aufsetzen, über die andere Teile des Programms die Werte der privaten Elementvariablen abfragen oder verändern können.
Eine öffentliche Zugriffsmethode ist eine Elementfunktion einer Klasse, mit der man den Wert einer privaten Elementvariablen entweder lesen oder setzen kann.
Warum soll man sich mit einer weiteren Ebene des indirekten Zugriffs herumplagen? Schließlich wäre es doch einfacher und bequemer, die Daten direkt zu bearbeiten, statt den Umweg über die Zugriffsfunktionen zu gehen.
Zugriffsfunktionen gestatten es, die Einzelheiten der Speicherung von der Verwendung der Daten zu trennen. Damit kann man die Speicherung der Daten verändern, ohne daß man die Funktionen neu schreiben muß, die mit den Daten arbeiten.
Eine Funktion, die das Alter von Cat
benötigt und deshalb auf itsAge
direkt zugreift,
muß neu geschrieben werden, wenn Sie als der Autor der Cat
-Klasse Änderungen an
der Art und Weise, in der die betreffende Information gespeichert wird, vornehmen.
Wenn diese Funktion aber GetAge()
aufruft, kann Ihre Cat
-Klasse dafür sorgen, daß
der richtige Wert zurückgeliefert wird, unabhängig davon, wie das Alter zu ermitteln
ist. Die aufrufende Funktion muß nicht wissen, ob die Klasse das Alter als Integer vom
Typ unsigned short
oder long
speichert oder ob sie es bei Bedarf jedes Mal neu berechnet.
Auf diese Weise wird Ihr Programm einfacher zu warten. Ihr Programm wird eine längere Lebensdauer haben, da es nicht durch Änderungen am Entwurf veraltet.
In Listing 6.2 sehen Sie die Cat
-Klasse mit leichten Änderungen, um private Elementdaten
und öffentliche Zugriffsmethoden einzurichten. Es handelt sich dabei jedoch
nicht um ein ausführbares Listing.
Listing 6.2: Eine Klasse mit Zugriffsmethoden
1: // Deklaration der Klasse Cat
2: // Datenelemente sind privat, oeffentliche Zugriffsmethoden
3: // helfen die Werte der privaten Daten auszulesen oder zu setzen
4:
5: class Cat
6: {
7: public:
8: // oeffentliche Zugriffsmethoden
9: unsigned int GetAge();
10: void SetAge(unsigned int Age);
11:
12: unsigned int GetWeight();
13: void SetWeight(unsigned int Weight);
14:
15: // oeffentliche Elementfunktionen
16: void Meow();
17:
18: // private Elementdaten
19: private:
20: unsigned int itsAge;
21: unsigned int itsWeight;
22:
23: };
Diese Klasse hat fünf öffentliche Methoden. Die Zeilen 9 und 10 enthalten die Zugriffsmethoden
für itsAge
und die Zeilen 12 und 13 die Zugriffsmethoden für itsWeight
. Diese Zugriffsmethoden setzen die Elementvariablen und liefern ihre Werte
zurück.
Zeile 16 deklariert die öffentliche Elementfunktion Meow()
. Meow()
ist keine Zugriffsfunktion.
Weder liest sie eine Elementvariable aus noch setzt sie welche. Ihre Aufgabe
für die Klasse ist eine andere: Sie gibt das Wort Meow()
aus.
Die Zeile 20 und 21 deklarieren die Elementvariablen.
Um das Alter von Frisky
zu setzen, würden Sie den Wert an die SetAge()
-Methode
wie folgt übergeben:
Cat Frisky;
Frisky.SetAge(); // setzt das Alter von Frisky mit Hilfe der oeffentlichen
// Zugriffsmethode
Indem Sie Methoden und Daten als private
deklarieren, ermöglichen Sie es dem
Compiler, Programmschwächen und -fehler zu finden, bevor Sie zu echten Laufzeitfehlern
ausarten. Jeder Programmierer, der sein Geld wert ist, kann einen Weg finden,
private
-Deklarationen zu umgehen. Stroustrup, der Begründer von C++, sagte:
»Die Mechanismen der Zugriffssteuerung in C++ bieten Schutz gegen Zufälle - nicht
gegen Betrug.« (ARM, 1990.)
Die Syntax für das Schlüsselwort
class
lautet:class klassen_name
{
// hier Schluesselwort der Zugriffssteuerung
// hier Deklaration der Variablen und Methoden der Klasse
};Das Schlüsselwort
class
wird verwendet, um neue Typen zu deklarieren. Eine Klasse ist Sammlung von Datenelementen, bei denen es sich um Variablen aller Typen handeln kann, einschließlich anderer Klassen. Darüber hinaus enthält die Klasse Funktionen, auch Elementmethoden genannt, mit denen die Daten in der Klasse manipuliert und andere Aufgaben für die Klasse erledigt werden.Objekte des neuen Typs werden fast genauso definiert wie Variablen. Sie geben den Typ (
class
) und dann den Variablennamen (das Objekt) an. Der Zugriff auf die Datenelemente und die Elementfunktionen erfolgt über den Punktoperator (.).Die Schlüsselwörter zur Zugriffssteuerung werden benutzt, um Abschnitte der Klasse als
public
oderprivate
zu deklarieren. Die Vorgabe für die Zugriffssteuerung istprivate
. Jedes Schlüsselwort ändert die Zugriffssteuerung von diesem Punkt an bis zum Ende der Klasse oder bis zum nächsten Zugriffsspezifizierer. Klassendeklarationen enden mit einer schließenden geschweiften Klammer und einem Semikolon.class Cat
{
public:
unsigned int Age;
unsigned int Weight;
void Meow();
};
Cat Frisky;
Frisky.Age = 8;
Frisky.Weight = 18;
Frisky.Meow();class Car
{
public: // die nächsten fünf sind public
void Start();
void Accelerate();
void Brake();
void SetYear(int year);
int GetYear();
private: // der Rest ist private
int Year;
Char Model [255];
}; // Ende der Klassendeklaration
Car OldFaithful; // eine Instanz von car
int bought; // eine lokale Integer-Variable
OldFaithful.SetYear(84) ; // weist year 84 zu
bought = OldFaithful.GetYear(); // setzt bought auf 84
OldFaithful.Start(); // ruft die start-Methode auf
Wie Sie gesehen haben, stellen Zugriffsfunktionen eine öffentliche Schnittstelle zu den privaten Datenelementen der Klasse dar. Jede Zugriffsfunktion muß wie alle anderen Methoden einer Klasse, die Sie deklarieren, implementiert werden. Diese Implementierung wird auch Definition genannt.
Die Definition einer Elementfunktion beginnt mit dem Namen der Klasse, gefolgt von
zwei Doppelpunkten, dem Namen der Funktion und ihren Parametern. Listing 6.3
zeigt die vollständige Deklaration einer einfachen Cat
-Klasse mit der Implementierung
einer Zugriffsfunktion und einer allgemeinen Elementfunktion.
Listing 6.3: Methoden einer einfachen Klasse implementieren
1: // Zeigt die Deklaration einer Klasse und
2: // die Definition von Klassenmethoden
3:
4: #include <iostream.h> // für cout
5:
6: class Cat // Beginn der Klassendeklaration
7: {
8: public: // Beginn des oeffentlichen Abschnitts
9: int GetAge(); // Zugriffsfunktion
10: void SetAge (int age); // Zugriffsfunktion
11: void Meow(); // Allgemeine Funktion
12: private: // Beginn des privaten Abschnitts
13: int itsAge; // Elementvariable
14: };
15:
16: // Die oeffentliche Zugriffsfunktion GetAge gibt
17: // den Wert des Datenelements itsAge zurück.
18: int Cat::GetAge()
19: {
20: return itsAge;
21: }
22:
23: // Definition der oeffentlichen
24: // Zugriffsfunktion SetAge .
25: // Gibt Datenelement itsAge zurück.
26: void Cat::SetAge(int age)
27: {
28: // Elementvariable itsAge auf den als
29: // Parameter age uebergebenen Wert setzen.
30: itsAge = age;
31: }
32:
33: // Definition der Methode Meow
34: // Rueckgabe: void
35: // Parameter: Keine
36: // Aktion: Gibt "meow" auf dem Bildschirm aus
37: void Cat::Meow()
38: {
39: cout << "Miau.\n";
40: }
41:
42: // Eine Katze erzeugen, Alter festlegen, Miauen lassen
43: // Alter mitteilen lassen, dann erneut miauen.
44: int main()
45: {
46: Cat Frisky;
47: Frisky.SetAge(5);
48: Frisky.Meow();
49: cout << "Frisky ist eine Katze, die " ;
50: cout << Frisky.GetAge() << " Jahre alt ist.\n";
51: Frisky.Meow();
52; return 0;
53: }
Miau.
Frisky ist eine Katze, die 5 Jahre alt ist.
Miau.
Die Zeilen 6 bis 14 enthalten die Definition der Klasse Cat
. Das Schlüsselwort public
in Zeile 8 teilt dem Compiler mit, daß die nachfolgenden Elemente öffentlich sind. Zeile
9 deklariert die öffentliche Zugriffsmethode GetAge()
, die den Zugriff auf die in Zeile
13 deklarierte private Elementvariable itsAge
realisiert. In Zeile 10 steht die öffentliche
Zugriffsfunktion SetAge()
. Diese Funktion übernimmt eine ganze Zahl als Argument
und setzt itsAge
auf den Wert dieses Arguments.
Zeile 11 deklariert die Klassenmethode Meow()
, bei der es sich nicht um eine Zugriffsmethode
handelt. In diesem Falle ist es eine allgemeine Methode, die das Wort
»Meow« auf dem Bildschirm ausgibt.
Zeile 12 leitet den privaten Abschnitt ein, der nur die Deklaration in Zeile 13 für die
private Elementvariable itsAge
enthält. Die Klassendeklaration endet mit einer schließenden
geschweiften Klammer und dem Semikolon in Zeile 14.
Die Zeilen 18 bis 21 enthalten die Definition der Elementfunktion GetAge()
. Diese
Methode übernimmt keine Parameter, gibt aber eine ganze Zahl zurück. Beachten
Sie, daß Klassenmethoden den Klassennamen enthalten, gefolgt von zwei Doppelpunkten
und dem Namen der Funktion (siehe Zeile 18). Diese Syntax weist den Compiler
an, daß die hier definierte Funktion GetAge()
diejenige ist, die in der Klasse Cat
deklariert wurde. Mit Ausnahme der Kopfzeile wird die Funktion GetAge()
wie jede andere
Funktion erzeugt.
Die Funktion GetAge()
besteht nur aus einer Zeile und gibt den Wert in itsAge
zurück.
Beachten Sie, daß die Funktion main()
nicht auf itsAge
zugreifen kann, da itsAge
privat
zur Klasse Cat
ist. Die Funktion main()
hat Zugriff auf die öffentliche Methode GetAge()
. Da GetAge()
eine Elementfunktion der Klasse Cat
ist, hat sie uneingeschränkten
Zugriff auf die Variable itsAge
. Dieser Zugriff erlaubt GetAge()
, den Wert von
itsAge
an main()
zurückzugeben.
In Zeile 26 beginnt die Definition der Elementfunktion SetAge()
. Diese Funktion übernimmt
einen ganzzahligen Parameter und setzt in Zeile 30 den Wert von itsAge
auf
den Wert dieses Parameters. Da es sich um eine Elementfunktion der Klasse Cat
handelt,
hat SetAge()
direkten Zugriff auf die Elementvariable itsAge
.
In Zeile 37 beginnt die Definition - oder Implementierung - der Methode Meow()
der
Klasse Cat
. Es handelt sich um eine einzeilige Funktion, die das Wort Meow
gefolgt von
einer Zeilenschaltung auf dem Bildschirm ausgibt. (Wie Sie wissen, bewirkt das Zeichen
\n
eine neue Zeile auf dem Bildschirm.)
Zeile 44 beginnt den Rumpf des Programms mit der bekannten Funktion main()
. Die
Funktion main()
übernimmt hier keine Argumente. In Zeile 46 deklariert main()
ein
Cat
-Objekt namens Frisky
. In Zeile 47 wird der Wert 5 an die Elementvariable itsAge
mit Hilfe der Zugriffsmethode SetAge()
zugewiesen. Beachten Sie, daß der Aufruf dieser
Methode mit dem Klassennamen (Frisky
) gefolgt vom Punktoperator (.
) und dem
Methodennamen (SetAge()
) erfolgt. In der gleichen Weise kann man jede andere Methode
in einer Klasse aufrufen.
Zeile 48 ruft die Elementfunktion Meow()
auf, und Zeile 49 gibt eine Meldung mittels
der Zugriffsmethode GetAge()
aus. In Zeile 51 steht ein weiterer Aufruf von Meow()
.
Für die Definition einer Integer-Variablen gibt es zwei Verfahren. Man kann die Variable definieren und ihr später im Programm einen Wert zuweisen:
int Weight; // eine Variable definieren
... // hier steht ein anderer Code
Weight = 7; // der Variablen einen Wert zuweisen
Man kann die Ganzzahl auch definieren und sie sofort initialisieren:
int Weight = 7; // definieren und mit 7 initialisieren
Die Initialisierung kombiniert die Definition der Variablen mit einer anfänglichen Zuweisung. Nichts hindert Sie daran, den Wert später zu ändern. Die Initialisierung stellt sicher, daß eine Variable niemals ohne einen sinnvollen Wert vorkommt.
Wie initialisiert man nun die Datenelemente einer Klasse? Klassen haben eine spezielle
Elementfunktion, einen sogenannten Konstruktor. Der Konstruktor kann bei Bedarf
Parameter übernehmen, aber keinen Rückgabewert liefern - nicht einmal void
. Der
Konstruktor ist eine Klassenmethode mit demselben Namen wie die Klasse selbst.
Zur Deklaration eines Konstruktors gehört in der Regel auch die Deklaration eines Destruktors
. Genau wie Konstruktoren Objekte der Klasse erzeugen und initialisieren,
führen Destruktoren Aufräumungsarbeiten nach Zerstörung des Objekts aus und geben
reservierten Speicher frei. Ein Destruktor hat immer den Namen der Klasse, wobei
eine Tilde (~
) vorangestellt ist. Destruktoren übernehmen keine Argumente und haben
keinen Rückgabewert. Der Destruktor der Klasse Cat
ist demnach wie folgt
deklariert:
~Cat();
Wenn Sie keinen Konstruktor oder Destruktor deklarieren, erzeugt der Compiler einen für Sie. Dieser Standardkonstruktor und der Standarddestruktor übernehmen keine Argumente und tun auch nichts.
Verdankt der Standardkonstruktor seinen Namen der Tatsache, daß er keine Argumente übernimmt oder daß er vom Compiler erzeugt wird, wenn ich keinen eigenen Konstruktor deklariere?
Antwort: Ein Konstruktor, der keine Argumente übernimmt, heißt Standardkonstruktor - egal ob Sie oder der Compiler ihn erzeugen. Sie erhalten standardmäßig einen Standardkonstruktor.
Etwas verwirrend ist, daß man als Standarddestruktor den Destruktor bezeichnet, der vom Compiler gestellt wird. Da kein Destruktor irgendwelche Argumente übernimmt, zeichnet sich der Standarddestruktor dadurch aus, daß er keine Aktion ausführt - er hat einen leeren Funktionskörper.
Wozu braucht man einen Konstruktor, der nichts bewirkt? Zum einem ist es eine Frage der richtigen Form. Alle Objekte müssen mit einem Konstruktor erzeugt und mit einem Destruktor wieder aufgelöst werden, und diese »nichts tuenden« Funktionen werden eben hierfür bei Bedarf aufgerufen. Um jedoch ein Objekt ohne Übergabe von Parametern zu deklarieren, wie in
Cat Rags; // Rags übernimmt keine Parameter
müssen Sie einen Konstruktor der folgenden Form haben
Cat();
Wenn Sie ein Objekt einer Klasse definieren, wird der Konstruktor aufgerufen. Übernimmt
der Cat
-Konstruktor zwei Parameter, können Sie ein Cat
-Objekt wie folgt definieren:
Cat Frisky (5,7);
Hätte der Konstruktor nur einen Parameter, würden Sie schreiben
Cat Frisky (3);
Für den Fall, daß der Konstruktor keine Parameter übernimmt (es sich also um einen Standardkonstruktor handelt), lassen Sie die Klammern weg und schreiben
Cat Frisky;
Dies ist die Ausnahme zu der Regel, daß alle Funktionen Klammern erfordern, auch wenn sie keine Parameter übernehmen. Deshalb ist die Schreibweise
Cat Frisky;
gültig. Interpretiert wird dies als Aufruf des Standardkonstruktors. Er liefert keine Parameter und weist auch keine Klammern auf.
Beachten Sie, daß Sie den Standardkonstruktor des Compilers nicht verwenden müssen. Es steht Ihnen jederzeit frei, einen eigenen Standardkonstruktor zu schreiben - das heißt, einen Konstruktor, der keine Parameter übernimmt. Es steht Ihnen frei, Ihren Standardkonstruktor mit einem Funktionskörper auszustatten, in der die Klasse bei Bedarf initialisiert werden kann.
Aus Formgründen sollte man einen Destruktor deklarieren, wenn man einen Konstruktor deklariert, auch wenn der Destruktor keine weiteren Aufgaben wahrnimmt. Obwohl der Standarddestruktor durchaus korrekt arbeitet, stört es nicht, einen eigenen Destruktor zu deklarieren. Der Code wird dadurch verständlicher.
In Listing 6.4 wurde die Klasse Cat
so umgeschrieben, daß ein Konstruktor zur Initialisierung
des Cat
-Objekts verwendet wird, wobei das Alter (age
) auf den von Ihnen übergebenen
Anfangswert gesetzt wird. Außerdem zeigt das Listing, wo der Aufruf des Destruktors
stattfindet.
Listing 6.4: Konstruktoren und Destruktoren
1: // Zeigt die Deklaration eines Konstruktors und
2: // eines Destruktors für die Klasse Cat
3:
4: #include <iostream.h> // für cout
5:
6: class Cat // Beginn der Klassendeklaration
7: {
8: public: // Beginn des oeffentlichen Abschnitts
9: Cat(int initialAge); // Konstruktor
10: ~Cat(); // Destruktor
11: int GetAge(); // Zugriffsfunktion
12: void SetAge(int age); // Zugriffsfunktion
13: void Meow();
14: private: // Beginn des privaten Abschnitts
15: int itsAge; // Elementvariable
16: };
17:
18: // Konstruktor von Cat
19: Cat::Cat(int initialAge)
20: {
21: itsAge = initialAge;
22: }
23:
24: Cat::~Cat() // Destruktor, fuehrt keine Aktionen aus
25: {
26: }
27:
28: // Die oeffentliche Zugriffsfunktion GetAge gibt
29: // den Wert des Datenelements itsAge zurück.
30: int Cat::GetAge()
31: {
32: return itsAge;
33: }
34:
35: // Definition von SetAge als oeffentliche
36: // Zugriffsfunktion
37:
38: void Cat::SetAge(int age)
39: {
40: // Elementvariable itsAge auf den als
41: // Parameter age uebergebenen Wert setzen.
42: itsAge = age;
43: }
44:
45: // Definition der Methode Meow
46: // Rueckgabe: void
47: // Parameter: Keine
48: // Aktion: Gibt "Miau" auf dem Bildschirm aus
49: void Cat::Meow()
50: {
51: cout << "Miau.\n";
52: }
53:
54: // Eine Katze erzeugen, Alter festlegen, Miauen lassen
55 // Alter mitteilen lassen, dann erneut miauen.
56: int main()
57: {
58: Cat Frisky(5);
59: Frisky.Meow();
60: cout << "Frisky ist eine Katze, die " ;
61: cout << Frisky.GetAge() << " Jahre alt ist.\n";
62: Frisky.Meow();
63: Frisky.SetAge(7);
64: cout << "Jetzt ist Frisky " ;
65: cout << Frisky.GetAge() << " Jahre alt.\n";
66; return 0;
67: }
Miau.
Frisky ist eine Katze, die 5 Jahre alt ist.
Miau.
Jetzt ist Frisky 7 Jahre alt.
Miau.
Listing 6.4 gleicht Listing 6.3, nur daß in Zeile 9 ein Konstruktor hinzufügt wurde, der
eine Ganzzahl übernimmt. Zeile 10 deklariert den Destruktor. Destruktoren übernehmen
niemals Parameter, und weder Konstruktoren noch Destruktoren geben Werte
zurück - nicht einmal void
.
Die Zeilen 19 bis 22 zeigen die Implementierung des Konstruktors, die der Implementierung
der Zugriffsfunktion SetAge()
ähnlich ist. Es gibt keinen Rückgabewert.
Die Zeilen 24 bis 26 zeigen die Implementierung des Destruktors ~Cat()
. Diese Funktion
führt nichts aus, man muß aber die Definition der Funktion einbinden, wenn man
sie in der Klassendeklaration deklariert.
Zeile 58 enthält die Definition des Cat
-Objekts Frisky
. Der Wert 5 wird an den Konstruktor
von Frisky
übergeben. Man muß hier nicht SetAge()
aufrufen, da Frisky
mit
dem Wert 5 in seiner Elementvariablen itsAge
erzeugt wurde. Zeile 61 zeigt diesen
Wert zur Bestätigung an. Auf Zeile 63 wird der Variablen itsAge
von Frisky
der neue
Wert 7 zugewiesen. Zeile 65 gibt den neuen Wert aus.
Verwenden Sie Konstruktoren, um Ihre Objekte zu initialisieren. | Achten Sie darauf, daß Ihre Konstruktoren und Destruktoren keine Rückgabewerte haben. |
Wenn man eine Elementfunktion einer Klasse als const
deklariert, verspricht man
fest, daß die Methode die Werte der Datenelemente der Klasse auf keinen Fall ändert.
Um eine Klassenmethode als konstant zu deklarieren, setzt man das Schlüsselwort
const
hinter die Klammern und vor das Semikolon. Die folgende Deklaration der konstanten
Elementfunktion EineFunktion()
übernimmt keine Argumente und gibt void
zurück. Sie sieht folgendermaßen aus:
void EineFunktion() const;
Zugriffsfunktionen werden häufig mit Hilfe des Modifizierers const
als konstante Funktionen
deklariert. Die Klasse Cat
hat zwei Zugriffsfunktionen:
void SetAge(int anAge);
int GetAge();
SetAge()
kann nicht const
sein, da diese Funktion die Elementvariable itsAge
ändern
soll. Dagegen kann und sollte man GetAge()
als const
deklarieren, da diese Funktion
die Klasse überhaupt nicht verändert. Sie gibt einfach den aktuellen Wert der Elementvariablen
itsAge
zurück. Die Deklaration dieser beiden Funktionen sollte daher wie
folgt aussehen:
void SetAge(int anAge);
int GetAge() const;
Wenn man eine Funktion als konstant deklariert und dann die Implementierung dieser
Funktion das Objekt ändert (durch Änderung des Wertes eines ihrer Elemente), gibt
der Compiler eine entsprechende Fehlermeldung aus. Wenn Sie zum Beispiel GetAge()
so implementieren, daß die Methode die Anzahl der Anfragen nach dem Alter
von Cat
protokolliert, ernten Sie einen Compiler-Fehler, denn der Aufruf dieser Methode
würde das Cat
-Objekt verändern.
Verwenden Sie
const
wo immer möglich. Deklarieren Sie Elementfunktionen, die den Zustand des Objekts nicht ändern sollen, stets alsconst
. Der Compiler kann Ihnen dann die Fehlersuche erleichtern. Das geht schneller und ist weniger aufwendig, als wenn man es selbst machen müßte.
Es gehört zum guten Programmierstil, so viele Methoden wie möglich als const
zu deklarieren.
Damit kann bereits der Compiler Fehler abfangen, und die Fehler zeigen
sich nicht erst im laufenden Programm.
Wie Sie bereits wissen, sind Klienten die Teile des Programms, die Objekte einer bestimmten Klasse erzeugen und verwenden. Man kann sich die öffentliche Schnittstelle zu dieser Klasse - die Klassendeklaration - als Vertrag mit diesen Klienten vorstellen. Der Vertrag sagt aus, wie sich die Klasse verhält.
Durch die Klassendeklaration von Cat
setzen Sie zum Beispiel einen Vertrag auf, der
besagt, daß das Alter einer Katze (eines Cat
-Objekts) vom Konstruktor der Klasse initialisiert,
durch die Zugriffsfunktion SetAge()
zugewiesen und durch die Zugriffsfunktion
GetAge()
gelesen werden kann. Außerdem sagen Sie zu, daß jedes Cat
-Objekt
weiß, wie zu Miauen ist (Funktion Meow()
). Achten Sie darauf, daß Sie in der öffentlichen
Schnittstelle nichts über die Elementvariable itsAge
verraten. Dieses Implementierungsdetail
ist nicht Teil Ihres Vertrags. Sie stellen ein Alter zur Verfügung (GetAge()
) und geben ein Alter an (SetAge()
). Der Mechanismus jedoch (itsAge
) ist nicht
sichtbar.
Wenn man GetAge()
zu einer konstanten Funktion macht (wie es empfohlen wird),
verspricht der Vertrag auch, daß GetAge()
nicht das Cat
-Objekt ändert, auf dem die
Funktion aufgerufen wird.
C++ ist äußerst typenstreng, womit gemeint ist, daß der Compiler die Vertragsbedingungen auf Einhaltung prüft und einen Fehler meldet, wenn sie verletzt werden. In Listing 6.5 sehen Sie ein Programm, das sich nicht kompilieren läßt, da Vertragsbedingungen gebrochen wurden.
Listing 6.5: Fehlerhafte Schnittstelle
1: // Demonstration von Compiler-Fehlern
2:
3:
4: #include <iostream.h> // für cout
5:
6: class Cat
7: {
8: public:
9: Cat(int initialAge);
10: ~Cat();
11: int GetAge() const; // const-Zugriffsfunktion
12: void SetAge (int age);
13: void Meow();
14: private:
15: int itsAge;
16: };
17:
18: // Konstruktor von Cat
19: Cat::Cat(int initialAge)
20: {
21: itsAge = initialAge;
21: cout << "Cat Constructor\n";
22: }
23:
24: Cat::~Cat() // Destruktor, fuehrt keine Aktion aus
25: {
26: cout << "Cat Destructor\n";
27: }
28: // GetAge, const-Funktion
29: // aber wir verletzen const!
30: int Cat::GetAge() const
31: {
32: return (itsAge++); // verletzt const!
33: }
34:
35: // Definition von SetAge, public
36: // Zugriffsfunktion
37:
38: void Cat::SetAge(int age)
39: {
40: // setzt Elementvariable itsAge auf den
41: // Wert, der vom Parameter age uebergeben wird
42: itsAge = age;
43: }
44:
45: // Definition der Meow-Methode
46: // Rueckgabewert: void
47: // Parameter: Keine
48: // Aktion: Gibt "miau" auf dem Bildschirm aus
49: void Cat::Meow()
50: {
51: cout << "Miau.\n";
52: }
53:
54: // zeigt verschiedene Verletzungen der
55 // Schnittstelle und resultierende Compiler-Fehler
56: int main()
57: {
58: Cat Frisky; // entspricht nicht der Deklaration
59: Frisky.Meow();
60: Frisky.Bark(); // Nein, du Dummkopf, Katzen bellen nicht.
61: Frisky.itsAge = 7; // itsAge ist private
62: return 0;
63: }
Wie schon oben erwähnt, läßt sich dieses Programm nicht kompilieren. Deshalb gibt es auch keine Ausgabe. (Aber es hat Spaß gemacht, ein Programm zu schreiben, das so viele Fehler enthält.)
Zeile 11 deklariert GetAge()
als konstante Zugriffsfunktion - und so sollte es auch sein.
Im Rumpf von GetAge()
jedoch, Zeile 32, wird die Elementvariable itsAge
inkrementiert.
Da diese Methode als konstant deklariert wurde, darf der Wert von itsAge
nicht
geändert werden. Deshalb wird ein Fehler gemeldet, wenn das Programm kompiliert
wird.
In Zeile 13 wird bei der Deklaration der Funktion Meow()
const
weggelassen. Das ist
zwar nicht unbedingt ein Fehler, jedoch schlechter Programmierstil. Ein guter Entwurf
würde berücksichtigen, daß diese Methode die Elementvariablen von Cat
nicht ändert.
Deshalb sollte Meow()
konstant sein.
Zeile 58 definiert eine Cat
-Objekt, Frisky
. Cat
hat jetzt einen Konstruktor, der einen
Integer als Parameter übernimmt. Das bedeutet, daß Sie einen Parameter übergeben
müssen. Da in Zeile 58 kein Parameter vorhanden ist, wird ein Fehler gemeldet.
Wenn Sie selbst einen Konstruktor stellen, erzeugt der Compiler keinen mehr. Wenn Sie also einen Konstruktor erzeugen, der einen Parameter übernimmt, gibt es keinen Standardkonstruktor, es sei denn, Sie schreiben einen.
Zeile 60 enthält den Aufruf an die Klassenmethode Bark()
. Da Bark()
nie deklariert
wurde, ist der Aufruf unwirksam.
In Zeile 61 wird itsAge
der Wert 7 zugewiesen. itsAge
ist jedoch ein privates Datenelement,
und deshalb wird ein Fehler ausgegeben, wenn das Programm kompiliert
wird.
Warum mit dem Compiler Fehler abfangen?
Auch wenn es absolut phantastisch wäre, 100 % fehlerfreien Code zu schreiben, gibt es nur wenig Programmierer, die dazu fähig sind. Viele Programmierer haben jedoch ein System entwickelt, mit dem sie die Anzahl der Fehler auf ein Minimum senken, indem sie sie möglichst früh im Programmierablauf abfangen und beheben.
Obwohl Compiler-Fehler äußerst ärgerlich sind und einen Programmierer an den Rand der Verzweiflung treiben können, sind sie besser als die Alternative. Bei einer weniger strengen Sprache können Sie ihre Verträge verletzen, ohne daß der Compiler dies auch nur bemängelt. Und später dann, zur Laufzeit, wird Ihr Programm abstürzen - wahrscheinlich genau dann, wenn Ihnen Ihr Boss über die Schulter schaut.
Compiler-Fehler - das heißt, während der Kompilierung gefundene Fehler - sind weitaus angenehmer als Laufzeitfehler - das heißt, während der Ausführung des Programms auftretende Fehler. Das hängt damit zusammen, daß sich Compiler-Fehler zuverlässiger einkreisen lassen. Es kann durchaus sein, daß ein Programm viele Male läuft, ohne alle möglichen Wege im Code zu durchlaufen. Somit kann ein Laufzeitfehler eine ganze Weile im Verborgenen bleiben. Compiler-Fehler werden während der Kompilierung gefunden, so daß man sie leicht identifizieren und beseitigen kann. Deshalb hat es sich bewährt, bereits im frühen Stadium der Programmentwicklung Fehler mit Hilfe des Compiler abzufangen.
Jede deklarierte Funktion muß eine Definition haben. Die Definition einer Funktion bezeichnet man auch als Implementierung. Wie jede andere Funktion, hat auch die Definition einer Klassenmethode einen Funktionskopf und einen Funktionsrumpf.
Die Definition muß in einer Datei stehen, die der Compiler finden kann. Die meisten
C++-Compiler erwarten, daß diese Datei auf .C
oder .CPP
endet. In diesem Buch
kommt die Erweiterung .CPP
zur Anwendung. Es empfiehlt sich aber, daß Sie in Ihrem
Compiler-Handbuch nachsehen, welche Erweiterung Ihr Compiler bevorzugt.
Viele Compiler nehmen an, daß es sich bei Dateien mit der Erweiterung
.C
um C-Programme handelt und daß C++-Programme mit.CPP
enden. Man kann zwar eine beliebige Erweiterung verwenden, mit.CPP
schafft man aber klare Verhältnisse.
Obwohl man durchaus die Deklaration in der gleichen Datei unterbringen kann wie
die Definition, gehört dieses Vorgehen nicht zum guten Programmierstil. Die von den
meisten Programmierern anerkannte Konvention ist die Unterbringung der Deklaration
in einer sogenannten Header-Datei, die gewöhnlich den gleichen Namen hat, aber
auf .H
, .HP
oder .HPP
endet. In diesem Buch werden Header-Dateien mit der Erweiterung
.HPP
verwendet. Auch hierzu sollten Sie im Compiler-Handbuch nachschlagen.
So schreibt man zum Beispiel die Deklaration der Klasse Cat
in eine Datei namens
CAT.HPP
und die Definition der Klassenmethoden in der Datei CAT.CPP
. Die Header-
Datei bindet man dann in die .CPP
-Datei ein, indem man den folgenden Code an den
Beginn von CAT.CPP
schreibt:
#include "Cat.hpp"
Damit weist man den Compiler an, CAT.HPP
in die Datei einzulesen, gerade so als hätte
man ihren Inhalt an dieser Stelle eingetippt. Bitte beachten Sie, daß einige Compiler
darauf bestehen, daß die Groß- und Kleinschreibung zwischen Ihrer #include
-Anweisung
und Ihrem Dateisystem übereinstimmt.
Warum macht man sich die Mühe, diese Angaben in separate Dateien zu schreiben,
wenn man sie ohnehin wieder zusammenführt? In der Regel kümmern sich die Klienten
einer Klasse nicht um die Details der Implementierung. Es genügt demnach, die
Header-Datei zu lesen, um alle erforderlichen Angaben beisammenzuhaben. Die Implementierungsdateien
können dann ruhig ignoriert werden. Und außerdem könnte es
Ihnen auch passieren, daß Sie die .HPP
-Datei in mehr als einer .CPP
-Datei einbinden
müssen.
Die Deklaration einer Klasse sagt dem Compiler, um welche Klasse es sich handelt, welche Daten sie aufnimmt und welche Funktionen sie ausführt. Die Deklaration der Klasse bezeichnet man als Schnittstelle (Interface), da der Benutzer hieraus die Einzelheiten zur Interaktion mit der Klasse entnehmen kann. Die Schnittstelle speichert man in der Regel in einer .HPP-Datei, einer sogenannten Header-Datei.
Die Funktionsdefinition sagt dem Compiler, wie die Funktion arbeitet. Man bezeichnet die Funktionsdefinition als Implementierung der Klassenmethode und legt sie in einer .CPP-Datei ab. Die Implementierungsdetails der Klasse sind nur für den Autor der Klasse von Belang. Klienten der Klasse - das heißt, die Teile des Programms, die die Klasse verwenden - brauchen nicht zu wissen (und sich nicht darum zu kümmern), wie die Funktionen implementiert sind.
So wie man den Compiler auffordern kann, eine normale Funktion als Inline-Funktion
einzubinden, kann man auch Klassenmethoden als inline
deklarieren. Das Schlüsselwort
inline
steht vor dem Rückgabewert. Die Inline-Implementierung der Funktion
GetWeight()
sieht zum Beispiel folgendermaßen aus:
inline int Cat::GetWeight()
{
return itsWeight; // das Datenelement Weight zurueckgeben
}
Man kann auch die Definition der Funktion in die Deklaration der Klasse schreiben, was die Funktion automatisch zu einer Inline-Funktion macht:
class Cat
{
public:
int GetWeight() { return itsWeight; } // inline
void SetWeight(int aWeight);
};
Beachten Sie die Syntax der Definition von GetWeight()
. Der Rumpf der Inline-Funktion
beginnt unmittelbar nach der Deklaration der Klassenmethode. Nach den Klammern
steht kein Semikolon. Wie bei jeder Funktion beginnt die Definition mit einer
öffnenden geschweiften Klammer und endet mit einer schließenden geschweiften
Klammer. Wie üblich spielen Whitespace-Zeichen keine Rolle. Man hätte die Deklaration
auch wie folgt schreiben können:
class Cat
{
public:
int GetWeight()
{
return itsWeight;
} // inline
void SetWeight(int aWeight);
};
Die Listings 6.6 und 6.7 bringen eine Neuauflage der Klasse Cat
. Dieses Mal aber
wurde die Deklaration in CAT.HPP
und die Implementierung der Funktionen in CAT.CPP
untergebracht. Listing 6.7 realisiert außerdem sowohl die Zugriffsfunktion als auch die
Funktion Meow()
als Inline-Funktionen.
Listing 6.6: Die Klassendeklaration von Cat in CAT.HPP
1: #include <iostream.h>
2: class Cat
3: {
4: public:
5: Cat (int initialAge);
6: ~Cat();
7: int GetAge() { return itsAge;} // inline!
8: void SetAge (int age) { itsAge = age;} // inline!
9: void Meow() { cout << "Miau.\n";} // inline!
10: private:
11: int itsAge;
12: };
Listing 6.7: Die Implementierung von Cat in CAT.CPP
1: // Zeigt Inline-Funktionen
2: // und das Einbinden von Header-Dateien
3:
4: #include "cat.hpp" // Header-Dateien unbedingt einbinden!
5:
6:
7: Cat::Cat(int initialAge) // Konstruktor
8: {
9: itsAge = initialAge;
10: }
11:
12: Cat::~Cat() // Destruktor, keine Aktionen
13: {
14: }
15:
16: // Katze erzeugen, Alter festlegen, miauen lassen
17: // Alter ausgeben, dann erneut miauen.
18: int main()
19: {
20: Cat Frisky(5);
21: Frisky.Meow();
22: cout << "Frisky ist eine Katze, die " ;
23: cout << Frisky.GetAge() << " Jahre alt ist.\n";
24: Frisky.Meow();
25: Frisky.SetAge(7);
26: cout << "Jetzt ist Frisky " ;
27: cout << Frisky.GetAge() << " Jahre alt.\n";
28: return 0;
29: }
Miau.
Frisky ist eine Katze, die 5 Jahre alt ist.
Miau.
Jetzt ist Frisky 7 Jahre alt.
Der Code aus Listing 6.6 und 6.7 entspricht dem Code aus Listing 6.4 mit Ausnahme
von drei Methoden, die in der Deklarationsdatei als inline
implementiert wurden, und
der Trennung der Deklaration in einer separaten Header-Datei CAT.HPP
.
Zeile 7 deklariert die Funktion GetAge()
und stellt ihre Inline-Implementierung bereit.
Die Zeilen 8 und 9 enthalten weitere Inline-Funktionen, wobei aber die Funktionalität
dieser Funktionen unverändert aus den vorherigen »Outline«-Implementierungen übernommen
wurde.
Die Anweisung #include "cat.hpp"
in Zeile 4 von Listing 6.7 bindet das Listing von
CAT.HPP
(Listing 6.6) ein. Durch das Einbinden von CAT.HPP
teilen Sie dem Prä-Compiler
mit, CAT.HPP
so in die Datei einzulesen, als ob CAT.HPP
direkt ab Zeile 5 eingegeben
worden wäre.
Diese Vorgehensweise erlaubt es Ihnen, Ihre Deklarationen getrennt von Ihrer Implementierung
in einer eigenen Datei unterzubringen und sie trotzdem verfügbar zu haben,
wenn sie vom Compiler benötigt werden. In der C++-Programmierung wird häufig
so verfahren: Klassendeklarationen werden in der Regel in einer .HPP
-Datei
abgelegt, die dann mit #include
in die verbundene .CPP
-Datei eingebunden wird.
In den Zeilen 18 bis 29 finden Sie die gleiche main()
-Funktion wie in Listing 6.4, was
beweist, daß die inline
-Implementierung der Funktionen keinen Einfluß auf ihre
Funktionsweise und ihre Verwendung hat.
Es ist nicht unüblich, komplexe Klassen aufzubauen, indem man einfachere Klassen
deklariert und sie in die Deklaration der komplexeren Klasse einbindet. Beispielsweise
kann man eine Klasse aWheel
, eine Klasse aMotor
, eine Klasse aTransmission
usw. deklarieren
und dann zur Klasse aCar
kombinieren. Damit deklariert man eine Beziehung:
Ein Auto hat einen Motor, es hat Räder, es hat eine Kraftübertragung.
Sehen wir uns ein weiteres Beispiel an. Ein Rechteck setzt sich aus Linien zusammen.
Eine Linie ist durch zwei Punkte definiert. Ein Punkt ist durch eine X- und eine Y-Koordinate
festgelegt. Listing 6.8 zeigt eine vollständige Deklaration einer Klasse Rectangle
, wie sie in RECTANGLE.HPP
stehen könnte. Da ein Rechteck mit vier Linien definiert
ist, die vier Punkte verbinden, und sich jeder Punkt auf eine Koordinate in einem
Diagramm bezieht, wird zuerst die Klasse Point
deklariert, um die X,Y-Koordinaten jedes
Punkts aufzunehmen. Listing 6.9 zeigt eine vollständige Deklaration beider Klassen.
Listing 6.8: Deklaration einer vollständigen Klasse
1: // Beginn von Rect.hpp
2: #include <iostream.h>
3: class Point // nimmt X,Y-Koordinaten auf
4: {
5: // Kein Konstruktor, Standardkonstruktor verwenden
6: public:
7: void SetX(int x) { itsX = x; }
8: void SetY(int y) { itsY = y; }
9: int GetX()const { return itsX;}
10: int GetY()const { return itsY;}
11: private:
12: int itsX;
13: int itsY;
14: }; // Ende der Klassendeklaration von Point
15:
16:
17: class Rectangle
18: {
19: public:
20: Rectangle (int top, int left, int bottom, int right);
21: ~Rectangle () {}
22:
23: int GetTop() const { return itsTop; }
24: int GetLeft() const { return itsLeft; }
25: int GetBottom() const { return itsBottom; }
26: int GetRight() const { return itsRight; }
27:
28: Point GetUpperLeft() const { return itsUpperLeft; }
29: Point GetLowerLeft() const { return itsLowerLeft; }
30: Point GetUpperRight() const { return itsUpperRight; }
31: Point GetLowerRight() const { return itsLowerRight; }
32:
33: void SetUpperLeft(Point Location) {itsUpperLeft = Location;}
34: void SetLowerLeft(Point Location) {itsLowerLeft = Location;}
35: void SetUpperRight(Point Location) {itsUpperRight = Location;}
36: void SetLowerRight(Point Location) {itsLowerRight = Location;}
37:
38: void SetTop(int top) { itsTop = top; }
39: void SetLeft (int left) { itsLeft = left; }
40: void SetBottom (int bottom) { itsBottom = bottom; }
41: void SetRight (int right) { itsRight = right; }
42:
43: int GetArea() const;
44:
45: private:
46: Point itsUpperLeft;
47: Point itsUpperRight;
48: Point itsLowerLeft;
49: Point itsLowerRight;
50: int itsTop;
51: int itsLeft;
52: int itsBottom;
53: int itsRight;
54: };
55: // Ende von Rect.hpp
1: // Beginn von rect.cpp
2: #include "rect.hpp"
3: Rectangle::Rectangle(int top, int left, int bottom, int right)
4: {
5: itsTop = top;
6: itsLeft = left;
7: itsBottom = bottom;
8: itsRight = right;
9:
10: itsUpperLeft.SetX(left);
11: itsUpperLeft.SetY(top);
12:
13: itsUpperRight.SetX(right);
14: itsUpperRight.SetY(top);
15:
16: itsLowerLeft.SetX(left);
17: itsLowerLeft.SetY(bottom);
18:
19: itsLowerRight.SetX(right);
20: itsLowerRight.SetY(bottom);
21: }
22:
23:
24: // Rechteckflaeche berechnen. Dazu Ecken bestimmen,
25: // Breite und Hoehe ermitteln, dann multiplizieren.
26: int Rectangle::GetArea() const
27: {
28: int Width = itsRight-itsLeft;
29: int Height = itsTop - itsBottom;
30: return (Width * Height);
31: }
32:
33: int main()
34: {
35: // Eine lokale Rectangle-Variable initialisieren
36: Rectangle MyRectangle (100, 20, 50, 80 );
37:
38: int Area = MyRectangle.GetArea();
39:
40: cout << "Flaeche: " << Area << "\n";
41: cout << "Obere linke X-Koordinate: ";
42: cout << MyRectangle.GetUpperLeft().GetX();
43: return 0;
44: }
Flaeche: 3000
Obere linke X-Koordinate: 20
Die Zeilen 3 bis 14 in Listing 6.8 deklarieren die Klasse Point
, die der Aufbewahrung
einer bestimmten X,Y-Koordinate eines Graphen dient. In der hier gezeigten Form
greift das Programm kaum auf Point
zurück. Allerdings ist Point
auch für andere Zeichenmethoden
vorgesehen.
Die Klassendeklaration von Point
deklariert in den Zeilen 12 und 13 zwei Elementvariablen
- itsX
und itsY
. Diese Variablen nehmen die Werte der Koordinaten auf. Bei
Vergrößerung der X-Koordinate bewegt man sich im Koordinatensystem nach rechts.
Mit wachsender Y-Koordinate bewegt man sich im Koordinatensystem nach oben.
(Manche Systeme, beispielsweise Windows, verwenden ein Koordinatensystem, bei
dem man sich mit steigender Y-Koordinate im Fenster weiter nach unten bewegt.)
Die Klasse Point
verwendet Inline-Zugriffsfunktionen, um die in den Zeilen 7 bis 10
deklarierten Punkte X
und Y
zu lesen und zu setzen. Die Klasse Point
übernimmt Standardkonstruktor
und Standarddestruktor vom Compiler. Daher muß man die Koordinaten
der Punkte explizit festlegen.
In Zeile 17 beginnt die Deklaration der Klasse Rectangle
. Ein Rectangle
-Objekt besteht
aus vier Punkten, die die Ecken des Rechtecks darstellen.
Der Konstruktor für die Klasse Rectangle
(Zeile 20) übernimmt vier Ganzzahlen, die
als top
(oben), left
(links), bottom
(unten) und right
(rechts) bezeichnet sind. Die Klasse
Rectangle
kopiert die an den Konstruktor übergebenen Parameter in vier Elementvariablen
(Listing 6.9) und richtet dann vier Point
-Objekte ein.
Außer den üblichen Zugriffsfunktionen verfügt Rectangle
über eine Funktion namens
GetArea()
, die in Zeile 43 deklariert ist. Die Klasse speichert die Fläche nicht in einer
Variablen, sondern läßt sie berechnen. Diese Aufgabe übernimmt die Funktion GetArea()
in den Zeilen 28 und 29 von Listing 6.9. Die Funktion ermittelt zunächst Breite
und Höhe des Rechtecks und multipliziert dann beide Werte.
Um die X-Koordinate der oberen linken Ecke des Rechtecks zu ermitteln, muß man
auf den Punkt UpperLeft
zugreifen und von diesem Punkt den Wert für X
abrufen. GetUpperLeft()
ist eine Methode von Rectangle
und kann somit direkt auf die privaten
Daten von Rectangle
(einschließlich itsUpperLeft
) zugreifen. Da itsUpperLeft
ein
Point
-Objekt ist und der Wert itsX
von Point
als privat deklariert ist, stehen GetUpperLeft()
diese Daten nicht direkt zur Verfügung. Statt dessen muß man die öffentliche
Zugriffsfunktion GetX()
verwenden, um diesen Wert zu erhalten.
In Zeile 33 von Listing 6.9 beginnt der Rumpf des eigentlichen Programms. Bis Zeile
35 ist praktisch noch nichts passiert. Es hat nicht einmal eine Reservierung von Speicher
stattgefunden. Man hat dem Compiler bislang lediglich mitgeteilt, wie ein Point
-
Objekt und ein Rectangle
-Objekt zu erzeugen sind, falls man diese Objekte irgendwann
benötigt.
Zeile 36 definiert ein Rectangle
-Objekt durch Übergabe der Werte für top
, left
, bottom
und right
.
Zeile 38 deklariert die lokale Variable Area
vom Typ int
. Diese Variable nimmt die
Fläche des erzeugten Rectangle
-Objekts auf. Area
wird mit dem von der Funktion GetArea()
des Rectangle
-Objekts zurückgegebenen Wert initialisiert.
Ein Klient von Rectangle
kann ein Rectangle
-Objekt erzeugen und dessen Fläche berechnen,
ohne die Implementierung von GetArea()
zu kennen.
RECT.HPP
steht in Listing 6.8. Allein aus der Header-Datei mit der Deklaration der
Klasse Rectangle
kann der Programmierer ersehen, daß GetArea()
einen int
-Wert zurückgibt.
Wie GetArea()
diese magische Berechnung ausführt, ist für den Benutzer der
Klasse Rectangle
nicht von Belang. Im Prinzip könnte der Autor von Rectangle
die
Funktion GetArea()
ändern, ohne die Programme zu beeinflussen, die auf die Klasse
Rectangle
zugreifen.
Was ist der Unterschied zwischen Deklarieren und Definieren?
Antwort: Eine Deklaration führt einen Namen für etwas ein, aber weist keinen Speicher zu. Eine Definition hingegen weist Speicher zu.
Bis auf wenige Ausnahmen sind alle Deklarationen auch Definitionen. Die vielleicht wichtigsten Ausnahmen sind die Deklaration einer globalen Funktion (ein Prototyp) und die Deklaration einer Klasse (in der Regel in einer Header-Datei).
Ein sehr enger Verwandter des class
-Schlüsselwortes ist das Schlüsselwort struct
. Es
wird eingesetzt, um eine Struktur zu deklarieren. In C++ ist eine Struktur das gleiche
wie eine Klasse, abgesehen davon, daß ihre Elemente standardmäßig öffentlich (public
) sind. Sie können eine Struktur genauso wie eine Klasse deklarieren, und Sie
können sie mit den gleichen Datenelementen und Funktionen ausstatten. Wenn Sie
sich dabei noch an die empfohlene Programmierpraxis halten und die privaten und öffentlichen
Abschnitte Ihrer Klasse immer explizit ausweisen, gibt es eigentlich überhaupt
keinen Unterschied.
Geben Sie das Listing 6.8 mit folgenden Änderungen noch einmal neu ein:
class Point
in struct Point
.
class Rectangle
in struct Rectangle
.
Starten Sie dann das Programm und vergleichen Sie die Ausgaben. Es sollte sich nichts daran geändert haben.
Wahrscheinlich fragen Sie sich, warum es zwei Schlüsselwörter gibt, die das gleiche bewirken. Das ist eher dem Zufall zu verdanken. Als C++ entwickelt wurde, war es als Erweiterung von C konzipiert. Und C baut auf Strukturen auf, wenn auch C-Strukturen keine Klassenmethoden haben. Bjarne Stroustrup, der Begründer von C++, verwendete Strukturen, änderte jedoch später den Namen in Klasse, um die neue erweiterte Funktionalität besser auszudrücken.
Heute haben Sie gelernt, wie man neue Datentypen - Klassen - erzeugt und wie man Variablen dieser neuen Typen - sogenannte Objekte - definiert.
Eine Klasse verfügt über Datenelemente, die Variablen verschiedener Typen einschließlich anderer Klassen sein können. Zu einer Klasse gehören auch Elementfunktionen, die sogenannten Methoden. Man verwendet diese Elementfunktionen, um die Datenelemente zu manipulieren und andere Aufgaben auszuführen.
Klassenelemente - sowohl Daten als auch Funktionen - können öffentlich (public
)
oder privat (private
) sein. Öffentliche Elemente sind jedem Teil des Programms zugänglich.
Auf private Elemente können nur die Elementfunktionen der Klasse zugreifen.
Es gehört zum guten Programmierstil, die Schnittstelle - oder Deklaration - der Klasse
in einer separaten Datei unterzubringen. Gewöhnlich schreibt man die Schnittstelle in
eine Datei mit der Erweiterung .HPP
. Die Implementierung der Klassenmethoden
bringt man in einer Datei mit der Erweiterung .CPP
unter.
Klassenkonstruktoren initialisieren Objekte. Klassendestruktoren zerstören Objekte und werden oft genutzt, um Speicherplatz, der von den Methoden dieser Klasse zugewiesen wurde, wieder freizugeben.
Frage:
Wie groß ist ein Klassenobjekt?
Antwort:
Die Größe eines Klassenobjekts im Speicher wird durch die Summe aller Größen
seiner Elementvariablen bestimmt. Klassenmethoden belegen keinen
Platz in dem für das Objekt reservierten Hauptspeicher.
Frage:
Wenn ich eine Cat
-Klasse mit einem privaten Element itsAge
deklariere
und dann zwei Cat
-Objekte, Frisky
und Boots
, definiere, kann Boots
dann
auf die Elementvariable itsAge
von Frisky
zugreifen?
Antwort:
Ja. Private Daten stehen den Elementfunktionen einer Klasse zur Verfügung,
und die verschiedenen Instanzen der Klasse können auf ihre jeweiligen Daten
zugreifen. In anderen Worten: Wenn Frisky
und Boots
beides Instanzen von
Cat
sind, können die Elementfunktionen von Frisky
sowohl auf die Daten von
Frisky
als auf die von Boots
zugreifen.
Frage:
Warum sollten Datenelemente nach Möglichkeit privat bleiben?
Antwort:
Indem man die Datenelemente privat macht, kann der Klient der Klasse die
Daten verwenden, ohne sich um die Art und Weise ihrer Speicherung oder
Verarbeitung kümmern zu müssen. Wenn zum Beispiel die Klasse Cat
eine
Methode GetAge()
enthält, können die Klienten der Klasse Cat
das Alter der
Katze abrufen, ohne zu wissen oder sich darum zu kümmern, ob die Katze ihr
Alter in einer Elementvariablen speichert oder es bei Bedarf berechnet.
Frage:
Wenn ich mit einer konstanten Funktion eine Klasse ändere und dadurch
einen Compiler-Fehler hervorrufe, warum sollte ich dann nicht
einfach das Schlüsselwort const
weglassen und Compiler-Fehlern aus
dem Wege gehen?
Antwort:
Wenn Ihre Elementfunktion aufgrund der Logik die Klasse nicht ändern sollte,
stellt die Verwendung des Schlüsselwortes const
eine gute Möglichkeit dar,
den Compiler beim Aufspüren verzwickter Fehler hinzuzuziehen. Wenn zum
Beispiel GetAge()
keinen Grund hat, die Klasse Cat
zu ändern, aber Ihre Implementierung
die Zeile
if (itsAge = 100) cout << "Hey! Du bist 100 Jahre alt\n";
enthält, führt die Deklaration von
GetAge()
alsconst
zu einem Compiler-Fehler. Hier war beabsichtigt, einen Test durchzuführen, obitsAge
gleich 100 ist. Statt dessen haben Sie versehentlich den Wert 100 anitsAge
zugewiesen. Diese Zuweisung ändert die Klasse. Da Sie aber explizit keine Änderungen zugelassen haben, kann der Compiler den Fehler ermitteln.
Derartige Fehler lassen sich nur schwer aufspüren, wenn man den Code lediglich durchsucht - das Auge sieht oft nur das, was man sehen will. Viel wichtiger ist noch, daß das Programm korrekt zu laufen scheint, obwohl
itsAge
nun auf eine falsche Zahl gesetzt wurde. Früher oder später treten dann Probleme auf.
Frage:
Gibt es überhaupt einen Grund, in einem C++-Programm eine Struktur
zu verwenden?
Antwort:
Viele C++-Programmierer reservieren das Schlüsselwort struct
für Klassen
ohne Funktionen. Das ist ein Rückfall in die alten C-Strukturen, die keine
Funktionen haben konnten. Offen gesagt, sind Strukturen nur verwirrend
und gehören nicht zum guten Programmierstil. Die heutige Struktur ohne Methoden
braucht morgen vielleicht eine Methode. Dann ist man gezwungen,
entweder den Typ in class
zu ändern oder die Regel zu durchbrechen und
schließlich eine Struktur mit Methoden zu erzeugen.
Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie die Lösungen in Anhang D lesen und zur Lektion des nächsten Tages übergehen.
public
) und privaten (private
) Datenelementen?
Cat
-Objekte deklarieren, können diese unterschiedliche Werte in ihren itsAge
-Datenelementen haben?
Cat
-Funktion Meow()
aussehen, die keine Parameter übernimmt und void
zurückliefert?
Employee
(Angestellter) mit folgenden Datenelementen deklariert: age
, YearsOfService
und Salary
Employee
neu, mit privaten Datenelementen und zusätzlichen öffentlichen Zugriffsmethoden, um jedes der Datenelemente zu lesen und zu setzen.
Employee
-Klasse ein Programm, das zwei Angestellte erzeugt. Setzen Sie deren Alter (age
), Beschäftigungszeitraum (YearsOfService
) und Gehalt (Salary
) und geben Sie diese Werte aus.
Employee
auf, die berichtet, wieviel tausend Dollar der Angestellte verdient, aufgerundet auf die nächsten 1000 Dollar.
Employee
so, daß Sie age
, YearsOfService
und Salary
initialisieren können, wenn Sie einen neuen Angestellten anlegen.
class Square
{
public:
int Side;
}
class Cat
{
int GetAge()const;
private:
int itsAge;
};
class TV
{
public:
void SetStation(int Station);
int GetStation() const;
private:
int itsStation;
};
int main()
{
TV myTV;
myTV.itsStation = 9;
TV.SetStation(10);
TV myOtherTv(2);
return 0;
}
© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH