C++/Tutorial: 12. Weitere Eigenschaften von Klassen
Nun haben wir bereits die Eigenschaften von Klassen besprochen, die am wichtigsten sind um damit ein objektorientiertes System zu modellieren, aber C++ bietet uns einige weitere Eigenschaften, die nicht unbedingt etwas mit objektorientierung zu tun haben, aber mit Klassen, eine davon sind die statischen Variablen.
Inhaltsverzeichnis
Statische Member
Statische Variablen
Statische Variablen(Klassenvariablen) kennen wir bereits im Bezug auf Dateien und auf Funktionen, aber auch Klassen haben statische Variablen. Dabei haben statische Variablen mit objektorientierter Programmierung nichts zu tun. Statische Variablen in Klassen sind vergleichbar mit globalen Variablen, sie existieren nur einmal und bleiben existent, sie befinden sich aber innerhalb einer Klasse. Statische Variablen werden mit dem Schlüsselwort static versehen. Zudem reicht es nicht aus statische Member zu deklarieren, sie müssen ähnlich den Methoden auch noch definiert werden. Der Zugriff auf sie erfolgt mit dem Scopeoperator :: angewand auf die Klasse(innerhalb der Klasse kann auf den Scopeoperator verzichtet werden). Zudem gelten die Zugriffsmodifier auch auf statische Variablen, sodass auf eine statische Variable, die private definiert wurde nur aus Methoden dieser Klasse (ob Instanz-, oder Klassenmethoden spielt dabei keine Rolle) zugegriffen werden kann. Was das bedeuted erklären wir an einem Beispiel einer Klasse, die zählt, wie viele Instanzen dieser Klasse existieren. Zunächst einmal deklarieren wir eine solche Klasse, sie benötigt eine statische, private Variable, die die Anzahl der Instanzen enthält. Einen Konstruktor, der hochzählt, wenn eine Instanz erstellt wurde und einen Destruktor, der runterzählt, wenn eine Instanz zerstört wurde
class example { public: example(); example(example const& that); ~example(); private: static unsigned instances; };
instances ist dabei die statische Variable, die genau einmal für diese Klasse existiert. Die Konstruktoren müssen wir nun so definieren, dass die Zahl der Instanzen
example::example() { instances++; } example::example(example const&) { instances++; }
Wenn eine Instanzen zerstört wird müssen wir natürlich wieder um eins runter zählen:
example::~example() { instances--; }
Wie bereits erwähnt müssen statische Variablen ebenfalls definiert werden, das sieht dann so aus:
unsigned example::instances(0); //definiert die statische Variable instances und initialisiert sie mit dem Wert 0
instances beinhaltet nun immer die aktuelle Anzahl der Objekte dieser Klasse.
Statische Methoden
Ebenso wie statische Variablen können auch statische Methoden existieren. Um eine Methode statisch zu deklarieren wird dieser einfach static vorangestellt. Der Zugriff erfolgt wie auf statische Variablen. Schauen wir uns das auch mal an einem Beispiel an, z.B. wollen wir in unsere Klasse eine statische Methode einführen, die die Anzahl aktueller Instanzen ausgibt. In public fügen wir daher die Deklaration ein:
static void print_number_of_instances();
Definieren tun wir die Funktion genauso, wie eine normale Memberfunktion:
void example::print_number_of_instances() { cout << "Es existieren derzeit " << instances << " Instanzen" << endl; }
Schauen wir uns nun einmal ein Beispiel an, wie die Klasse verwendet wird:
int main() { example::print_number_of_instances(); //0 example e1; example e2; { example e3; example::print_number_of_instances(); //3 } example::print_number_of_instances(); //2 }
Const correctness
Eine weitere, in C++ relativ einzigartige Fähigkeit ist die Möglichkeit Klassen "const-correct" zu definieren. Nehmen wir ein (zugegeben, relativ praxisfernes) Beispiel: eine Klasse, die einen int Member hat und einen getter, der diesen zurückgibt.
class bar { public: bar(int a); //damit eine Klasse const sein kann, braucht sie einen Konstruktor int get_a(); private: int m_a; }; bar::bar(int a) { m_a = a; } int bar::get_a() { return m_a; };
Nehmen wir an, wir erstellen ein konstantes Objekt dieser Klasse:
int main() { bar const b; }
Da b nun konstant ist, ist auch m_a konstant und kann nicht verändert werden. get_a verändert diese aber nicht, also müssten wir sie doch eigentlich aufrufen können? Geht aber nicht. Wieso? Damit wir eine Funktion auf ein konstantes Objekt aufrufen können, müssen wir diese Funktion als const deklarieren, d.h. wir stellen dem Funktionskopf in Deklaration und Definition ein const nach:
int get_a() const;
int bar::get_a() const { return m_a; };
Solche Funktionen können nun auch auf konstante Objekte aufgerufen werden. const garantiert dem Compiler, dass die Funktion die Member nicht verändert, so ist es auch nicht möglich in einer solchen Funktion eine Membervariable zu verändern. Das Objekt ist nun in der Lage const zu sein und kann dennoch korrekt verwendet werden. Wann immer ein Funktion nur lesend auf die member zugreift, sollte sie const deklariert werden, das nennt man const-correct.
Semantische const-correctness
Wir haben zwar bereits eine Klasse const-correct definiert, aber bisher macht dies ausschließlich einen syntaktischen Unterschied. Nehmen wir mal an, wir haben eine Funktion, get_pointer_to_a, die eben dies tut: einen Pointer auf m_a zurückgeben:
class bar { public: bar(int a); //damit eine Klasse const sein kann, braucht sie einen Konstruktor int get_a() const; int* get_pointer_to_a(); private: int m_a; }; int* bar::get_pointer_to_a() { return &m_a; }
get_pointer_to_a ist hier noch nicht als const deklariert, wir können sie also nicht auf ein konstantes Objekt aufrufen. Wollen wir sie als const deklarieren, werden wir aber feststellen, dass dies nicht so einfach geht, der GCC liefer z.B. die Fehlermeldung: error: invalid conversion from ‘const int*’ to ‘int*’ Woran das liegt ist einfach zu verstehen. Innerhalb einer const Funktion, sind alle Member konstant. Wenn wir get_pointer_to_a also const deklarieren, ist m_a vom Typ int const m_a. Wenn wir nun einen Pointer auf diese Addresse anlegen ist dieser vom Typ int const*, unsere Funktion soll aber int* zurück geben => Fehler. Wir müssen also unsere Funktion int const* zurückgeben lassen. Aber was wenn wir das nicht wollen? Eigentlich wollen wir, dass get_pointer_to_a auf ein normales Objekt auch einen Pointer zurückgibt, mit dem wir m_a verändern können, auf ein konstantes Objekt aber einen Pointer auf int const zurückgibt, da wir eine Konstante ja nicht verändern wollen. Die Lösung ist simpel: wir können eine Funktion nach const überladen. Wenn wir die selbe Funktion einmal const und einmal nicht const erstellen, wird die const Version nur auf konstante Objekte der Klasse angewand, unsere Klasse sieht also folgendermaßen aus:
class bar { public: bar(int a); //damit eine Klasse const sein kann, braucht sie einen Konstruktor int get_a() const; //lässt sich auf const und nicht const Objekte aufrufen und gibt m_a zurück int* get_pointer_to_a(); //gibt einen Pointer auf m_a zurück, wenn auf nicht konstante Objekte aufgerufen int const* get_pointer_to_a() const; //gibt einen Pointer auf ein nicht veränderbares m_a zurück, wenn auf konstante Objete aufgerufen private: int m_a; }; bar::bar(int a) { m_a = a; } int* bar::get_pointer_to_a() { return &m_a; } int const* bar::get_pointer_to_a() const { return &m_a; } int bar::get_a() const { return m_a; };
mutable
Ein weiterer Fall, der eintreten kann, ist dass wir ein konstantes Objekt haben, und in einer const Funktion trotzdem eine Variable verändern wollen. Z.B. wenn diese Variable keinerlei Einfluss auf das Verhalten des Objektes hat, sondern nur aus Geschwindigkeitsgründen existiert. Damit eine Variable auch aus const Funktionen verändert werden kann muss sie als "mutable" deklariert sein.