C++-Programmierung: Klassen
Eine aktuelle Seite zum gleichen Thema ist unter C++-Programmierung/ Eigene Datentypen definieren verfügbar.
Die Klasse (class) ist die zentrale Datenstruktur in C++. Sie kapselt zusammengehörige Daten und Funktionen vom Rest des Programmes ab. Sie ist das Herz der objektorientierten Programmierung (OOP).
Strukturierung
[Bearbeiten]Eine Klasse besteht in C++ üblicherweise aus zwei Quellcode-Dateien: Deklaration und Implementation.
Deklaration erfolgt in einer Header-Datei
[Bearbeiten]Bei der Deklaration einer Klasse werden ihre Elemente (members, das umfasst Daten und Funktionen) festgelegt. Eine typische Deklaration könnte so aussehen:
class EineKlasse
{
public: // öffentlich
EineKlasse(); // der Default-Konstruktor
EineKlasse(int a=0); // weiterer Konstruktor mit Parameter und Defaultwert
EineKlasse(const EineKlasse& a); // Copy-Konstruktor
~EineKlasse(); // der Destruktor
int eineFunktion(int x=42); // eine Funktion mit einem (Default-) Parameter
private: // privat
int m_eineVariable;
};
Die erste Zeile gibt der neuen Klasse den Namen EineKlasse
. Im Klassenrumpf werden die Elemente deklariert, dann folgt ein abschließendes Semikolon. (Dieses zu vergessen ist eine häufige Fehlermeldung!) Die Reihenfolge der Member-Deklarationen ist beliebig; es empfiehlt sich aber, nach einem bestimmten System vorzugehen, z.B. mit den Konstruktoren und Destruktoren zu beginnen.
Eine solche Deklaration steht üblicherweise in einer Headerdatei. Es ist hilfreich, eine Headerdatei pro Klasse (EineKlasse.h
) und eine Implementations-Quelltextdatei (EineKlasse.cpp
) anzulegen. Alle Programmteile, die die Klasse EineKlasse
verwenden wollen, müssen die Headerdatei mittels
#include "EineKlasse.h"
einbauen kann man dies ebenfalls, aber nur falls diese in dem selben Ordner wie der Programmteil gespeichert ist. Sollte die Headerdatei in den include-Ordner des Compilers verschoben worden sein, lässt sie sich mittels
#include <EineKlasse.h>
einbinden.
Implementierung erfolgt in einer CPP-Datei
[Bearbeiten]Die Implementierung, d.h. der Code für die jeweiligen Memberfunktionen, erfolgt in einer CPP-Datei. Um die Funktion
int eineFunktion(int x=42)
zu implementieren, schreiben Sie
int EineKlasse::eineFunktion(int x)
{
//Anweisungen;
}
Dabei wird vor den Funktionsnamen eineFunktion
noch der Name der Klasse EineKlasse
gefolgt von zwei Doppelpunkten gestellt. Dies qualifiziert die Funktion, so dass der Compiler weiß, zu welcher Klasse die Funktionsdefinition gehört. Beachten Sie: Die Default-Initialisierung des Parameters x
wird hier nicht wiederholt.
Teile einer Klasse
[Bearbeiten]Eine Klasse setzt sich aus verschiedenen Teilen zusammen: Das sind zum einen Membervariablen und Memberfunktionen, zum anderen Konstruktoren und ein Destruktor, die spezielle Funktionen darstellen.
Konstruktoren
[Bearbeiten]Konstruktoren werden bei Erschaffung einer Instanz einer Klasse aufgerufen und dienen dazu, Initialisierungen auszuführen, etwa Membervariablen Standardwerte zuzuweisen. Also z.B. (in Datei EineKlasse.cpp
):
EineKlasse::EineKlasse()
: m_eineVariable(0)
{
// weitere Initialisierungen des Objekts
}
Die Funktionsdefinition beginnt mit dem Namen der Klasse und dem Bereichsoperator ::
. Der Konstruktor hat immer denselben Namen wie die Klasse. Er hat keinen Rückgabewert, kann aber einen oder mehrere Parameter besitzen:
EineKlasse::EineKlasse(int a)
: m_eineVariable(a)
{
// weitere Initialisierungen
}
Initialisierungen von Membervariablen (z.B. m_eineVariable
) erfolgen nach dem Funktionskopf, abgetrennt durch einen Doppelpunkt :
. Wollen Sie mehrere Members initialisieren, trennen Sie diese durch Kommas:
EineKlasse::EineKlasse(int a, float b)
: m_eineVariable(a), m_nocheineVariable(b)
{
// weitere Initialisierungen
}
Ein besonderer und sehr hilfreicher Konstruktor ist der Kopierkonstruktor (copy constructor), mit dem Sie das Kopieren aus einer anderen Instanz der gleichen Klasse genau steuern können, falls Sie keine eigenen Regeln zur Kopie festlegen, liefert Ihnen der Compiler eine Definition in Form einer Eins-zu-eins-Kopie:
EineKlasse::EineKlasse(const EineKlasse& a)
: m_eineVariable(a.m_eineVariable)
{
}
Sie hätten die Membervariable ebenso durch eine Zuweisung im Konstruktorkörper initialisieren können, im allgemeinen lohnt sich jedoch die Initialisierung über die Initialisierungsliste, falls Membervariablen selbst Objekte sind durchlaufen sie sonst eine Standardinitialisierung. Wie bei einer Kopie nicht anders zu erwarten, übernimmt die neu zu erschaffende Instanz den Wert der bereits existierenden Vorlage. Der Ausdruck const EineKlasse& a
zeigt an, dass als Parameter eine read-only Referenz der Vorlage übergeben wird, statt einer Wertkopie. Dazu mehr in den Kapiteln Referenzen und Funktionen.
Destruktoren
[Bearbeiten]Der Destruktor trägt ebenfalls denselben Namen wie die Klasse, aber mit einer vorgestellten Tilde ~
. Er hat keinen Rückgabewert und keine Parameter. Der Destruktor wird aufgerufen, wenn eine Instanz der Klasse zerstört wird. Das passiert, wenn der Bereich (z.B. ein Funktionsrumpf), in dem die Klasse lokal instanziert wird, endet oder der Operator delete
für die Instanz aufgerufen wird.
Im Destruktor können also abschließende Aktionen unternommen werden, z.B. reservierter Speicherplatz freigegeben werden, oder andere Objekte davon unterrichtet werden, dass es die Instanz ab jetzt nicht mehr gibt. Oft bleibt der Destruktorrumpf aber auch leer, wie in unserem einfachen Beispiel:
EineKlasse::~EineKlasse()
{
}
Memberfunktionen
[Bearbeiten]Memberfunktionen (auch Elementfunktionen oder Methoden genannt) sind innerhalb einer Klasse deklarierte Funktionen. In einem konsequent objektorientiert geschriebenen Programm sollte jede Funktion außer der Funktion main()
Memberfunktion einer Klasse sein. Die Implementierung geschieht wie auch bei Konstruktoren und Destruktoren in der Quelldatei EineKlasse.cpp
, also etwa:
int EineKlasse::eineFunktion(int a)
{
int ergebnis = m_eineVariable * a;
std::cout << "Ergebnis = " << ergebnis << std::endl;
return ergebnis;
}
In diesem Beispiel wird also der Parameter mit der Membervariablen multipliziert. Das Ergebnis wird ausgegeben (siehe Einfache Ein- und Ausgabe) und auch als Funktionswert zurückgegeben.
Innerhalb der Klasse, also in einer anderen Memberfunktion, rufen Sie die Funktion einfach mit ihrem Namen eineFunktion
auf. Außerhalb der Klasse müssen Sie die Instanz angeben, für die die Methode aufgerufen werden soll, siehe Beispiele unten.
Membervariablen
[Bearbeiten]Für Membervariablen gilt wie für Memberfunktionen, dass sie innerhalb der Klasse nur mit ihrem Namen angesprochen werden, von außerhalb mit dem Namen der Instanz qualifiziert werden müssen.
Erschaffung und Zerstörung von Instanzen
[Bearbeiten]Eine Instanz einer Klasse zu erschaffen funktioniert genauso wie bei einer Struktur: Man gibt die Namen von Klasse und Instanz an. Zusätzlich übergibt man gegebenenfalls in den Klammern die Parameter an den Konstruktor.
EineKlasse t1; // ruft Default-Konstruktor auf
EineKlasse t2(5); // ruft EineKlasse::EineKlasse(int) auf
t2.eineFunktion(7); // ruft Memberfunktion für die Instanz t2 auf
Die Instanz wird automatisch zerstört, wenn ihr Bezugsrahmen verlassen wird.
Der Operator .
(Punkt) dient zum Zugriff auf Elemente (Membervariablen ebenso wie Memberfunktionen).
Für Heap-Objekte verwenden Sie new
und delete
. Der Operator ->
(„Pfeil“) besorgt Dereferenzierung und Memberzugriff in einem.
EineKlasse *zeiger = new EineKlasse(3); // Instanz auf dem Heap erzeugen
zeiger->eineFunktion(2); // Memberfunktion aufrufen
delete zeiger; // freigeben
Zugriffskontrolle
[Bearbeiten]Ein grundlegendes Konzept von OOP ist die Datenkapselung, die in C++ mit den Schlüsselwörtern private
, protected
und public
gesteuert wird. Wenn kein Schlüsselwort angegeben wird, ist die definierte Variable bzw. Methode automatisch privat (private), d.h. sie kann nur innerhalb von Memberfunktionen verwendet werden. Versuche, von außerhalb der Klasse zuzugreifen, werden vom Compiler abgewiesen. Um ein Element öffentlich zugänglich zu machen, deklariert man es als public.
Man sollte in der strikten objektorientierten Programmierung alle Attribute kapseln (privat machen) und den Zugriff über öffentliche Methoden gewährleisten.
Die Unterscheidung, welche Members welchen Zugriffsschutz bekommen, nehmen Sie in der Deklaration der Klasse vor:
class EineKlasse
{
public:
int liesGeheimnis() const;
protected:
void bestimmeGeheimnis(int Wert);
private:
int geheimnis;
friend class AndereKlasse;
};
Das Schlüsselwort (public, protected, private) am Beginn eines Abschnitts bestimmt die Sichtbarkeit aller nachfolgend deklarierten Elemente bis zur nächsten Festlegung. Mit dem Schlüsselwort friend werden Klassen oder Funktionen gekennzeichnet, für die die Zugriffsschutzregeln außer Kraft gesetzt sind. Im Einzelnen bedeutet das:
public (öffentlich)
[Bearbeiten]Auf diese Elemente ist aus jeder Codepassage heraus Zugriff erlaubt. Die Methode liesGeheimnis in unserem Beispiel ist zugreifbar für Code, der EineKlasse verwendet, implementiert oder eine Klasse implementiert die von EineKlasse erbt. Der schreibende Zugriff jedoch über bestimmeGeheimnis ist der Öffentlichkeit nicht erlaubt, ebenso der direkte Zugriff auf die Variable geheimnis.
protected (geschützt)
[Bearbeiten]Auf Elemente dieser Zugriffsschutz-Stufe besteht Zugriff von Codeteilen die:
- Entweder die Klasse implementieren, oder für solche,
- die Klassen implementieren die direkt oder indirekt von der Klasse erben.
Wenn indirekt geerbt wird, ist zu beachten, dass die Klasse, von der letztlich geerbt wird, selbst public oder protected erbt.
private (privat)
[Bearbeiten]Diese Elemente sind nur aus Codeteilen heraus zugreifbar, die der Implementierung dieser Klasse dienen, üblicherweise also Methoden-Implementierungen.
friend (befreundet)
[Bearbeiten]Klassen oder Funktionen, die hinter diesem Schlüsselwort genannt werden, haben vollen Zugriff auf alle Elemente der Klasse. Mitunter erleichtert diese Möglichkeit das Programmiererleben enorm, man sollte sich jedoch vor einer inflationären Verwendung dieses „Kniffs“ hüten: Die Kontrolle der Folgen solcher Freizügigkeit wird mit jedem Freund aufwändiger.
Das Prinzip des Information-Hiding (=Datenkapselung)
[Bearbeiten]Es gehört zum guten Programmierstil, Membervariablen nicht direkt zugänglich, also public zu deklarieren, sondern nur öffentliche Memberfunktionen zum Lesen und Verändern der Variablen zur Verfügung zu stellen - sofern dies überhaupt notwendig ist. Diese Funktionen werden üblicherweise mit setVariablenname bzw. getVariablenname bezeichnet, man spricht in diesem Zusammenhang von "Settern" und "Gettern".
Dies bringt den Vorteil, dass in der Set-Funktion die Werte auf Gültigkeit geprüft werden können, und dass die Implementierung der Klasse vor dem Anwender der Klasse versteckt wird und sich somit ohne größere Schwierigkeiten für diesen ändern kann.
Statische Elemente
[Bearbeiten]Jede Instanz einer Klasse besitzt „ihre eigenen“ Membervariablen. In manchen Situationen sollen aber Daten nur einmal vorhanden sein, so dass alle Instanzen der Klasse darauf zugreifen können. Siehe Statische Elemente in Klassen.
this
[Bearbeiten]Im Kontext einer Klasse X
, also in deren Memberfunktionen, bezeichnet das Schlüsselwort this
einen Zeiger auf das aktuelle Objekt. this
hat den Typ X *const
; wenn die Memberfunktion als const
deklariert ist, sogar const X *const
. Der this-Zeiger kann z.B. nützlich sein, wenn
- eine Memberfunktion eine andere Funktion (i.d.R. Member einer anderen Klasse) aufrufen soll, die
this
als Argument benötigt, - ein Zuweisungsoperator das Resultat
*this
zurückgibt, - ein Membername durch eine gleichnamige lokale Variable überdeckt wird:
this->a
greift immer auf die Membervariablea
zu.
class Klasse
{
int a;
Klasse() : a(3) {}
void test()
{
int a = 5; // lokale Variable namens ''a''
a = 7; // ändert diese lokale Variable a
this->a = 10; // ändert die Membervariable von 3 auf 10
}
};