30. April 2009
Wer die Regeln der Programmiersprache C++ gelernt hat und weiß, wie man Variablen und Funktionen definiert, ist nicht automatisch ein guter C++-Entwickler. Während die Regeln in C++ eindeutig sind, wie Variablen und Funktionen zu definieren sind, besteht viel Spielraum bei der Strukturierung eines Programms. Wo soll eine Variable definiert werden - lokal in einer Funktion, als Eigenschaft in einer Klasse oder global im Programm? Soll eine Funktion als freistehende Funktion definiert werden oder als Methode zu einer Klasse gehören? Welche Klassen sollen überhaupt entwickelt werden und wie sollen ihre Schnittstellen aussehen? Dieser Artikel versucht, einige Best Practices in C++ vorzustellen, die sich bei vielen Entwicklern und in vielen Projekten bewährt haben.
Dieser Inhalt ist unter einer Creative Commons-Lizenz lizensiert.
Wer die Programmiersprache C++ lernt, lernt Regeln, die eingehalten werden müssen, um Quellcode zu erstellen, der von einem C++-Compiler übersetzt werden kann. Wer also zum Beispiel in C++ eine Variable anlegen möchte, muss einen Datentyp, ein Leerzeichen, einen Variablennamen und einen Strichpunkt eingeben. Diese Regeln sind verbindlich. Wer sie einmal gelernt und verinnerlicht hat, kann C++ programmieren. Nicht jeder, der die grundsätzlichen Regeln der Programmiersprache C++ kennt, ist aber automatisch ein guter C++-Programmierer.
C++ ist eine sehr flexible Sprache, die es Entwicklern freistellt, wie sie ein C++-Programm strukturieren möchten. So kann eine Variable zum Beispiel lokal in einer Funktion oder global außerhalb jeder Funktion definiert werden. Sie kann als Objekt-Eigenschaft Bestandteil einer Klasse sein oder - wenn das Schlüsselwort static
verwendet wird - eine Klassen-Eigenschaft. Allein die Regeln zu kennen, nach denen Variablen definiert werden, reicht nicht aus. Es müssen Entscheidungen getroffen werden wie die, an welcher Stelle eine Variable denn nun eigentlich definiert werden soll.
Weil es unbegrenzt viele Möglichkeiten gibt, ein C++-Programm zu strukturieren, stellt sich zwangsläufig die Frage, was wann am besten ist. Die Frage lässt sich nicht abschließend beantworten, da die Antwort jeweils von den Anforderungen abhängt, die ein Programm zu erfüllen versucht. Dies ist schließlich eine der Stärken von C++: Egal, was kommt, Sie können es in C++ so entwickeln, wie Sie wollen und wie es für Sie und Ihre Anforderungen am besten ist.
In diesem Artikel lernen Sie grundsätzliche Richtlinien und Tipps und Tricks kennen, die sich in vielen C++-Projekten bewährt haben. Nichts von dem, was Sie in diesem Artikel kennenlernen, ist verbindlich - wenn es verbindlich wäre, wäre C++ entsprechend designt worden. Der Artikel soll Ihnen vielmehr helfen, von anderen C++-Entwicklern zu lernen, auch wenn Sie später in Ihren eigenen Programmen sicher von der einen oder anderen Vorgehensweise abweichen werden.
Eines der wesentlichen Paradigmen, die die Programmiersprache C++ unterstützt, ist das der Objektorientierung. Für C++ gelten bei der Objektmodellierung daher grundsätzlich die gleichen Prinzipien, wie sie auch für andere objektorientierte Programmiersprachen gelten.
Objektorientierte Programme basieren auf Klassen, die wesentliche Objekte beschreiben, die zum Erfüllen von Anforderungen eines Programms herausgearbeitet wurden. Programmiersprachen wie C++ versuchen auf diese Weise, Entwicklern zu helfen, indem sie Objekte, die sich beim Diskutieren von Anforderungen eines Programms quasi automatisch herauskristallisieren, in den Programmcode übernehmen können. Wer zum Beispiel ein Programm mit einer grafischen Benutzeroberfläche schreiben will, denkt üblicherweise an Fenster, Menüs, Symbolleisten, Eingabefelder etc. Objektorientierte Programmiersprachen wie C++ ermöglichen es nun, diese Objekte als Klassen zu beschreiben, um dann auch beim Arbeiten mit Code die gleichen Objekte vor Augen zu haben.
Die Herausforderung für den Modellierer ist hier, die richtigen Klassen zu identifizieren, die einfach zu verstehen und intuitiv zu handhaben sind. Dabei muss oft ein schmaler Grad zwischen lösungsorientierten Klassen und Flexibilität gewählt werden. Wer heute ein Fenster mit Menüs und Symbolleisten braucht, benötigt morgen vielleicht etwas ganz anderes. Da sich Anforderungen ändern können, muss die Architektur objektorientierter Programme darauf vorbereitet sein. Typischerweise kann aber auch nicht zu viel Zeit investiert werden, um Architekturen mehr und mehr flexibel zu machen, da der eigentliche Grund, warum ein Programm entwickelt wird, der ist, dass es möglichst fertig wird und benutzt werden kann.
Klassen beschreiben Objekte, indem sie Fähigkeiten von Objekten als Methoden und Eigenschaften als Variablen definieren. So könnte eine Klasse, die ein Fenster in einer grafischen Benutzeroberfläche darstellt, eine Methode schliessen()
und eine Variable titel besitzen. Der Aufruf der Methode würde dazu führen, dass das Fenster geschlossen wird. Die Variable würde den Namen des Fensters speichern, der unter Windows ganz oben in der Fensterleiste angezeigt wird.
Wie bei anderen objektorientierten Programmiersprachen ist es auch in C++ empfehlenswert, Eigenschaften vor dem Zugriff aus anderen Klassen zu schützen. Man bezeichnet das als Datenkapselung. Sie wird in C++ über die Schlüsselwörter private
und protected
erreicht. Eigenschaften sollten grundsätzlich nicht mit public
deklariert werden.
Der Vorteil der Datenkapselung ist, dass nur Methoden der eigenen Klasse und friend
-Funktionen Zugriff auf geschützte Eigenschaften haben. Je weniger Code Zugriff auf Variablen hat, umso einfacher ist es nachzuvollziehen, wer wann auf die Variablen zugreift und sie wie verändert. Sollten Sie zum Beispiel feststellen, dass etwas in Ihrem Programm schief läuft und eine Eigenschaft auf einmal einen ungültigen Wert besitzt, müssen Sie lediglich die Methoden der Klasse und friend
-Funktionen überprüfen. Wäre die Eigenschaft nicht geschützt, könnte jede andere Klasse und Funktion direkt auf sie zugreifen, was für Sie die Überprüfung von wesentlich mehr Code notwendig machen würde. Die Datenkapselung hilft also, den Zugriff auf Variablen einzuschränken und nur denjenigen Teilen im Programm Zugriff einzuräumen, die ein berechtigtes Interesse haben. Daraus folgt auch, dass kleinere Klassen grundsätzlich vorzuziehen sind.
Die Datenkapselung erfordert, dass Eigenschaften geschützt werden und daher von außerhalb der Klasse nicht zugänglich sind (mit der Ausnahme von friend
-Funktionen). Da sie nur innerhalb der Klasse verwendet werden können, können sie später ohne Probleme angepasst werden. Schwieriger ist es, öffentliche Methoden zu ändern. Diese stellen die Schnittstelle dar, die von anderen Teilen des Programms verwendet wird, um mit einem Objekt zu agieren. Wenn Sie die Schnittstelle ändern wollen, müssen Sie häufig auch unterschiedliche Programmteile anpassen, von denen aus auf die aktuelle Schnittstelle zugegriffen wird. Auf die Schnittstelle einer Klasse sollte daher besonderes Augenmerk gelegt werden, um sie möglichst stabil zu machen.
Wenn Sie Klassen erstellen, um Objekte zu beschreiben, fangen Sie mit der Schnittstelle an. Sie definieren also zuerst öffentliche Methoden. Eigenschaften werden erst in einem zweiten Schritt hinzugefügt. Wenn Sie zuerst die Schnittstelle beschreiben und Eigenschaften ignorieren, werden Sie sich stärker auf die Frage konzentrieren, was das Objekt eigentlich tun soll. Eigenschaften werden dann im zweiten Schritt hinzugefügt, um die Implementation der Methoden zu unterstützen. Wenn Sie zum Beispiel eine Klasse fenster
entwerfen, würde sich eine Methode anzeigen()
anbieten, die bei Aufruf das Fenster erscheinen lässt. Dabei soll dann auch in der Fensterleiste der Name des Programms erscheinen. Ob dieser Name dann aber in einer Eigenschaft titel gespeichert ist oder zum Beispiel dynamisch innerhalb der Methode anzeigen()
aus einer Datei geladen wird, ist zweitrangig. Die Schnittstelle einer Klasse bestehend aus öffentlichen Methoden geht vor.
Die Datenkapselung kann verbessert werden, wenn Funktionen, die keinen Zugriff auf geschützte Eigenschaften einer Klasse benötigen, nicht als Methoden, sondern tatsächlich als freistehende Funktionen implementiert werden können. Wenn Funktionen von Klassen entkoppelt werden, können sie einfacher wiederverwendet werden, indem sie zum Beispiel als Template definiert werden. Anstatt die Methode anzeigen()
der Klasse fenster
hinzuzufügen, könnte sie als freistehende Funktion implementiert werden, um nicht nur Fenster, sondern auch andere grafische Elemente ein- und ausblenden zu können. Dies setzt natürlich voraus, dass die Funktion implementiert werden könnte, ohne auf geschützte Eigenschaften der Klasse fenster
zuzugreifen. Das Anzeigen grafischer Elemente könnte aber als eine wichtige Funktion identifiziert werden, die gleichberechtigt neben Klassen stehen soll, die grafische Elemente repräsentieren. Dies entspricht den Algorithmen der C++-Standardbibliothek, die als freistehende Funktionen definiert sind und auf viele Container aus dem C++-Standard angewandt werden können.
Objektmodelle bestehen normalerweise nicht aus unabhängigen Klassen, sondern beschreiben auch Beziehungen zwischen diesen. Die beiden wichtigsten Beziehungen sind "ein Teil von" und "eine Art von". Die erstgenannte Beziehung wird durch Komposition, die zweitgenannte durch Vererbung modelliert.
Bei der Komposition verweist eine Klasse auf andere Klassen. Wenn zum Beispiel ein Fenster eine Schaltfläche fenster
eine Eigenschaft ok vom Typ schaltflaeche
bekommen. Die Schaltfläche wäre dann ein Teil des Fensters.
Die Komposition wird andauernd angewandt. Klassen, die völlig unabhängig sind und auf keine anderen Klassen verweisen, sind eher selten. Da die Komposition eine recht lose Bindung zwischen Klassen darstellt und bei entsprechender Datenkapselung leicht zu verändern ist, stellt sie kein Problem dar.
Die Vererbung wird im Vergleich zur Komposition kritischer gesehen. Zum einen entstehen durch die Vererbung Klassenhierarchien, die eine wesentlich stärkere Bindung zwischen Klassen herstellen. So ist es zum Beispiel normalerweise nicht möglich, im Nachhinein Klassen aus einer Hierarchie zu entfernen, ohne unterschiedliche Teile eines Programms ändern zu müssen. Zum anderen tragen Klassenhierarchien, die aus vielen Klassen mit langen Vererbungslinien bestehen, nicht unbedingt zur Übersicht bei. Da Kindklassen Schnittstellen ihrer Elternklassen erben, wächst üblicherweise in jeder Generation der Umfang der Schnittstellen. Es macht es für Entwickler nicht unbedingt einfacher, wenn Klassen Hunderte von Methoden anbieten.
Der Trend geht in C++ daher eher weg von der Vererbung. Das heißt nicht, dass die Vererbung nicht mehr eingesetzt werden sollte. Wenn nicht unbedingt notwendig, wird im C++-Standard auf die Vererbung aber verzichtet. So gibt es zum Beispiel Anforderungen, die von Iteratoren erfüllt werden müssen. Anstatt die Anforderungen als abstrakte Klasse zu definieren, von der ein Iterator abgeleitet und gezwungen wird, bestimmte Methoden zu implementieren, begnügt man sich damit, die Anforderungen im C++-Standard lediglich schriftlich festzuhalten. Iteratoren müssen Anforderungen erfüllen, die lediglich im Text stehen und nicht als C++-Code definiert sind.
Um Anforderungen überprüfbar zu machen, werden in der kommenden Version des C++-Standards voraussichtlich Konzepte eingeführt, die mit dem Schlüsselwort concept
erstellt werden können. Sie werden im Zusammenhang mit Templates angewandt, um zu definieren, welche Anforderungen Datentypen erfüllen müssen, um mit ihnen ein Template zu instantiieren. Die Entwicklung von C++ geht hier eindeutig in Richtung Templates und weg von großen, unübersichtlichen Klassenhierarchien.
Die Abkürzung RAII steht für Resource Acquisition Is Initialization. Damit bezieht man sich auf eine Regel, die besagt, dass wann immer eine Ressource geöffnet wird, die auch wieder geschlossen werden muss, dies mit Hilfe eines Objekts passieren soll, das mit der Ressource initialisiert wird. Denn wenn das Objekt gelöscht wird, wird automatisch der Destruktur aufgerufen, in dem die Ressource geschlossen werden kann. Dies hat den Vorteil, dass kein Entwickler vergessen kann, eine Ressource zu schließen. Selbst dann, wenn man als Entwickler entsprechende Funktionen zum Schließen von Ressourcen nicht vergisst, sollte RAII beherzigt werden. Sollte nämlich eine Ausnahme geworfen werden, kann es sein, dass die Funktion zum Schließen einer Ressource nicht aufgerufen wird, weil die Ausnahme die aktuelle Funktion vorzeitig abbricht.
Ein prominentes Beispiel für RAII ist die Klasse std::auto_ptr
aus dem C++-Standard. Es handelt sich hierbei um einen sogenannten smart pointer, dem ein Zeiger auf dynamisch reservierten Speicher übergeben wird. Der Vorteil für den Entwickler ist, dass er nicht mehr daran denken muss, an geeigneter Stelle den dynamisch reservierten Speicher mit delete
freizugeben. Dies ist besonders dann hilfreich, wenn der Speicher in einer Funktion reserviert, in einer anderen Funktion aber verwendet wird und dort anschließend freigegeben werden muss. Da die Speicherfreigabe in solchen Fällen leicht vergessen wird, können smart pointer wie std::auto_ptr
sehr hilfreich sein.
Beachten Sie, dass Sie RAII auch in anderen Fällen anwenden sollten - nicht nur bei der Reservierung und Freigabe von dynamischen Speicher. So gibt es zum Beispiel unter Windows viele Funktionen, durch die eine Ressource im Betriebssystem geöffnet werden muss, bevor dann auf sie zugegriffen werden kann. Damit das Schließen der Ressource nicht vergessen wird, bietet es sich an, eine entsprechende Klasse zu entwickeln, die das Öffnen der Ressource im Konstruktur und das Schließen im Destruktur vornimmt.
In Konstruktoren werden üblicherweise Eigenschaften eines Objekts initialisiert. Während wie in anderen Methoden auch innerhalb eines Konstruktors über den =
-Operator auf Eigenschaften zugegriffen werden könnte, um sie zu initialisieren, sollten sogenannte Initialisierungslisten vorgezogen werden.
#include <string> class person { public: person() : name("Boris"), maennlich(true), schuhgroesse(43) { } private: std::string name; bool maennlich; int schuhgroesse; };
Initialisierungslisten werden hinter einem Doppelpunkt zwischen dem Kopf und dem Rumpf des Konstruktors angegeben. Dabei werden Eigenschaften durch Komma getrennt in der Reihenfolge initialisiert, in der sie in der Klasse definiert sind. Eigenschaften, die nicht in der Initialisierungsliste auftauchen, werden nicht explizit initialisiert. Für primitive Datentypen bedeutet dies, dass keine Initialisierung stattfindet. Für Eigenschaften, die auf Klassen basieren, wird der Standardkonstruktor aufgerufen. Für Eigenschaften in Initialisierungslisten kann jedoch ein geeigneter Konstruktor aufgerufen werden. Die Initialisierung von Eigenschaften über einen geeigneten Konstruktoraufruf ist nicht nur natürlicher als Eigenschaften per Standardkonstruktor zu initialisieren und ihnen dann im zweiten Schritt über den =
-Operator einen Wert zuzuweisen. Initialisierungslisten sind auch aus Performance-Gründen vorzuziehen.
Das Überladen von Operatoren kann den Umgang mit Klassen vereinfachen. So gewöhnt man sich zum Beispiel schnell daran, Variablen vom Typ std::string
mit dem +
-Operator zu verknüpfen. Da der Code leicht verständlich ist, ist dieser überladene Operator eines gutes Beispiel.
Wenn Sie Klassen definieren, überlegen Sie sich, ob es Sinn macht, den einen oder anderen Operator zu überladen. Ob es Sinn macht oder nicht hängt davon ab, ob die Anwendung eines Operators einleuchtend ist und den Code verständlicher macht. Es ist dabei von Vorteil, Operatoren ähnlich zu definieren, wie es verschiedene Klassen in der C++-Standardbibliothek tun. Da Entwickler im Allgemeinen mit dem C++-Standard vertraut sind, werden sie überladene Operatoren im Zusammenhang mit neuen Klassen einfacher einsetzen können, wenn sie eine ähnliche Bedeutung haben wie überladene Operatoren im C++-Standard.
Gute Beispiele für überladene Operatoren sind []
, <<
oder >>
. Der []
-Operator wird von verschiedenen Containern überladen, um mit einem Index auf ein Element im Container zuzugreifen. Die <<
- und >>
-Operatoren werden im Zusammenhang mit Streams angewandt, um Daten auf einen Stream auszugeben oder von einem Stream zu lesen. Falls Sie Klassen erstellen, die ähnliche Operationen unterstützen - also zum Beispiel einen Zugriff per Index - würde sich das Überladen entsprechender Operatoren anbieten.
Operatoren, die nicht überladen werden sollten, sind ||
, &&
und ,
. Da Entwickler bei diesen Operatoren intuitiv von einer bestimmten Funktionsweise ausgehen und nicht daran denken, dass sich die Funktionsweise geändert haben könnte, wäre das Überladen dieser Operatoren kontraproduktiv und würde viele Entwickler verwirren.
Ausnahmen sollten dann geworfen werden, wenn eine im Allgemeinen fehlerfreie Funktion dennoch mal fehlschlägt. Ein gutes Beispiel ist der Operator new
, mit dem dynamisch Speicher angefordert werden kann. new
gibt einen Zeiger auf einen neuen Speicherbereich zurück. Im Allgemeinen kann man davon ausgehen, dass der Aufruf von new
funktioniert. Da Computer nicht unbegrenzt Speicher haben, könnte es aber sein, dass eine dynamische Speicherallokation fehlschlägt und new
keinen Zeiger auf neu reservierten Speicher zurückgegeben kann. Für diesen unwahrscheinlichen, aber dennoch möglichen Fall wirft new
eine Ausnahme vom Typ std::bad_alloc
.
So wie new
sollten auch Ihre Funkionen und Klassen gegebenenfalls Ausnahmen werfen. Es hängt dabei von der Definition der entsprechenden Funktion oder Methode ab, ob ein Fehler normal ist und den Aufrufer nicht unbedingt überrascht - dann sollte keine Ausnahme geworfen werden, sondern ein Fehler zum Beispiel per Rückgabewert gemeldet werden. Tritt ein Fehler aber nur in Ausnahmefällen auf, so dass eine ständige Überprüfung eines Rückgabewerts dem Aufrufer nicht zugemutet werden soll, sollte besser eine Ausnahme geworfen werden.
Wenn eine Ausnahme geworfen wird, sollte ein geeigneter Datentyp gewählt werden, der die Art des Problems beschreibt. new
wirft beispielsweise eine Ausnahme vom Typ std::bad_alloc
, die verdeutlicht, dass eine Speicherallokation fehlgeschlagen ist. Gibt es für die Art des Problems, für das Sie eine Ausnahme werfen möchten, keinen passenden Datentyp aus dem C++-Standard, sollten Sie eine eigene Klasse definieren. Für den Fall, dass Sie keine eigene Klasse definieren wollen, sondern einfach nur einen allgemeinen Fehlertyp brauchen, empfiehlt sich die Klasse std::runtime_error
.
Entscheiden Sie sich, eine neue Klasse zu definieren, sollten Sie sie in die Klassenhierarchie für Ausnahmen aus dem C++-Standard integrieren. Die Elternklasse, von der alle Ausnahme-Klassen im C++-Standard wie auch std::bad_alloc
abgeleitet sind, ist std::exception
. Wenn Sie Ihre Klasse von std::exception
oder einer anderen Klasse der Ausnahme-Klassenhierarchie ableiten, kann mit folgender try-catch
-Anweisung jede beliebige Ausnahme abgefangen werden.
try { } catch (std::exception&) { }
Würde Ihre Klasse nicht in die Ausnahme-Klassenhierarchie des C++-Standards integriert werden, dürften Entwickler nicht vergessen, ihre try-catch
-Anweisung anzupassen, die sämtliche Ausnahmen abfangen soll, bevor ein Programm ohne Meldung beendet wird. Das würde nicht nur einen Anwender verdutzt zurücklassen, sondern würde es auch für Entwickler schwierig machen nachzuvollziehen, aus welchem Grund ein Programm auf einmal beendet wurde.
Wenn ein Verweis auf ein anderes Objekt notwendig ist, kann in C++ sowohl eine Referenz als auch ein Zeiger verwendet werden. Der entscheidende Unterschied ist, dass eine Referenz einmal initialisiert nicht neu gesetzt werden kann. Eine Referenz verweist also immer auf das gleiche Objekt. Damit ist bereits klar, dass Sie dann einen Zeiger verwenden müssen, wenn auf unterschiedliche Objekte verwiesen werden muss. Haben Sie zum Beispiel verschiedene Fenster vom Typ fenster
und wollen in einer Variablen speichern, welches Fenster momentan im Vordergrund liegt, müssen Sie eine Variable vom Typ fenster*
anlegen. Denn diese kann jeweils neu gesetzt werden und die Adresse des fenster
-Objekts speichern, das das momentan im Vordergrund liegende Fenster repräsentiert.
Beim Definieren von Funktionen, denen keine Kopie eines Objekts übergeben werden soll, stellt sich die Frage, ob der Parameter eine Referenz oder ein Zeiger sein soll. Grundsätzlich ist die Referenz vorzuziehen, da der Zugriff auf das Objekt dann etwas einfacher mit dem .
-Operator erfolgen kann. Ist der Parameter für die Funktion aber optional, bietet sich ein Zeiger an, da ein Zeiger auf 0 gesetzt werden kann. Ist der Zeiger auf 0 gesetzt, bedeutet das für die Funktion, dass kein Parameter angegeben wurde. Ein Parameter, der eine Referenz oder ein Zeiger ist, kann also dem Aufrufer die zusätzliche Information bieten, dass in einem Fall ein Objekt angegeben werden muss, im anderen Fall die Angabe optional ist.
Das Schlüsselwort const
kann in C++ verwendet werden, um Variablen als konstant zu definieren. Es wird typischerweise im Zusammenhang mit Funktions- und Methodendefinitionen verwendet, um dem Aufrufer mitzuteilen, ob ein Parameter von einer Funktion verändert wird oder nicht.
void f(int i, const char *c, bool &b);
Obige Funktion f()
erwartet drei Parameter: Der erste Parameter vom Typ int
wird als Kopie übergeben. Der zweite Parameter wird ebenfalls als Kopie übergeben. Da es sich um einen Zeiger handelt, der auf einen Speicherbereich verweist, kann aber angegeben werden, ob der Speicherbereich verändert wird oder nicht. Die Angabe const char*
bedeutet, dass der Zeiger c, der selbst eine Kopie ist, den Speicher, auf den er zeigt, nicht verändern wird. Der dritte Parameter ist eine Referenz auf eine Variable vom Typ bool
. Da das Schlüsselwort const
nicht verwendet wird, kann die referenzierte Variable von der Funktion geändert werden.
Es ist möglich, das Schlüsselwort const
hinter Methodenköpfen anzugeben.
#include <string> class person { public: person(std::string n) : Name(n) { } std::string name() const { return Name; }; private: std::string Name; };
Die Klasse person
bietet eine Methode name()
an, die den Namen eines Objekts zurückgibt. Da name()
ein Objekt nicht ändert, kann sie als const
deklariert werden.
Eine Methode, die als const
deklariert ist, kann ausschließlich Methoden der eigenen Klasse aufrufen, die ebenfalls als const
deklariert sind. Für ein Objekt, das als const
definiert ist, können ausschließlich Methoden aufgerufen werden, die ihrerseits mit const
deklariert sind. Diese Regeln stellen sicher, dass zum Beispiel ein konstantes Objekt nicht versehentlich geändert wird, indem eine Methode aufgerufen wird, die nicht konstant ist.
Beachten Sie, dass const
nicht bedeutet, dass eine Variable oder ein Objekt nicht geändert werden kann. Es handelt sich bei const
vielmehr um ein Versprechen, dass eine Methode einen Parameter oder eine Eigenschaft nicht ändert. Eine Methode sollte sich an das Versprechen halten - sie muss es aber nicht. Es ist in C++ also durchaus möglich, eine Variable, die mit const
definiert wurde, zu ändern. Da ein Aufrufer erwartet, dass sich eine Methode an das Versprechen hält, das sie mit const
gegeben hat, sollte eine Methode idealerweise keine Änderungen vornehmen, die für den Aufrufer unerwartet und überraschend wären.
#include <iostream> void f(const int &a, int &b) { b = 99; } int main() { int i = 0; f(i, i); std::cout << i << std::endl; }
Im obigen Beispiel wird eine Variable i im ersten Parameter als konstante Referenz und im zweiten Parameter als nicht-konstante Referenz an die Funktion f()
weitergereicht. Die Funktion f()
kann nun über den ersten Parameter a ausschließlich lesend auf die referenzierte Variable zugreifen, weil es sich um eine konstante Referenz handelt. Über den zweiten Parameter b kann jedoch der Wert in der referenzierten Variable geändert werden. Dass es sich um die jeweils gleiche Variable i handelt, spielt keine Rolle.
const
bedeutet nicht, dass eine Variable nicht geändert werden kann. const
bedeutet, dass eine Variable über eine mit const
-definierte Referenz und einen mit const
-definierten Zeiger nicht geändert werden kann. Über andere Referenzen und über andere Zeiger, die nicht konstant sind, darf die Variable sehr wohl geändert werden.
Copyright © 2008, 2009 Boris Schäling