Allgemeiner Aufbau einer Klasse

Einführung

Klassen stellen im Prinzip eine Erweiterung von Strukturen dar. In der Erweiterung inbegriffen ist unter anderem die Möglichkeit der Vererbung oder etwa die Möglichkeit den Zugriff auf Membervariablen mit Hilfe von Zugriffsbezeichnern und Memberfunktionen zu steuern.

Deklaration

Ihre Deklaration unterscheidet sich zunächst nur durch das Schlüsselwort 'class':

class <classname> 
{ 
	<Datatype> <Data>; 
	<Datatype> <Data>; 	
	... 
}; 

So lässt sich das Strukturbeispiel aus dem letzten Kapitel zunächst 1 : 1 übertragen:

class RECTANGLE 
{ 
	unsigned long ulWidth; 
	unsigned long ulHeight; 	
};

Gemeinsamkeiten und Unterschiede 

Genauso wie bei Strukturen lässt sich nun 'RECTANGLE' als eigener Datentyp verwenden, mit denen sich Variablen deklarieren lassen:

//Deklaration einer RECTANGLE-Instanz
RECTANGLE rect;

Möchte man jedoch die Werte der Membervariablen ändern so erhält man einen Kompilerfehler. Folgender Aufruf ist offenbar nicht möglich:

//Deklaration einer RECTANGLE-Instanz
RECTANGLE rect;

//Ändern der Breite
rect.ulWidth = 600;	//Fehler C2248: kein Zugriff auf privates Element

Bei Klassen sind standardmäßig alle Membervariablen für äußere Zugriffe geschützt, d.h. die Werte lassen sich standardmäßig nicht ändern. Bei Strukturen hingegen sind alle Membervariablen hingegen "öffentlich". (Tatsächlich ist dies in C++ der einzige Unterschied zwischen dem Schlüsselwort 'struct' und dem Schlüsselwort 'class'.) Nun lassen sich jedoch Zugriffsbezeichner deklarieren, welche explizit angeben, ob eine Membervariable "öffentlich" oder "privat" ist. Dazu dienen die Zugriffsbezeichner 'public' und 'private'. Ändert man die Klassendeklaration wie folgt ab, dann ist das Ändern von Membervariablen wieder möglich:

class RECTANGLE 
{
public:
	unsigned long ulWidth; 		//nun öffentlich
	unsigned long ulHeight; 	//nun öffentlich
};

