Einführing in C++ Kapitel 3 — Zeiger in C++

Aufgrund der Bedeutung, die Zeigern in C und C++ zukommt, werden wir in diesem Kapitel näher auf die wichtigsten Fragen eingehen. Selbst wenn Du in der Verwendung von Zeigern absolut firm bist, solltest Du dieses Kapitel nicht links (respektive rechts) liegen lassen, da auch einige Neuerung von C++ hier präsentiert werden.

Repetitorium

Beispielprogramm: ZEIGER.CPP

Das Programm ZEIGER.CPP ist ein einfaches Beispiel für die Verwendung von Zeigern. Hier wiederholen wir die Grundzüge des Umgangs mit Zeigern, das heißt, wenn Du Dich auf diesem Gebiet sicher fühlst, kannst Du dieses Beispielprogramm ohne weiteres überspringen.

In ANSI-C ebenso wie in C++ wird ein Zeiger deklariert, indem man dem Variablennamen ein Sternchen voransetzt. Der Zeiger zeigt dann auf eine Variable dieses spezifischen Typs und sollte nicht für Variablen anderen Typs verwendet werden. Daher ist ZgInt ein Zeiger auf Variablen vom Typ int und sollte nur für solche verwendet werden. Natürlich weiß die erfahrene C Programmiererin, daß sie den Zeiger auch für alle anderen Typen einsetzen kann, einfach mit Hilfe einer "cast" Typenumwandlung, sie ist dann aber auch für die korrekte Verwendung verantwortlich.

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel Wiederholung:

Der C++-Programmierer: C++ lernen - professionell anwenden - Lösungen nutzen
Eines der jedenfalls besseren Bücher zur Einführung in C++ ist umfassend und verständlich, übersichtlich und gelungen. Halbwegs hübsch anzuschauen ist es auch.
›› Mehr C++-Bücher

Abb. 3-1

In Zeile 12 wird dem Zeiger ZgInt die Adresse der Variablen Schwein zugewiesen, und in Zeile 13 verwenden wir den Namen des Zeigers ZgInt, um den Wert der Variablen Hund zum Wert von Schwein zu addieren. Das Sternchen dereferenziert den Zeiger genauso wie es auch in C geschieht. Abbildung 3-1 stellt den Speicherinhalt nach der Zeile 13 graphisch dar. Eine Box mit Sternchen ist ein Zeiger. Wir verwenden die Adresse, um in Zeile 14 den Wert der Variable Schwein auszugeben und zu zeigen, wie man einen Zeiger mit dem Ausgabestromobjekt cout verwendet. Genauso wird dem Zeiger auf eine Variable vom Typ float, ZgFloat, die Adresse von x zugewiesen, um dann in einer einfachen Rechnung in Zeile 18 Verwendung zu finden.

Konstante Zeiger und Zeiger auf Konstanten

Die Definition von C++ erlaubt es, einen Zeiger auf eine Konstante so zu definieren, daß der Wert, auf den der Zeiger zeigt, nicht verändert werden kann, sehr wohl aber die Adresse des Zeigers. Er kann dann also auf eine andere Variable oder Konstante zeigen. Diese Methode, einen Zeiger auf eine Konstante zu definieren, illustriert Zeile 22. Neben Zeigern auf Konstante sind auch konstante Zeiger möglich, solche die nicht verändert werden können. Dies zeigen wir in Zeile 23. Beachte, daß wir diese Zeiger im Beispielcode nicht verwenden.

Diese beiden Methoden können dazu dienen, beim Kompilieren eine zusätzliche Kontrolle durchzuführen und so die Qualität des Code zu heben. Wenn Du sicher bist, daß ein Zeiger immer auf dieselbe Variable oder Konstante zeigen wird, solltest Du ihn als konstanten Zeiger definieren. Bist Du sicher, daß ein Wert nicht verändert wird, definierst Du ihn als Konstante und der Compiler wird Dich darauf aufmerksam machen, wenn Du doch versuchen solltest, den Wert zu ändern.

Ein Zeiger auf void in C++

