2.1 Was ist Objektorientierung?
Schon früh in der Entwicklung der höheren Programmiersprachen hat man erkannt, dass das zusammenbauen von Programmen aus einzelnen Funktionen mit zunehmender Projektgröße immer unübersichtlicher wird. Es entstand der Wunsch, mehr Struktur in die Funktionssammlungen zu bringen. Folgende Überlegungen verdeutlichen anschaulich, was Objekte im abstrakten Sinn bedeuten:
Betrachten wir einmal eine Kaffeemaschine: Zunäst fallen uns einige Eigenschaften direkt auf. Es erscheint trivial, aber dennoch achten wir als erstes auf Farbe und Form des Gerätes. Auf der technischen Seite fällt zum Beispiel die Füllmenge auf. Sicher werden wir noch einige andere Eigenschaften finden. Was uns nicht direkt auffällt, sind die Funktionen, die wir wie selbstverständlich mit der Maschine nutzen: Wir bewegen sie über die Tischplatte (TRIVIAL!!), und schalten sie ein. Es liesen sich weitere finden, es soll uns jedoch genügen.
Was hat die Kaffeemaschine jetzt mit Programmieren zu tun? Ganz einfach: Objektorientiertes Programmieren soll es uns ersparen, unsere \"Kaffeemaschine\" erst aus einzelnen Funktionen und Variablen zusammenbauen zu müssen. Stattdessen nehmen wir ein einmal vorher definiertes Objekt (eine zusammengehörende Struktur, welche Funktionen und Variablen unter einem Mantel vereint) und binden dies in unser Programm ein.
Ich will gleich mit der Bezeichnung reinen Tisch machen: Als Klasse bezeichnen wir die reine Struktur, welche unseren Automaten formal beschreibt. Von einem Objekt reden wir dann, wenn wir in unserem Programm Speicher belegen, der so strukturiert wird, wie es die Klasse beschreibt. Einfach gesagt: Wir legen einen Speicherplatz an und nennen das Objekt (statt Variable) und dieses Objekt ist durch die Klasse definiert (statt durch den Datentyp bei Variablen).
2.2 Definition einer Klasse
Ich will im Folgenden auf ein Modell zur Beschreibung einer Autofirma, speziell deren Fuhrpark, eingehen. Für den Außenstehenden hat der Händler eben einen Fuhrpark auf dem Parkplatz stehen. Wer sich daführ interessiert, wirft aber eher einen Blick auf die einzelnen Wagen. Man interessiert sich zum Beispiel für Farbe und Typ, Kilometerstand und Baujahr. Für den Händler besteht der Fuhrpark aus einer Liste verschiedener Autos, die alle mit gleichen Eigenschaften beschreibbar sind. Er wird sich also ein Modell zurecht legen, mit dem er so einfach wie Möglich alle Autos in einer Datenbank erfassen kann. Dazu definiert er zunächst eine Klasse, die ein einzelnes Auto durch die Eigenschaften Typ und Baujahr beschreibt. Weil er bereits im Tutorial über ANSI-C etwas über dynamische Listen erfahren hat, führt er einen Zeiger ein, der auf andere Objekte der selben Klasse verweisen kann. Damit möchte er später durch seinen Fuhrpark navigieren können:
class Auto
{
public:
void SetNext( Auto* zeiger );
Auto* GetNext( void );
int Baujahr;
private:
Auto *next; // Zeiger auf das nächste Auto
};
Der Zeiger next ist als privat definiert, das heißt, nur Funktionen, die auch in der Klasse Auto deklariert werden (zu der Klasse gehören), dürfen lesend oder schreibend auf den Zeiger zugreifen. Auf alle anderen Klassenmitglieder (Variablen und Funktionen) kann auch von externen Funktionen aus zugegriffen werden. Um *next trotzdem manipulieren zu können, werden die Funktionen SetNext und GetNext definiert, deren Bedeutung aus dem Namen hervorgehen sollte.
Zugegebenermaßen ist diese Klasse noch etwas mager, aber sie zeigt alles, was bis hierher nötig ist. Damit wäre dann auch die Struktur vorgegeben, die unsere Klasse Auto beschreibt. Sehen wir uns jetzt noch an, wie wir der Klasse erkläern, was zu tun ist, wenn die verschiedenen Funktionen (wir sprechen auch von Member-Funktionen im Gegensatz zu Member-Variablen) aufgerufen werden:
void Auto::SetNext( Auto *zeiger ) { next = zeiger; }
Auto* Auto::GetNext( void ) { return next; }
Wichtig ist, dass wir dem Compiler sagen, welcher Klasse die Funktion angehört, die wir gerade beschreiben. Dazu dient der Scope-Oerator :: , der vorne den Namen der Klasse und dannach den Namen der Funktion erwartet.
2.3 Tipps zur Namensgebung
Später werden wir dazu übergehen, größere Projekte zu Programmieren. Dabei verliert man leicht den Überblick über die Vielzahl von Variablen und Funktionen. Deshalb ist es wichtig, sich früh eine Standartisierung der Variablennamen anzugewöhnen. Dabei ist es (nach meiner persönlichen Meinung) weniger wichtig, Standarts einzuhalten, als sich selbst in einem Programm zurecht zu finden. Zumindest, solange man alleine an dem Projekt arbeitet. Naja, bei Gruppenarbeiten sollte man einen gemeinsamen Nenner finden. Ich möchte hier einige Vorschläge unterbreiten, wie man Bezeichner für Varablen bauen kann:
Zunächst ist es wichtig, das man Variablen möglichst aussagekräftig sind. Es gilt wieder: So lang wie nötig, aber so kurz wie möglich! In der obigen Klasse könnte die Variable Baujahr auch BJahr oder BaujahrDesWagens oder ähnlich heißen. Letzteres möchte ich aber nicht allzu oft in einen Quellcode eingeben müssen. Ersteres kann man gut aus dem Kontext der Klasse heraus verstehen. Für eine Zählervariable zum Beispiel ist count die bessere Wahl als nur co. Wählt Variablennamen am besten so, dass auch andere Programmierer den Sinn aus dem Namen ablesen können. Notfalls immer einen Kommentar hinter den Variablennamen schreiben, und zwar hinter die Deklaration der Variablen!
Präfix-Technik
Toll, bis dahin war ja noch alles logisch, und die meisten hätten es sowieso danach gehandelt. Es hat sich jedoch auch als günstig erwiesen, vor wichtige Variablen ein Präfix zu setzten, das Aussagen über den Typ der Variablen macht. So könnte zum Beispiel eine Ganzzahl mit i (für integer) beginnen (also iCount) oder ein Zeiger mit p (für Pointer). Nehmt einfach den ersten Buchstaben des Typs als Präfix. Schreibt das Präfix klein, den ersten Buchstaben der Variablen groß. Bei Dateien ist es üblich, das Präfix h (für Handle) zu verwenden. h nimmt man auch für Resourcen oder Threads, dazu aber erst in höheren Semestern.
Für Klassen hat es sich eingebürgert, ein GROßES(!) C zu schreiben (zum Beispiel CAuto). Den ersten wirklichen Buchstaben des Namens schreibt man aber nach wie vor groß. ACHTUNG: Wer später mit der Microsoft Foundation Class (MFC) arbeitet, muss aufpassen, dass er nicht mit den MFC-Klassennahmen kollidiert oder entsprechende Maßnahmen ergreifen, um bei dopperter Namensvergabe eindeutig zu bleiben.
Mitgliedsvariablen von Klassen (im Folgenden Member-Variablen oder Eigenschaften der Klasse genannt) bekommen ein weiteres Präfix: m_ (zum Beispiel m_pMainWnd). Dies sagt einfach nochmal aus, dass es sich hier um Member-Variablen einer Klasse handelt.
2.4 Konstante Elementfunktionen und Inline-Definition
Es gibt Situationen, in dennen eine Funktion einer Klasse keine Eigenschaften verändern muss, zum Beispiel weil sie nur den Wert einer Eigenschaft zurückgibt. Solch Funktionen bezeichnen wir als Konstant. Dies machen wir dem Compiler klar, indem wir nach dem Namen und den Funktionsklammern das Schlüsselwort const plazieren:
class CAuto
{
public:
int GetAnzahl() const;
double GetWert() const { return m_dWert; }
private:
int m_iAnzahl;
double m_dWert;
};
int CAuto::GetAnzahl() const
{ return m_iAnzahl; }
Ich habe bereits die Regeln zur Namensvergabe umgesetzt. Die zweite Funktion ist eine Inline-Funktion (siehe dazu auch Kapitel 1.7). Diese Technik erspart uns ein späteres Definieren wie bei GetAnzahl(). Ich habe das void in den Funktionsklammern weggelassen, was durchaus erlaubt ist in C++. Nur die Klammern darf man nicht weglassen.
2.5 Konstruktor und Destruktor
Jede Klasse kennt zwei Ereignisse. Das Initialisieren der Klasse (zum Zeitpunkt der Deklaration des Objektes) und das Verlassen des Gültigkeitsbereichen der Klasse. Im ersten Fall wird der Konstruktor der Klasse aufgerufen, im zweiten der Destruktor. Beide müssen natürlich in der Klasse deklariert sein. Dabei tragen beide den Namen der Klasse, wobei dem Destruktor eine Tilde ( ~ ) vorangestellt wird. Beide haben keinen Rückgabetyp, auch nicht void! Der Konstuktor kann überladen werden und Parameter annehmen (auch Standartparameter). Erhält er keine Parameter, spricht man von einem Standartkonstruktor. Dem Destruktor kann nichts übergeben werden - mann kann ihn deshalb auch nicht überladen. Ein Beispiel soll das alles verdeutlichen. Ich habe es als Textfile hinterlegt.
Siehe Beispiel: g2c2p1.txt
Ich nutze hier einen eigentlich unsauberen Seiteneffekt: Die Variable m_iAnzahl wird zu keiner Zeit mit einem Wert belegt, deshalb enthält sie zum Zeitpunkt der Ausgabe einen Zufallswert, den ich nutze, um die Klasse in Konstruktor und Destruktor eindeutig zu identifizieren. Die Ausgabe auf dem Bildschirm macht so deutlich, wann welcher Konstruktor oder Destruktor aufgerufen wird.
2.5.1 Der Kopierkonstruktor
Eine besondere Form des Konstruktors ist der Kopierkonstruktor. Er wird aufgerufen, wenn man direkt beim initialisieren eines Objektes diese neue Instanz der Klasse mit Werten aus einer bereits vorhandenen Instanz füllen möchte, also ein Objekt in ein anderes Objekt gleichen Typs kopieren will. Der Kopierkonstruktor ist wie der Standartkonstruktor ohne Rückgabetyp. Er hat als einzigen Parameter einen Referenzparameter vom Typ der Klasse. Um den erzeugten Quellcode zu verbessern und der Sauberkeit des Programmierens wegen definiert man den Parameter als konstant, so das die Funktion am Ende wie folgt aussieht:
KLASSE::KLASSE( const KLASSE& quelle );
Dabei steht KLASSE für den Namen der Klasse und quelle wird im folgenden eben die Quellklasse, aus der die Werte herausgenommen werden sollen.
2.6 Statische Elementvariablen
Statische Elementvariablen sind nicht an eine Instanz einer Klasse (ein Objekt) gebunden, sondern werden nur einmal im Speicher angelegt. Alle Instanzen können dann gemeinsam auf die gleiche Variable zugreifen und so zum Beispiel eine Zählvariable beeinflussen, welche die Anzahl der Instanzen einer Klasse mitzählt. Hier wieder ein einfaches Beispiel:
Siehe Beispiel: g2c2p2.txt
2.7 Der friend-Operator
Wenn wir auf Elemente von Klassen zugreifen wollen, dann müssen wir stets darauf achten, ob uns das erlaubt ist (public-Elemente) oder eben verboten (private-Elemente). Dies bereitet zum Beispiel Probleme bei der Erzeugung verketteter Listen. Diese müssen ja über Zeiger untereinander kommunizieren. Diese Zeiger deklariert man gerne als privat. Leider muss man dann allerdings wieder Funktionen zum Umbiegen der Zeiger bereitstellen.
Der friend-Operator erlaubt es einer Klasse, eine zweite Klasse als befreundet zu erklären. Die befreundete Klasse kann dann auf private Elemente der erklärenden Klasse zugreifen.
Ebenso kann eine Klasse globale Funktionen oder Member-Funktionen einer anderen Klasse als friend deklarieren. Dann kann eben nur die angegebenen Funktion auf die Klassenelemnte zugreifen. Folgender Code zeigt, wie man den friend-Operator benutzt:
void globaleFunktion( Klasse2 ref );
class Klasse1
{
public:
void SetzteWert( Klasse2& ref );
}
class Klasse2
{
private:
double Wert;
friend Klasse1;
friend globaleFunktion;
friend Klasse1::SetzteWert( Klasse2& ref ); // Hier Redundant, weil Klasse1 komplett friend ist
}
void Klasse1::SetzteWert( Klasse2& ref )
{ ref.Wert = 100.90; // Erlaubt, wegen Friend }
void globaleFunktion( Klasse2 ref )
{ cout |