Obiger Quellcode lässt sich nun fehlerfrei kompilieren. Wird ein Zugriffsbezeichner angegeben, dann gilt er für alle (anders als in anderen Sprachen, wie C# und Java) weiteren Member, bis ein neuer Zugriffsbezeichner angegeben wird:

class AClass 
{
	int var1;	//standardmäßig privat
	long var2;	//standardmäßig privat
public:
	int var3; 	//öffentlich
	long var4; 	//öffentlich
private:
	int var5;	//privat
	long var6;	//privat
public:
	int var7;	//öffentlich

};

Bei dieser Klasse lässt sich von außen nur auf die Membervariablen 'var3' und 'var4' und 'var7' zugreifen. Alle weiteren sind 'private' und somit geschützt.

Memberfunktionen

Es lassen sich nun Membervariablen mit Hilfe von Zugriffsbeichnern schützen. Wie jedoch ist es dann möglich auf die Werte weiterhin zuzugreifen, denn wenn dies nicht möglich wäre, dann wäre eine Membervariable, die geschützt ist, augenscheinlich einfach sinnlos. 

Es lassen sich in Klassen neben Membervariablen auch Memberfunktionen deklarieren. Diese Funktionen werden genauso deklariert, wie die "globalen" Funktionen. (siehe Kapitel Funktionen). Allerdings werden sie innerhalb der Klasse deklariert (und optional auch dort implementiert). Innerhalb eines solchen Funktionsrumpfes sind alle geschützten Membervariablen (und auch -funktionen) zugänglich. 

Eine Klasse mit Memberfunktionen sieht im allgemeinen wie folgt aus: 

class <classname> 
{
public: 
	<Rückgabetyp> <Funktionsname>(<Argumentliste>)
	{
		//Wert bestimmen und zurückgeben
	}
	...
	
}

Die Methode wäre nun öffentlich, doch das ließe sich durch ein Anpassen des Zugriffsbezeichners leicht ändern. Es besteht auch die Möglichkeit die Implementierung außerhalb der Klassendeklaration durchzuführen:

class <classname> 
{
public: 
	<Rückgabetyp> <Funktionsname>(<Argumentliste>);
	...
	
}

//Implementierung (auch innerhalb einer anderen Datei möglich)
<Rückgabetyp> <classname>::<Funktionsname>(<Argumentliste>)
{
	//Wert bestimmen und zurückgeben
}

In diesem Fall wird durch den Operator '::', dem der Klassenname vorangestellt ist, erreicht, dass die Funktion als Memberfunktion der entsprechenden Klasse implementiert wird. Allerdings muss die Funktion weiterhin innerhalb der Klasse deklariert werden. 

Nun greifen wir noch einmal das 'RECTANGLE'-Beispiel auf. Nur diesmal sollen vom "Benutzer" nur Werte zwischen '0' und '1000' eingegeben werden können. Zu diesem Zweck müssen Höhe und Breite geschützte Membervariablen sein und der Zugriff nur über jeweils zwei Methoden möglich sein. In der Übertragung könnte dies in etwa so aussehen:

class RECTANGLE
{
private:
	unsigned long m_ulWidth, m_ulHeight;
public:
	//Setzen der Höhe
	void SetHeight(unsigned long ulHeight)
	{
		//Höhe darf höchstens den Wert 1000 annehmen
		if (ulHeight > 1000) m_ulHeight = 1000;
		else m_ulHeight = ulHeight;
	}
	
	//Ermitteln der Höhe
	unsigned long GetHeight(void)
	{
		return m_ulHeight;
	}

	//Setzen der Breite
	void SetWidth(unsigned long);
	
	//Ermitteln der Breite
	unsigned long GetWidth(void);
	
	
};

//Implementierung für 'SetWidth'
void RECTANGLE::SetWidth(unsigned long ulWidth)
{
	//Breite darf höchstens den Wert 1000 annehmen
	if (ulWidth > 1000) m_ulWidth = 1000;
	else m_ulWidth = ulWidth;
	
}

//Implementierung für 'GetWidth'
unsigned long RECTANGLE::GetWidth(void)
{
	return m_ulWidth;
}

Um beiden Möglichkeiten gerecht zu werden, sind zwei Methoden innerhalb der Klasse und zwei Methoden außerhalb der Klasse implementiert worden. 

Hier nun ein Beispiel, in dem die Klasen zur Anwendung kommt:

void main(void)
{
	RECTANGLE r;

	r.SetHeight(500);
	r.SetWidth(1001);

	cout << "Breite: " << r.GetWidth() << endl;	//Ausgabe: "Breite: 1000"
	cout << "Hoehe: " << r.GetHeight() << endl;	//Ausgabe: "Hoehe: 500"
}

Wie zu erkennen, lassen sich auf die Methoden genauso aufrufen, wie herkömmliche Methoden. Da sie allerdings Memberfunktionen sind, muss zunächst die Instanz der Klasse angegeben werden, für die die Methode ausgeführt werden soll. Dies geschieht genauso wie der Zugriff auf die Variablen mit Hilfe des '.'-Operators. 

Konstruktor

Nach der Deklaration einer Klasseninstanz, sind die Membervariablen allesamt nicht initialisiert. Man kann zu diesem Zweck sogenannte Konstruktoren implementieren. Dabei handelt es sich um Membefunktionen, die genauso wie die Klasse heißen, keinen Rückgabetyp haben und automatisch aufgerufen werden, wenn eine Instanz einer Klasse gebildet wird. Genau wie alle anderen Funktionen  (ob global oder ob Member einer Klasse) lassen sich auch Konstruktoren überladen. Ein Konstruktor, der keine Argumentliste enthält, wird als Standardkonstruktor bezeichnet. Soll nun ein sogenannter Standardkonstruktor für die Klasse 'RECTANGLE' deklariert und implementiert werden, dann könnte dies wie folgt aussehen:

class RECTANGLE
{
private:
	unsigned long m_ulWidth, m_ulHeight;
public:
	//Standardkonstruktor
	RECTANGLE(void);

	//Setzen der Höhe
	void SetHeight(unsigned long ulHeight);
	
	//Ermitteln der Höhe
	unsigned long GetHeight(void);

	//Setzen der Breite
	void SetWidth(unsigned long);
	
	//Ermitteln der Breite
	unsigned long GetWidth(void);
	
	
};

//Implementierung des Standardkonstruktors
RECTANGLE::RECTANGLE(void)
{
	//Member initialisieren
	m_ulWidth = m_ulHeight = 0;
}

//Implementierung der anderen Memberfunktionen...

Wird nun eine Instanz der Klasse 'RECTANGLE' gebildet, dann werden die Member 'm_ulWidth' und 'm_ulHeight' jeweils mit '0' initialisiert:

RECTANGLE r;	//Höhe und Breite sind nun mit 0 initialisiert

Wie bereits erwähnt lassen sich auch Konstruktoren überladen. So könnte man einen Konstruktor implementieren, bei dem man die Werte von Höhe und Breite gleich bei der Deklaration übergeben kann:

class RECTANGLE
{
private:
	unsigned long m_ulWidth, m_ulHeight;
public:
	//Standardkonstruktor
	RECTANGLE(void);

	//Konstruktor
	RECTANGLE(unsigned long, unsigned long);

	//Setzen der Höhe
	void SetHeight(unsigned long ulHeight);
	
	//Ermitteln der Höhe
	unsigned long GetHeight(void);

	//Setzen der Breite
	void SetWidth(unsigned long);
	
	//Ermitteln der Breite
	unsigned long GetWidth(void);
	
	
};

//Implementierung des Konstruktors
RECTANGLE::RECTANGLE(unsigned long ulWidth, unsigned long ulHeight)
{
	//Member initialisieren
	SetWidth(ulWidth);
	SetHeight(ulHeight);
}

//Implementierung des Standardkonstruktors und der anderen Memberfunktionen...

Nun ließen sich für den soeben implementierten Konstruktor natürlich auch Standardwerte implementieren, welche den Standardkonstruktor gänzlich überflüssig machen würde: 

class RECTANGLE
{
private:
	unsigned long m_ulWidth, m_ulHeight;
public:
	//Konstruktor
	RECTANGLE(unsigned long = 0, unsigned long = 0);

	//Setzen der Höhe
	void SetHeight(unsigned long ulHeight);
	
	//Ermitteln der Höhe
	unsigned long GetHeight(void);

	//Setzen der Breite
	void SetWidth(unsigned long);
	
	//Ermitteln der Breite
	unsigned long GetWidth(void);
	
	
};

//Implementierung der Memberfunktionen...

In beiden Fällen lassen sich Instanzen erstellen, bei denen entweder Höhe und Breite mit '0' oder mit den jeweiligen Werten initialisiert werden:

RECTANGLE r1;			//Höhe: 0, Breite: 0
RECTANGLE r2(600, 480);		//Höhe: 480, Breite: 600

Anhand dieser Deklarationen erkennt man, wie die entsprechenden Konstruktoren aufgerufen werden. Man übergibt die Werte, (die für den Konstruktor benötigt werden, ) indem man sie bei der Deklaration, wie bei einem Funktionsaufruf in Form einer Argumentliste, an den Variablennamen "anhängt". 

Zum Schluss sollte noch der Kopierkonstruktor vorgestellt werden. Dabei handelt es sich um einen Konstruktor, bei dem einfach eine Instanz des gleichen Typs via Referenz übergeben wird und die Daten dieser Referenz für die neu zu erstellende Referenz einfach übernommen werden. Im Beispiel der 'RECTANGLE'-Klasse sähe die Implementierung in etwa wie folgt aus:

class RECTANGLE
{
private:
	unsigned long m_ulWidth, m_ulHeight;
public:
	//Kopierkonstruktor	
	RECTANGLE(RECTANGLE& r);	//am besten const RECTANGLE& r

	//Konstruktor
	RECTANGLE(unsigned long = 0, unsigned long = 0);

	//Setzen der Höhe
	void SetHeight(unsigned long ulHeight);
	
	//Ermitteln der Höhe
	unsigned long GetHeight(void);

	//Setzen der Breite
	void SetWidth(unsigned long);
	
	//Ermitteln der Breite
	unsigned long GetWidth(void);
	
	
};

//Implementierung des Kopierkonstruktors
RECTANGLE::RECTANGLE(RECTANGLE& r)
{
	//Member übernehmen
	m_ulWidth = r.m_ulWidth;
	m_ulHeight = r.m_ulHeight;
}

//Implementierung der Memberfunktionen...

Der Kopierkonstruktur ermöglicht folgende Aufrufe:

RECTANGLE r1(600, 480);
RECTANGLE r2(r1);
RECTANGLE r3 = r1;

Bei Klassen, in denen nur Wertdatentypen vorkommen (also keine Zeiger), braucht der Kopierkonstruktor nicht eigens implementiert werden. Der automatisch implementierte Kopierkonstruktor macht im Prinzip nichts anderes, den Wert jeder Membervariablen zu kopieren. Da in dem eigenständig programmierten Kopierkonstruktor im obigen Beispiel auch nur die Werte für Höhe und Breite kopiert werden, ist diese Implementierung nicht nötig. Der Kopierkonstruktor sollte aber immer dann selbst implementiert werden, wenn die Klasse Zeiger enthält. Denn der automatisch implementierte  Kopierkonstruktor würde den Wert des Zeigers, (der ja eigentlich eine Adresse ist) auch nur kopieren. Somit existieren dann zwei Zeiger auf die gleichen Daten. Erklärt eine der beiden Instanzen die Daten dann für ungültig und setzt den Zeiger dann zurück, dann bekommt die zweite Instanz davon nichts mit und der Zeiger verweist auf einen ungültigen Bereich. In diesem Falle sollte also der Kopierkonstruktor in jedem Fall selbst implementiert werden.

Der Destruktor

Der Destruktor stellt das Gegenstück zu dem Konstruktor dar. Er wird immer dann aufgerufen, wenn die Instanz der Klasse nicht mehr gültig ist. Im Destruktor sollten alle notwendigen Aufräumarbeiten stattfinden. Angenommen eine Instanz hat während ihres Daseins Speicher reserviert und in einer Membervariablen (in Form eines Zeigers) Zugriff darauf, dann sollte der Speicher freigegeben werden, bevor die Instanz ungültig wird, da sonst der Speicher verloren geht und ein sogenanntes Speicherleck entsteht. Im Gegensatz zum Konstruktor gibt es für jede Klasse nur jeweils einen Destruktor. Genau wie jeder Konstruktor hat aber auch er keinen Rückgabetyp. Außerdem hat ein Destruktor keine Argumentliste. Der Name des Destruktors ergibt sich aus dem Klassennamen mit einem vorangestellten '~'

Es ließe sich folgendes Beispiel konstruieren, bei dem ein Destruktor (anders als bei der 'RECTANGLE'-Klasse) auch sinnvoll ist. Dazu folgender Quelltext:

class CSimpleArray
{
private:
	int *m_pData, m_Length;
public:
	//Konstruktor
	CSimpleArray(int Length = 0)
	{
		if (Length <= 0) m_Length = 1;
		else m_Length = Length;
		
		//Speicher reservieren
		m_pData = new int[m_Length];
	}
	//Destruktor
	~CSimpleArray(void)
	{
		if (m_pData) 
		{
			delete[] m_pData;
			m_pData = 0;
		}
	}
	
	//Wert setzen
	int& DataAt(int Index)
	{
		if (Index < 0 || Index >= m_Length) 
		{Index = 0;}
		
		return *(m_pData + Index); 
	}
};

Wird von dieser Klasse eine Instanz gebildet, dann wird Speicher reserviert. Verliert die Instanz ihre Gültigkeit, dann wird eben dieser Speicher wieder freigegeben. Zwischenzeitlich können die Daten mit 'DataAt' gelesen und verändert werden, in dem man den Wert der erhaltenen Referenz ausliest oder verändert.

Übrigens bietet sich hier durchaus eine eigene Implementierung des Kopierkonstruktors an, da hier Speicher reserviert und der Zugriff darauf durch einen Zeiger erreicht. Kopierkonstruktor sollte dann, anstatt einfach die Zeigervariable zu kopieren, neuer Speicher reserviert und die Daten kopiert werden. Die Implementierung dieses Konstruktors könnte dann in etwa so aussehen (Außerdem muss der Kopierkonstruktor noch in der Klasse deklariert werden):

//Implementierung des Kopierkonstruktors (memory.h sollte eingebunden werden)
CSimpleArray::CSimpleArray(const CSimpleArray& arr)
{
	//Arraylänge übernehmen
	m_Length = arr.m_Length;
	
	//neuen Speicher reservieren
	m_pData = new int[m_Length];
	
	//Speicher kopieren 
	memcpy(m_pData, arr.m_pData, m_Length * sizeof(int));
	
}

Wird eine Klasseninstanz auf dem Heap mit Hilfe des Operators 'new' erstellt, dann muss der Speicher durch den Aufruf von 'delete' bekanntlich ja wieder freigegeben werden. In diesem Fall wird genau dann der Destruktor aufgerufen. Wird die Klasseninstanz wie gewohnt auf dem Stack erstellt, dann wird der Destruktor aufgerufen, wenn die Instanz ihren Gültigkeitsbereich verliert, d.h. wenn sie beispielsweise innerhalb einer Funktion erstellt wird, dann verliert die Klassenistanz bei Funktionsende ihre Gültigkeit und der Destruktor wird aufgerufen.

Das Schlüsselwort 'friend'

Es existiert eine Möglichkeit die Einschränkungen für geschützte Member für ganz bestimmte Funktionen oder Klassen zu lockern. So könnte man einer Klasse oder Funktion beispielsweise erlauben die Werte für Höhe und Breite der 'RECTANGLE'-Klasse selbstständig zu ändern um z.B. einer Klasse oder Funktion zu erlauben auch Werte außerhalb des gültigen Bereichs einzutragen.

Dazu deklarieren wir zunächst die Klasse 'RECTANGLEX', die als Membervariable eine Instanz der Klasse 'RECTANGLE' enthält und den Zugriff auf die Höhe und Breite dieser Instanz durch zwei Methoden erlaubt, wobei beliebige Werte möglich sind:

class RECTANGLEX
{
private:
	RECTANGLE m_rect;
public:
	RECTANGLEX(unsigned long ulWidth = 0, unsigned long ulHeight = 0)
	{
		//Werte übernehmen
		m_rect.m_ulWidth = ulWidth;
		m_rect.m_ulHeight = ulHeight;
	}
	
	//Breite
	unsigned long& Width(void) {return m_rect.m_ulWidth;}
	
	//Höhe
	unsigned long& Height(void) {return m_rect.m_ulHeight;}
};

Außerdem  soll eine Funktion 'SetRect' implementiert werden, welche die Werte einer 'RECTANGLE'-Instanz via Referenz ändern kann:

void SetRect(RECTANGLE& r, unsigned long ulWidth, unsigned long ulHeight)
{
	r.m_ulWidth = ulWidth;
	r.m_ulHeight = ulHeight;
}

Bei der Kompilierung sollten jetzt noch Fehler auftauchen. Allerdings lassen sich diese mit Hilfe des Schlüsselwortes 'friend' beseitigen. Es lassen sich nämlich für Klassen 'friend'-Funktionen und 'friend'-Klassen deklarieren, die uneingeschränkten Zugriff auf alle Member (Funktionen und Variablen) besitzen. Um sowohl die Funktion 'SetRect' als auch die Klasse 'RECTANGLEX' als 'friend' der Klasse 'RECTANGLE' zu deklarieren muss man lediglich irgendwo innerhalb der Klassendeklaration von 'RECTANGLE' folgende Zeilen einfügen:

friend class RECTANGLEX;
friend void SetRect(RECTANGLE&, unsigned long, unsigned long);

Nun sollte sich alles wie gewohnt kompilieren lassen. Nun lassen sich über die Funktion 'SetRect', die Member von einer 'RECTANGLE'-Instanz trotz des Zugriffsbezeichners verändern.

Der 'this'-Zeiger

Eine jede Instanz einer Klasse besitzt den sogenannten 'this'-Zeiger. Dieser Zeiger ist ein Zeiger des Datentyps der jeweiligen Klasse und ist innerhalb dieser als der Bezeichner 'this' bekannt  Dieser Zeiger ist insofern besonders, als dass er auf die aktuelle Instanz verweist. Dazu ein kleines Beispiel zur Verdeutlichung:

class A
{
public:
	A* GetAddress(void) {return this;}	//'this'-Zeiger zurückgeben
};

#include <iostream.h>
void main(void)
{
	//Instanz der Klasse
	A a;
	
	cout << "Adresse von a: " << a.GetAddress() << endl;
	cout << "Adresse von a: " << &a << endl;
}

Beide Ausgaben dürften identisch sein (bei mir: "Adresse von a: 0x0065FDF4"). So lassen sich auch auf alle Member innerhalb der Klasse mit Hilfe des 'this'-Zeigers zugreifen:

class B
{
private:
	int m_b;
public:
	B(void)
	{
		this->m_b = 0;	//identisch mit m_b = 0
	}
};

In der Praxis kommt es u.a. vor, dass eine Klasse innerhalb einer Methode sich am Ende selbst zurückgibt. Ein Beispiel erscheint in der nächsten Lektion und dort sollte die Funktion des 'this'-Zeigers klar werden.

Für die nächsten Lektionen

Hier wurde der allgemeine Aufbau einer Klasse verdeutlicht. Dieser Aufbau wird in den folgenden Lektionen immer wieder aufgegriffen und vertieft. Die hier vermittelten Grundlagen (Memberfunktionen, Destruktor, Konstruktor, 'this'-Zeiger und Zugriffsbezeichner) bilden aber die Basis und sollten aus diesem Grunde nun zumindest im Groben bekannt sein. In der nächsten Lektion wird es um statische Member gehen.

Zurück Nach oben Weiter