Der Zeiger auf void ist zwar Teil des ANSI-C Standards, aber doch relativ neu, sodaß wir hier kurz darauf eingehen. Einem Zeiger auf void kann der Wert jedes anderen Zeigertyps zugewiesen werden. Wie Du siehst, wird dem Zeiger auf void, universal in Zeile 15 die Adresse einer Variable vom Typ int zugewiesen wird, in Zeile 20 aber die Adresse einer Variable vom Typ float, ohne "cast" und ohne Compiler-Fehler. Das ist ein relativ neues Konzept in C und C++. Es gibt der Programmiererin die Möglichkeit, einen Zeiger zu definieren, der auf eine Vielfalt von Dingen zeigen kann um die Informationen innerhalb eines Programmes so richtig zum Laufen bringt. Ein gutes Beispiel wäre etwa die malloc() Funktion, die eine Zeiger auf void zurückgibt. Dieser Zeiger kann auf nahezu alles zeigen, sodaß man den zurückgegebenen Zeiger auf den richtigen Typ zeigen läßt.

Für einen Zeiger auf void wird im Speicher so viel Platz zur Verfügung gestellt, daß er mit allen vordefinierten einfachen Typen, die in C++ oder ANSI-C verfügbar sind, verwendet werden kann. Er kann aber auch mit allen zusammengesetzten Typen, die die Programmiererin definieren kann, verwendet werden, da sich zusammengesetzte Typen aus einfachen zusammensetzen.

Wenn bei diesem trivialen Programm auch nur die kleinste Unklarheit auftritt, solltest Du noch einmal ein gutes C Programmierbuch zur Hand nehmen und die Verwendung von Zeigern nachschlagen, bevor Du in dieser Einführung weitergehst. Im weiteren Verlauf wird nämlich ein profundes Wissen um Zeiger und ihre Verwendung vorausgesetzt. Es ist unmöglich, ein etwas komplexeres Programm zu schreiben, ohne Zeiger zu verwenden.

Kompiliere das Programm und führe es aus.

Dynamische Speicherverwaltung in C++

Beispielprogramm: NEWDEL.CPP

Das Programm NEWDEL.CPP ist ein erstes Beispiel für die Verwendung der Operatoren new und delete. Die Operatoren new und delete bewerkstelligen die dynamische Speicherbelegung und -freigabe in sehr ähnlicher Weise wie malloc() und free() in C.

Da die dynamische Speicherverwaltung in C sehr oft verwendet wird und dem ergo auch in C++ so sein würde, entschlossen sich die Entwickler von C++, dies als Teil der Sprache selbst zu implementieren, anstatt diese Funktionalität einer Bibliothek zu überlassen. Die Operatoren new und delete sind also ein Teil der Programmiersprache, genauso wie etwa der Additionsoperator. Deshalb sind diese Operatoren sehr effizient und einfach in der Anwendung wie wir in diesem Beispielprogramm sehen werden.

In den Zeilen 15 und 16 verwenden wir Zeiger wie wir es in C immerzu getan haben, Zeile 17 illustriert die Verwendung des new Operators. Dieser Operator verlangt ein Argument, das ein Typ sein muß, wie hier illustriert. Der Zeiger Zeiger2 zeigt nun auf die Variable vom Typ int, für die wir dynamisch Speicherplatz bereitgestellt haben. Die Ganzzahl kann genauso verwendet werden, wie jede dynamisch bereitgestellte Variable in ANSI-C verwendet wird. Die Zeilen 19 und 20 zeigen die Ausgabe des Wertes, der der Variable in Zeile 18 zugewiesen wurde.

In Zeile 21 führen wir eine weitere Variable dynamisch ein und Zeile 22 läßt Zeiger2 auf dieselbe dynamisch bereitgestellte Variable zeigen wie Zeiger1. In diesem Fall geht jede Referenz auf die Variable, auf die Zeiger2 zuerst zeigte, verloren und diese Variable kann nicht wieder verwendet oder freigegeben werden. Sie ist verloren im Speicher bis unser Programm die Kontrolle wieder an das Betriebssystem übergibt und der Speicherplatz neu belegt wird. Dies ist offensichtlich kein guter Programmierstil. Beachte, daß Zeiger1 in Zeile 26 mit dem Operator delete wieder freigegeben wird und Zeiger2 nun nicht mehr freigegeben werden kann, da er auf nichts zeigt. Da der Zeiger Zeiger1 selbst nicht verändert wird, zeigt er noch immer auf dieselbe Speicheradresse. Möglicherweise könnten wir diese Adresse noch einmal verwenden, aber das wäre schrecklicher Programmierstil, da wir keine Garantie haben, was das System mit dem Zeiger oder den Daten macht. Die Speicheradresse wird an die Liste der freien Adressen zurückgegeben und wohl bald wieder von einem Programm verwendet werden.

Da der delete Operator der Definition nach nichts tut, wenn ihm der Wert NULL übergeben wird, kannst Du das System zwar anweisen, die Daten, auf die ein Zeiger mit dem Wert NULL zeigt, zu löschen, allein es wird nichts passieren. Das ist also nur unnötiger Code. Der delete Operator kann nur solche Daten freigeben, die mit dem new Operator angefordert wurden. Wird der delete Operator mit irgendeiner anderen Art von Daten verwendet, ist die Operation nicht definiert und es kann eigentlich alles passieren. Nach dem ANSI Standard ist auch ein Systemabsturz ein erlaubtes Ergebnis dieser undefinierten Aktion und kann vom Autor des Compilers als solches definiert werden.

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel Dynamik:

Der C++-Programmierer: C++ lernen - professionell anwenden - Lösungen nutzen
Eines der jedenfalls besseren Bücher zur Einführung in C++ ist umfassend und verständlich, übersichtlich und gelungen. Halbwegs hübsch anzuschauen ist es auch.
›› Mehr C++-Bücher

In Zeile 28 deklarieren wir einige Variablen vom Typ float. Du erinnerst Dich sicherlich, daß Du in C++ Variablen nicht am Anfang eines Blocks deklarieren mußt. Eine Deklaration ist eine ausführbare Anweisung und kann daher überall in einer Liste von solchen Anweisungen stehen. Eine der Variablen vom Typ float wird bei der Deklaration bereitgestellt, um zu zeigen, daß dies möglich ist. Wir wenden auf diese Variablen einige der Funktionen an, die wir zuerst auch auf die ganzzahligen Variablen verwendet haben.

In den Zeilen 36 bis 44 findest Du einige Beispiele für die Verwendung von Strukturen. Diese sollten eigentlich selbsterklärend sein.

Schließlich wirst Du Dich wundern, wie es möglich ist, einen Block von willkürlicher Größe festzulegen, wo doch der new Operator einen Typen als Argument verlangt, um die Größe des Blocks festzulegen. Um dies zu erreichen, verwenden wir das Konstrukt in Zeile 48. Hier belegen wir einen Speicher der Größe von 37 Variablen des Typs char, das heißt 37 Bytes. In Zeile 50 belegen wir einen Block, der um 133 Bytes größer ist als eine Datum Struktur. Dadurch wird klar, daß der new Operator mit all der Flexibilität von malloc() verwendet werden kann. Die eckigen Klammern in den Zeilen 49 und 51 sind notwendig, um dem Compiler zu sagen, daß er einen Array freigibt.

Zeiger auf Funktionen

Beispielprogramm: FUNKTZG.CPP

Das Programm FUNKTZG.CPP gibt ein Beispiel für die Verwendung eines Zeigers auf eine Funktion. Es muß gesagt werden, daß es sich hier um nichts Neues handelt, Zeiger auf eine Funktion sind in ANSI-C genauso verfügbar wie in C++ und funktionieren für beide Sprachen in derselben, hier beschriebenen Weise. In C Programmen wird dieses Konstrukt allerdings seltener verwendet, weshalb wir es hier erwähnen. Wenn Du in Zeigern auf Funktionen firm bist, kannst Du dieses Beispielprogramm getrost überspringen.

Das einzig Erwähnenswerte an diesem Programm ist der Zeiger auf eine Funktion, den wir in Zeile 7 kreieren. Wir deklarieren einen Zeiger auf eine Funktion, die nichts (void) zurückgibt und einen Parameter verlangt, eine Variable vom Typ float. Dir wird nicht entgangen sein, daß alle drei Funktionen, die wir in den Zeilen 4 bis 6 deklarieren, diese Vorgaben erfüllen und damit von diesem Zeiger aufgerufen werden können. Wenn Du in C keine Prototypen verwendet hast, werden Dir diese Zeilen etwas eigenartig vorkommen. Laß Dich aber jetzt nicht verwirren, wir werden auf Prototypen im nächsten Kapitel eingehen.

In Zeile 14 rufen wir die Funktion DruckeEtwas() mit dem Parameter Pi auf. In Zeile 15 weisen wir dem Funktionszeiger Funktionszeiger den Wert DruckeEtwas zu, um in Zeile 16 mittels des Funktionszeigers dieselbe Funktion noch einmal aufzurufen. Aufgrund der Zuweisung in Zeile 15 sind die Zeilen 14 und 16 also in ihrem Resultat absolut identisch. Die Zeilen 17 bis 22 zeigen noch einige weitere Beispiele für die Verwendung von Funktionszeigern. Schau Dir diese Beispiele in Ruhe an, ich lasse Dich mit ihnen alleine.

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel interessante Konstruktionen:

Clean Code - Refactoring, Patterns, Testen und Techniken für sauberen Code: Deutsche Ausgabe
Auch, wenn man nicht alles hier gut findet, macht Martins "Clean Code" einen unvermittelt (und unverhinderbar) zur besseren Programmiererin. Viele, viele Beispiele, ergo sehr, sehr lehrreich.
›› Mehr Softwareentwicklung-Bücher

Da wir einem Zeiger auf eine Funktion den Namen einer Funktion zuweisen konnten, ohne einen Zuweisungsfehler zu produzieren, muß der Name einer Funktion ein Zeiger auf genau diese Funktion sein. Exakt das ist auch der Fall. Ein Funktionsname ist nichts Anderes als ein Zeiger auf die Funktion, allerdings ein konstanter Zeiger und damit unveränderbar. Dasselbe ist uns auch beim Studium der Arrays in ANSI-C untergekommen. Der Name eines Array ist ein konstanter Zeiger auf das erste Element des Array.

Da es sich beim Namen einer Funktion um einen Zeiger auf die Funktion handelt, können wir diesen Namen einem Funktionszeiger zuweisen und den Funktionszeiger verwenden, um die Funktion aufzurufen. Die einzige Bedingung ist, daß der Rückgabewert sowie die Zahl und die Art der Parameter übereinstimmen müssen. Die meisten C und C++ Compiler werden Dich nicht warnen, wenn die Parameterlisten bei der Zuweisung nicht übereinstimmen. Das wäre auch gar nicht möglich, weil die Zuweisung zur Laufzeit erfolgt, wenn keine Typeninformationen verfügbar sind.

Kompiliere das Programm und führe es aus.

Programmieraufgaben

  1. Wenn Daten, für die der Speicherplatz dynamisch angefordert wurde, gelöscht werden, sind sie eigentlich noch immer im Speicher vorhanden. Wiederhole die Ausgabeanweisung in Zeile 24 und 25 vom Programm NEWDEL.CPP gleich nach dem delete in Zeile 25, um festzustellen, ob die Werte noch immer gespeichert sind. Wiederhole die Ausgabe noch einmal kurz vor Ende des Programmes, wenn die Daten schon überschrieben sein sollten, um zu sehen, was ausgegeben wird. Selbst wenn Du die richtigen Daten bekommst, ist es schrecklicher Programmierstil, sich darauf zu verlassen, daß die Daten nicht überschrieben wurden, was in einem größeren dynamischen Programm sehr wahrscheinlich ist.
  2. Schreibe eine neue Funktion für das Programm FUNKTZG.CPP, die als einzigen Parameter eine Variable vom Typ int verlangt und versuche, diese Funktion mittels des Funktionszeigers aufzurufen, um zu sehen, ob Du der Funktion die richtigen Daten übergeben kannst.

(weiter »)

[ des ]