Programmieren in C++: Einführung Boris Schäling Programmieren in C++: Einführung Boris Schäling Veröffentlicht 2010-02-03 Copyright © 2001-2010 Boris Schäling [mailto:[email protected]] Inhaltsverzeichnis Inhalt ......................................................................................................................... vi Voraussetzungen ......................................................................................................... vii 1. Einführung ............................................................................................................... 1 C++ heute ........................................................................................................... 1 Sprachabgrenzung ................................................................................................. 2 Vom Quellcode zum Maschinencode ........................................................................ 3 Bekannte Compiler ................................................................................................ 4 Mein erstes C++-Programm .................................................................................... 5 Ein- und Ausgabe mit C++ ..................................................................................... 5 Bestandteile der Programmiersprache C++ ................................................................ 7 2. Variablen ................................................................................................................. 8 Variablen als Informationsspeicher ........................................................................... 8 Variablen anlegen ................................................................................................. 8 Informationen in Variablen speichern ....................................................................... 9 Informationen zwischen Variablen austauschen .......................................................... 9 Intrinsische Datentypen ........................................................................................ 10 Der Datentyp char ............................................................................................... 14 Arrays ............................................................................................................... 15 3. Operatoren .............................................................................................................. 20 Verarbeiten von Daten in Variablen ........................................................................ 20 Arithmetische Operatoren ..................................................................................... 20 Logische Operatoren ............................................................................................ 21 Bitweise Operatoren ............................................................................................ 22 Vergleichsoperatoren ............................................................................................ 22 Kombinierte Zuweisungsoperatoren ........................................................................ 23 Inkrement- und Dekrement-Operator ....................................................................... 23 Präzedenz-Tabelle ............................................................................................... 24 Kontextabhängigkeit ............................................................................................ 26 Aufgaben ........................................................................................................... 27 4. Kontrollstrukturen .................................................................................................... 28 Verzweigungen ................................................................................................... 28 Schleifen ............................................................................................................ 32 Kontrolltransferanweisungen ................................................................................. 34 Aufgaben ........................................................................................................... 36 5. Funktionen ............................................................................................................. 37 Definition .......................................................................................................... 37 Aufruf ............................................................................................................... 38 Referenzvariablen ................................................................................................ 40 Gültigkeitsbereiche .............................................................................................. 42 Aufgaben ........................................................................................................... 43 6. Strukturen .............................................................................................................. 44 Allgemeines ....................................................................................................... 44 Definition .......................................................................................................... 44 Anwendung ........................................................................................................ 45 Enumerationen .................................................................................................... 47 Aufgaben ........................................................................................................... 48 7. Zeiger .................................................................................................................... 50 Allgemeines ....................................................................................................... 50 Variablen-Modell ................................................................................................ 50 Adressen und Zeiger ............................................................................................ 53 Praxis-Beispiele .................................................................................................. 57 Dynamische Speicherallokation .............................................................................. 59 Aufgaben ........................................................................................................... 63 8. Präprozessor ........................................................................................................... 64 Allgemeines ....................................................................................................... 64 iii Programmieren in C++: Einführung Symbolische Konstanten ....................................................................................... Bedingte Kompilierung ........................................................................................ Fehlermeldung .................................................................................................... Dateien einfügen ................................................................................................. Aufgaben ........................................................................................................... 9. Klassen und Objekte ................................................................................................ Allgemeines ....................................................................................................... Objektorientierte Programmierung .......................................................................... Praxis-Beispiel .................................................................................................... A. Übungen ................................................................................................................ Aufgaben ........................................................................................................... iv 64 65 68 68 69 70 70 70 71 74 74 Tabellenverzeichnis 2.1. Auszug aus der ASCII-Tabelle ................................................................................ 14 3.1. Präzedenz-Tabelle von C++ .................................................................................... 25 v Inhalt Was Sie lernen werden In diesem Buch lernen Sie die Programmiersprache C++ kennen. Ihnen werden Vorteile und Einsatzbereiche dieser Programmiersprache aufgezeigt. Sie lernen, wie Sie selber einfache C++-Anwendungen erstellen. Es wird auf die Bestandteile Variablen, Operatoren, Kontrollstrukturen, Funktionen, Strukturen und Zeiger von C++ und den Präprozessor eingegangen. Somit besitzen Sie ausreichend Basiswissen, um sich dann mit den objektorientierten Konzepten der Programmiersprache auseinanderzusetzen. Das letzte Kapitel enthält hierfür einen Ausblick auf Klassen und Objekte in C++. vi Voraussetzungen Was Sie wissen müssen Für das Verständnis dieses Buchs sollten Sie grundlegende Kenntnisse im Umgang mit Ihrem Betriebssystem besitzen, also beispielsweise problemlos Dateien erstellen und speichern und Anwendungen ausführen können. Allgemeine Kenntnisse zur Programmierung wie beispielsweise ein Überblick über den Aufbau von Programmiersprachen sind von Vorteil, aber nicht zwingend notwendig. Sie finden entsprechende Informationen im Buch Allgemeine Grundlagen der Programmierung [http:// www.highscore.de/grundlagen/]. vii Kapitel 1. Einführung C++ heute Leistungsfähigkeit und Flexibilität C++ stellt heute, zu Beginn des 21. Jahrhunderts, die wichtigste Programmiersprache für die Entwicklung leistungsfähiger Anwendungen dar. So ist C++ beispielsweise die am häufigsten verwendete Programmiersprache für die Entwicklung professioneller Anwendungen unter Microsoft Windows. Sehr viele weit verbreitete, erfolgreiche und unter Anwendern beliebte Programme sind in C++ entwickelt. Als Gründe für die Beliebtheit von C++ unter Software-Entwicklern können die hohe Ausführungsgeschwindigkeit, große Flexibilität und einfache Bewältigung komplexer Aufgabensituationen genannt werden. Auch die Kompatibilität zur Programmiersprache C spielt eine entscheidende Rolle. In den vergangenen zehn Jahren hat C++ durch Java und die .NET-Plattform starke Konkurrenz bekommen. Während bei Java eine hohe Portabilität im Vordergrund steht – also die Möglichkeit, ein Programm ohne Anpassung auf verschiedenen Betriebssystemen laufen zu lassen – ermöglicht die .NET-Plattform ein reibungsloses Zusammenspiel von Softwarekomponenten, die in völlig unterschiedlichen Programmiersprachen geschrieben sind. Die .NET-Plattform unterstützt zwar mit C++/ CLI eine modifizierte Version von C++, um C++-Entwicklern die .NET-Plattform schmackhaft zu machen. C++ und C++/CLI sind dennoch unterschiedliche Programmiersprachen mit unterschiedlichen Zielen. Ein wichtiger Bestandteil sowohl von Java als auch der .NET-Plattform und gleichzeitig ein entscheidender Unterschied zu C++ ist die virtuelle Maschine. Sie gaukelt einem Java- und .NET-Programm vor, auf einem in der Java- oder .NET-Spezifikation klar definierten Computer zu laufen. Javaund .NET-Programme laufen auf dieser virtuellen Maschine und greifen ausschließlich auf diese zu. Die virtuelle Maschine ist eine Zwischenschicht, die die Schnittstellen zum Betriebssystem und zur Hardware abstrahiert und quer über alle Computerkonfigurationen hinweg Java- und .NET-Programmen eine einheitliche Ausführungsumgebung zur Verfügung stellt. Java- und .NET-Programme wissen also grundsätzlich nicht, ob Ihr Computer einen Prozessor von Intel, AMD oder den eines anderen Herstellers besitzt. Sie müssen es auch nicht wissen, da sie lediglich auf der virtuellen Maschine laufen und nie direkt auf Ihren Prozessor zugreifen. C++-Programme haben direkten Zugriff auf den Prozessor und profitieren daher grundsätzlich von einer hohen Ausführungsgeschwindigkeit: Der direkte Zugriff auf einen Prozessor ist im Allgemeinen schneller als der Zugriff auf eine virtuelle Maschine, die ihrerseits auf den Prozessor zugreifen muss. Der direkte Zugriff auf den Prozessor ermöglicht es Entwicklern außerdem, ein C++-Programm für einen Prozessor zu optimieren und einen zusätzlichen Geschwindigkeitsvorteil zu erreichen. Software, bei der es auf eine herausragende Performance ankommt, wird daher üblicherweise in C++ entwickelt. C++ gilt als eine sehr flexible Programmiersprache, die mehrere Programmierparadigmen unterstützt. Unter Programmierparadigma versteht man eine bestimmte Sicht- und Herangehensweise an ein Problem. Programmierparadigmen sind Hilfsmittel, um Entwicklern das Lösen von Problemen zu erleichtern. Das bekannteste und heute am weitesten verbreitete Programmierparadigma ist die Objektorientierung. Sie ist auch das am häufigsten eingesetzte Programmierparadigma in C++. Dennoch können Sie auch ein C++-Programm ohne objektorientierte Konzepte erstellen. Die Objektorientierung wird Ihnen in C++ als ein Paradigma von vielen angeboten. So ist es mit C++ möglich, verschiedene Paradigmen in unterschiedlichen Komponenten der gleichen Software zu verwenden oder auch Paradigmen zu mischen. Weil kein Paradigma grundsätzlich besser oder schlechter ist als ein anderes und es von Fall zu Fall abhängt, welches Paradigma am sinnvollsten ist, bietet Ihnen C++ die freie Auswahl. Genaugenommen hängt die Auswahl eines Programmierparadigmas sogar vom Entwickler ab, da Entwickler sich bei gleicher Aufgabenstellung durchaus für unterschiedliche Paradigmen entscheiden können. 1 Einführung Neben der hohen Ausführungsgeschwindigkeit dank direktem Zugriff auf die Hardware und der Unterstützung verschiedener Programmierparadigmen zur Bewältigung unterschiedlicher Aufgabenstellungen ist die Kompatibilität zur Programmiersprache C von großer Bedeutung. C ist eine ältere Programmiersprache als C++, in der unglaublich viel Software entwickelt wurde und verfügbar ist. Darüberhinaus wird C als Systemprogrammiersprache zur Entwicklung zahlreicher Betriebssysteme inklusive Windows und Linux verwendet. Die hohe Kompatibilität zu C ermöglicht C++-Entwicklern den direkten Zugriff auf sämtlichen existierenden C-Code inklusive Betriebssystemfunktionen. Auf diese Weise können C++-Programme optimal mit ihrer Umgebung kommunizieren und in diese eingebettet werden. Sprachabgrenzung Was C++ ist und was nicht C++ ist für den Programmieranfänger einfach nur der Name einer neuen Programmiersprache. Hinter dem Kürzel C++ können sich jedoch recht unterschiedliche Dinge verbergen, die ganz klar getrennt werden sollten, um im allgemeinen Sprachgebrauch zu wissen, was der andere meint. Unter C++ versteht man erst einmal die Syntax und Semantik einer Programmiersprache. Das heißt, C ++ definiert eine Reihe von Schlüsselwörtern und legt fest, welche Bedeutung die einzelnen Schlüsselwörter haben. Dies ist die eigentliche Definition einer Programmiersprache. In diesem Buch werden Sie genau diesen Grundwortschatz kennenlernen, aus dem C++ besteht. Das zweite Buch dieser Reihe mit dem Titel Programmieren in C++: Aufbau [http://www.highscore.de/cpp/aufbau/] erklärt Ihnen dann, welche Regeln zu beachten sind, wenn Sie objektorientiert programmieren wollen. C++ als Programmiersprache wurde vor langer Zeit von Bjarne Stroustrup [http:// www.research.att.com/~bs/homepage.html] basierend auf der Programmiersprache C entwickelt und ist schon lange de facto standardisiert. Das heißt, Sie können Ihr C++-Programm normalerweise mit jedem beliebigen C++-Compiler übersetzen - egal, von welchem Hersteller der Compiler stammt, egal, unter welchem Betriebssystem er läuft. Der Grundwortschatz ist nicht compiler- oder betriebssystemabhängig. Seit 1998 gibt es einen offiziellen C++ Standard. Dieser Standard bezieht sich zum einen auf die Syntax und Semantik der Programmiersprache C++. Dies ist der eher uninteressantere Teil des Standards, weil hier eigentlich nur die bis dato bereits weit verbreitete und allgemeingültige Syntax von C++ von offizieller Seite bestätigt wurde. Es gibt zwar ein paar Neuheiten zum bisherigen de-facto-Standard, aber diese Erweiterungen halten sich in Grenzen und sind leicht überschaubar. Der viel wichtigere Teil des Standards ist die C++-Standardbibliothek. In dieser Bibliothek sind nützliche Funktionen, Klassen und Objekte zusammengefasst und standardisiert worden, um diese Hilfsmittel allen C++-Programmierern in einheitlicher Form zur Verfügung zu stellen. Es handelt sich also bei dieser Bibliothek um eine Art Werkzeugkasten, auf den jeder C++-Programmierer in seinen Programmen zugreifen kann. In diesem Werkzeugkasten liegen Werkzeuge, die alle mit Hilfe von C++ entwickelt wurden. Der Standard selbst legt nun fest, was für Werkzeuge im Werkzeugkasten enthalten sein müssen, welche Funktion diese Werkzeuge haben, wie sie angewandt werden etc. Sie als C++-Programmierer brauchen sich nicht der Standardbibliothek bedienen. Sie können Ihre Programme auch ohne diesen Werkzeugkasten erstellen. In der Praxis jedoch greift fast jedes C++Programm auf Elemente der Standardbibliothek zu. Eine Programmierung ohne die Standardbibliothek würde oftmals bedeuten, dass Funktionen selber programmiert werden müssten, obwohl sie in der Standardbibliothek definiert sind. So müssen zum Beispiel viele Programme Dateien öffnen und lesen – die Standardbibliothek bietet entsprechende Hilfsmittel an, die Sie wiederverwenden können, ohne Betriebssystemfunktionen aufrufen und viele kleine Schritte selber machen zu müssen. Da kein C++-Programmierer das Rad neu erfinden will, was zeitaufwändig und fehleranfällig wäre, finden Sie daher kaum ein C++-Programm, das nicht auf Funktionen, Klassen und Objekte aus der Standardbibliothek zugreift. Die Entwicklung der Standardbibliothek ist gleichzeitig ein Schritt in Richtung höherer Portabilität. Denn mit dieser Standardbibliothek kann auf jedem Betriebssystem auf ganz bestimmte Werkzeuge in 2 Einführung C++-Programmen zugegriffen werden. Anstatt zum Beispiel auf jedem Betriebssystem andere Funktionen aufzurufen, um Dateien zu öffnen und zu lesen, können Sie auf die Standardbibliothek zugreifen, die bereits für das jeweilige Betriebssystem angepasst ist und weiß, wie mit Hilfe welcher Betriebssystemfunktionen Dateien zu öffnen und zu lesen sind. Da der C++-Standard von einem internationalen Standardisierungsgremium überwacht und weiterentwickelt wird, wird verhindert, dass Unternehmen inkompatible Versionen von C++ entwickeln. Der C++-Standard hat heute eine so starke Stellung, dass er von keinem Unternehmen, das Entwicklungswerkzeuge zu C++ anbietet, ignoriert werden kann. Es kann daher erwartet werden, dass sich sämtliche Unternehmen nach dem C++-Standard richten. Da im Standardisierungsgremium viele unterschiedliche Interessen aufeinander treffen, schreitet die Entwicklung des C++-Standards aber nicht unbedingt schnell voran. Nachdem die erste Version 1998 veröffentlicht wurde, wird erwartet, dass die zweite Version 2010 verfügbar sein wird. Da die Entwicklung von C++ in der Praxis schneller voranschreitet als der Standardisierungsprozess, wurde unter anderem von Mitgliedern des Standardisierungsgremiums Boost [http://www.boost.org/] gegründet. Boost ist ein Tummelplatz im Internet von mehreren tausend C++-Entwicklern, die neue Bibliotheken entwickeln und gemeinschaftlich verbessern - mit dem Ziel, diese eines Tages in eine neue Version des C++-Standards zu integrieren. Die Entwicklung der Boost C++ Bibliotheken geht hierbei schneller voran als die Entwicklung des C++-Standards. Während der C++-Standard von 1998 zum Beispiel keinerlei Hilfsmittel zur Entwicklung von Netzwerkanwendungen bietet, gibt es eine entsprechende Netzwerk-Bibliothek bei Boost. Da sich Boost sehr stark am C++-Standard ausrichtet, sind die Boost C++ Bibliotheken im Allgemeinen auf einem sehr hohen Niveau. Da sie noch nicht standardisiert sind, können sie sich aber schneller ändern, was ein ständiges Lernen erfordert. In der Praxis sind die Boost C++ Bibliotheken ungemein wichtig, da der C++-Standard von 1998 zu alt ist, um für alle Aufgabenstellungen in der heutigen Softwareentwicklung bereits fertige Lösungen bieten zu können. Das Buch Die Boost C++ Bibliotheken [http://www.highscore.de/cpp/boost/] stellt verschiedene Boost C++ Bibliotheken vor, die in der heutigen Softwareentwicklung eigentlich unverzichtbar sind und wichtige Erweiterungen des C++-Standards darstellen. Vom Quellcode zum Maschinencode Kompilieren und Linken Um ein C++-Programm zu schreiben, wird der Quellcode einfach in einer Text-Datei (genauer ASCIIDatei) gespeichert. Ein einfacher Text-Editor, wie er beispielsweise standardmäßig unter Windows installiert ist, reicht zum Schreiben von C++ aus. Wie Sie eventuell aus dem Buch Allgemeine Grundlagen der Programmierung [http://www.highscore.de/grundlagen/] wissen, können vom Programmierer geschriebene Quellcodes aber nicht automatisch vom Computer ausgeführt werden. Während der Programmierer Quellcode schreiben und lesen kann, versteht der Computer nur Maschinencode. Das heißt, der Quellcode muss in Maschinencode übersetzt werden, damit das Programm vom Computer überhaupt ausgeführt werden kann. Eine Übersetzung von Quellcode in Maschinencode findet in C++ in zwei Schritten statt: Zuerst wird der Quellcode durch den Compiler in Objektcode übersetzt. Dann wird der Objektcode durch den Linker in eine ausführbare Datei umgewandelt. Im ersten Schritt übersetzt der Compiler den von Ihnen geschriebenen C++-Quellcode direkt in den äquivalenten Maschinencode. Der Compiler geht Schritt für Schritt durch den Quellcode und verwendet sozusagen ein Wörterbuch, um bestimmte Anweisungen in C++ durch fest definierten Maschinencode zu ersetzen. Die Datei, die vom Compiler nach getaner Arbeit erzeugt wird, heißt Objektdatei. Während C++-Quellcode normalerweise in Dateien mit der Endung cpp gespeichert wird, erhalten Objektdateien standardmäßig die Endung o oder obj. Nun kann es sein – und das ist in der Praxis normalerweise immer der Fall – dass Sie in Ihrem Quellcode auf Hilfsmittel zugreifen, die Sie nicht selber programmiert haben, sondern wiederverwenden. Sie rufen beispielsweise eine Funktion auf, die von einem anderen Programmierer entwickelt wurde, 3 Einführung und die Sie so nützlich finden, dass Sie sie in Ihrem Programm verwenden wollen. Dieser andere Programmierer muss Ihnen diese Funktion natürlich irgendwie zur Verfügung stellen. Dies geschieht normalerweise durch Bibliotheken. Eine Bibliothek ist nichts anderes als eine Datei, in der lauter Funktionen definiert sind. Eine Bibliothek muss hierbei nicht unbedingt als Quellcode vorliegen, sondern kann auch in Maschinencode zur Verfügung gestellt werden. Wenn Sie also die Funktion des anderen Programmierers aufrufen, kann der Compiler nicht den dazugehörigen Quellcode übersetzen, weil Sie ja gar nicht diese Funktion programmiert haben. Der Linker merkt nun, dass zu der besagten Funktion der Maschinencode fehlt, und kopiert den entsprechenden Maschinencode aus der Bibliothek in die Objektdatei hinein. Hat der Linker den Maschinencode für alle Funktionen kopiert, die nicht von Ihnen programmiert wurden und in Bibliotheken liegen, erhalten Sie eine ausführbare Datei. Diese Datei ist Ihr fertiges Programm in Maschinencode. Im Betriebssystem Windows erkennen Sie derartige Dateien an der Endung exe, unter Unix und Linux ist das eXecute-Flag gesetzt. Der Computer kann nun Ihr C++-Programm ausführen. Bekannte Compiler Microsoft, Borland, GCC, etc. Wie Sie bereits wissen benötigt man zum Erstellen eines C++-Programms immer Compiler und Linker. Im allgemeinen Sprachgebrauch wird normalerweise nur der Compiler erwähnt, weil jedem C+ +-Programmierer klar ist, dass es zu diesem Compiler auch einen Linker geben muss. Unter den Windows-Betriebssystemen sind Microsoft Visual C++ [http://msdn.microsoft.com/en-us/ visualc/] und Borland C++Builder [http://www.codegear.com/products/cppbuilder] die am weitesten verbreiteten C++-Compiler. Genaugenommen handelt es sich hierbei nicht nur um Compiler, sondern um komplette Entwicklungsumgebungen. Eine Entwicklungsumgebung stellt eine Anwendung dar, die für eine unkomplizierte, einfache und möglichst schnelle Programmierung optimiert ist. Ein wichtiger Bestandteil von Entwicklungsumgebungen ist selbstverständlich der Compiler. Daneben stellen Entwicklungsumgebungen aber noch eine Reihe nützlicher Funktionen bereit, mit denen sich beispielsweise Quellcode visuell darstellen lässt, um in größeren Projekten leichter die Übersicht zu behalten. Eine professionelle Entwicklung von C++-Anwendungen ist ohne Entwicklungsumgebungen kaum möglich. Der am weitesten verbreitete Compiler unter den Unix- und Linux-Betriebssystemen ist GCC [http:// gcc.gnu.org/]. GCC ist eigentlich ein ganzes Paket an Compilern für verschiedene Programmiersprachen. Um C++-Programme zu übersetzen, bedient man sich des Compilers g++ von GCC. Im Gegensatz zu den Programmen von Microsoft und Borland handelt es sich bei GCC und g++ tatsächlich nur um Compiler und nicht um Entwicklungsumgebungen. Während also bei Microsoft und Borland beispielsweise Kompilieren und Linken komfortabel über Menüs ausgeführt werden kann, werden die Compiler aus GCC über Kommandozeilenbefehle gestartet. Um das ständige Wiederholen von Befehlen und vor allem langen Parameterketten zu vermeiden, stehen Make-Dateien zur Verfügung, mit denen Quellcode anhand bestimmer Regeln automatisch übersetzt werden kann. Da Make-Dateien auch wieder zusätzliche Einarbeitungszeit durch den Programmierer benötigen, sind Entwicklungsumgebungen vor allem für den Programmieranfänger sicherlich hilfreicher. Hier kann voll und ganz auf das Erlernen der Sprache konzentriert werden anstatt sich mit Kommandozeilenbefehlen, Parametern und Make-Dateien herumzuschlagen. Neben oben vorgestellten C++-Compilern gibt es auch weniger bekannte wie beispielsweise Comeau C++ [http://www.comeaucomputing.com/]. Unter Windows ist jedoch vor allem die Microsoft-Entwicklungsumgebung, unter Unix und Linux GCC marktbeherrschend. GCC ist Open-Source-Software und steht seit eh und je kostenlos zur Verfügung. Sollten Sie mit Unix oder Linux arbeiten, empfiehlt sich die Verwendung von g++. Häufig wird GCC automatisch installiert und steht eventuell auf Ihrem System schon zur Verfügung. Mit dem Befehl g++ --version können Sie schnell überprüfen, ob g++ vorhanden ist und welche Version installiert ist. Wenn Sie mindestens die Version 4.0 installiert haben, sollten Sie sämtliche Beispiele aus diesem Buch kompilieren können. 4 Einführung Microsoft und Borland verkaufen ihre Entwicklungsumgebungen je nach Edition zu mehr oder weniger hohen Preisen. Beide bieten jedoch auch eine kostenlose Edition an, die geradezu ideal für C++Einsteiger ist. Microsoft nennt seine kostenlose Entwicklungsumgebung Visual C++ Express [http:// www.microsoft.com/express/vc/], bei Borland heißt sie Turbo C++ [http://www.turboexplorer.com/ cpp]. Alle Beispiele in diesem Buch sind mit der Version 2008 von Visual C++ Express getestet worden. Wenn Sie sichergehen wollen, dass die Beispiele bei Ihnen ohne Probleme funktionieren, können Sie ebenfalls Visual C++ Express installieren. Turbo C++ sollte normalerweise die Beispiele ebenfalls problemlos kompilieren, nur wurde das wie gesagt nicht explizit getestet. Mein erstes C++-Programm Hallo, Welt! Im Folgenden sehen Sie einen kurzen Quellcode - Ihr erstes C++-Programm! #include <iostream> int main() { std::cout << "Hallo, Welt!" << std::endl; } Geben Sie den Quellcode entweder in Ihre Entwicklungsumgebung ein oder speichern Sie ihn in einer Datei ab. Verwenden Sie eine Entwicklungsumgebung, führen Sie das C++-Programm über den entsprechenden Menüeintrag aus. In Visual C++ Express wäre das im Menü Erstellen der Eintrag Ausführen. Arbeiten Sie hingegen ohne Entwicklungsumgebung und möchten Ihren Quellcode, den Sie beispielsweise in der Datei hallowelt.cpp gespeichert haben, mit Hilfe von GCC in Maschinencode übersetzen, geben Sie folgendes ein: g++ -o hallowelt hallowelt.cpp. Anschließend können Sie mit ./hallowelt das Programm ausführen. Wenn Sie sich wundern, warum Sie weder mit Ihrer Entwicklungsumgebung noch mit GCC zwei Schritte - nämlich zum Kompilieren und Linken - ausführen müssen, dann liegt das daran, dass sowohl Ihre Entwicklungsumgebung als auch g++ beide Schritte automatisch direkt hintereinander ausführen. Ein- und Ausgabe mit C++ Grundlegende Informationen zur Ein- und Ausgabe Alle Beispielprogramme in diesem Buch geben Daten auf den Bildschirm aus und verlangen hin und wieder eine Eingabe des Anwenders. Die Ausgabe auf den Bildschirm ist notwendig, damit tatsächlich überprüft werden kann, ob die Programme wie gewünscht funktionieren. Ohne die Ausgabe auf den Bildschirm wäre eine Erfolgskontrolle nur schwer möglich. Wenn Sie obiges Beispielprogramm ausgeführt haben, bekamen Sie die Meldung Hallo, Welt! auf den Bildschirm ausgegeben. Sie greifen innerhalb dieses kleinen Programms bereits auf ein Objekt aus der C++-Standardbibliothek zu, nämlich auf std::cout. Es handelt sich hierbei um die Standardausgabe, die normalerweise mit dem Monitor verbunden ist. Die Standardausgabe ist ein Mechanismus, der von der C++-Standardbibliothek zur Verfügung gestellt wird, um Daten unkompliziert auf ein Ausgabemedium auszugeben. Die Standardausgabe std::cout ist für gewöhnlich mit dem Monitor verbunden. Beachten Sie die Formulierung: Die Standardausgabe std::cout ist nicht der Monitor, sondern mit dem Monitor verbunden. So ist es zum Beispiel möglich, die Standardausgabe mit einem anderen Ausgabegerät zu verbinden und umzuleiten. Vielleicht ist Ihnen aufgefallen, dass an verschiedenen Stellen das Kürzel std:: auftaucht. Es handelt sich hierbei um die Abkürzung von Standard. std ist ein sogenannter Namensraum. Aufgrund der 5 Einführung großen Verbreitung von C++ kann es sein, dass Programmierer beispielsweise gleichlautende Funktionen entwickeln. Um Überschneidungen zu vermeiden, wurden im offiziellen Standard Namensräume eingeführt: Jeder Entwickler sollte also in seinem Programm beispielsweise alle Funktionen einem Namensraum zuordnen. Beim Funktionsaufruf wird nun nicht mehr nur der Name der Funktion angegeben, sondern zusätzlich der Namensraum, in dem sich die Funktion befindet. Dass es jetzt zu Überschneidungen kommt, ist zwar nicht ausgeschlossen, aber unwahrscheinlicher: Denn jetzt müssen nicht nur Funktionsnamen gleichlauten, sondern auch noch Namensräume. Die Funktionen, Klassen und Objekte, die im C++-Standard definiert sind, sind alle dem Namensraum std zugeordnet. Um also auf Funktionen, Klassen und Objekte im C++-Standard korrekt zuzugreifen, wird jeweils std getrennt durch den Zugriffsoperator :: vorangestellt. Code-Zeilen, die mit einem # beginnen, sind sogenannte Präprozessor-Anweisungen. Sie werden in einem späteren Kapitel mehr über den Präprozessor erfahren. Da jedes Beispielprogramm in diesem Buch aber mindestens einen Präprozessor-Befehl #include enthält, soll dieser hier kurz vorgestellt werden. Mit #include wird der Inhalt einer Datei an die Stelle eingefügt, an der das #include steht. Diese Präprozessor-Anweisung wird üblicherweise verwendet, um Header-Dateien einzubinden. Header-Dateien enthalten Definitionen für Funktionen und andere Hilfsmittel, die Sie in Ihrem Programm einsetzen wollen. Ohne eine vorherige Definition wäre ein Einsatz fremder Hilfsmittel nicht möglich, da der Compiler nicht erkennen könnte, was für Hilfsmittel Sie eigentlich versuchen einzusetzen. So müssen Sie zum Beispiel immer die Header-Datei iostream mit #include einbinden, wenn Sie auf std::cout zugreifen wollen. Da std::cout nicht zum Grundwortschatz der Programmiersprache C++ gehört, würde der Compiler ohne eine vorherige Definition nicht verstehen, was dieses std::cout eigentlich sein soll. Indem Sie mit #include die Datei iostream einbinden, sieht der C++-Compiler die in dieser Datei enthaltene Definition und erkennt, dass die Verwendung von std::cout in Ihrem Programm korrekt ist. Im folgenden Programm sehen Sie nun zum ersten Mal eine Eingabe: #include <iostream> #include <string> int main() { std::string vorname; std::cin >> vorname; std::cout << "Sie heissen " << vorname << std::endl; } Die Eingabe - also Ihr Vorname - kann beliebig lang sein, darf aber keine Leerzeichen enthalten. Sie wird, wenn Sie Enter drücken, in der Variablen vorname gespeichert. Diese Variable basiert auf dem Datentyp std::string, der - wie am Namensraum std erkannt werden kann - zum C++-Standard gehört. So wie für die Verwendung von std::cout die Header-Datei iostream eingebunden werden musste, müssen Sie für std::string mit #include auf die Header-Datei string verweisen - diese Header-Datei enthält die Definition von std::string. Wenn Sie die Bedeutung der Variablen vorname nicht verstehen und nicht wissen, was ein Datentyp ist, macht das nichts Sie lernen Variablen und Datentypen im Kapitel 2, Variablen ausführlich kennen. std::cin ist das Gegenstück zu std::cout und ist mit der Tastatur verbunden. Über std::cin wird der Vorname gelesen und in der Variablen vorname gespeichert. In der letzten Zeile wird dann der Vorname auf den Monitor ausgegeben. Das obige Beispielprogramm wird nun um eine zweite Eingabe erweitert. #include <iostream> #include <string> int main() 6 Einführung { std::string vorname; std::cin >> vorname; std::string nachname; std::cin >> nachname; std::cout << "Sie heissen " << vorname << " " << nachname << std::endl; } Sie werden nun aufgefordert, nach dem Vornamen den Nachnamen einzugeben. So wie der Vorname wird auch der Nachname in einer Variablen vom Typ std::string gespeichert - was wiederum bedeutet, dass er beliebig lang sein kann, aber kein Leerzeichen enthalten darf. Nach den Eingaben werden dann sowohl der Vor- als auch der Nachname in einer Zeile ausgegeben. In den Beispielprogrammen in den folgenden Kapiteln werden Sie viele Ein- und Ausgaben sehen. So können Sie die Programme mit unterschiedlichen Eingaben füttern und überprüfen, welche Ergebnisse sie ausgegeben. Das hilft Ihnen nicht nur, besser zu verstehen, wie die Beispielprogramme im Detail funktionieren und wie sie Daten verarbeiten. Es ermöglicht Ihnen auch, eigene Programme auf Fehler hin zu überprüfen, da eine einfache Kontrolle Ihres Codes möglich ist. Bestandteile der Programmiersprache C++ Aufbau der Programmiersprache C++ besteht wie auch andere Programmiersprachen aus folgenden Bestandteilen: Variablen, Operatoren, Kontrollstrukturen und Funktionen. Darüberhinaus bietet C++ wie auch die Programmiersprache C Strukturen und Enumerationen an. Während C nun das Hauptaugenmerk auf Zeiger legt, stellt C+ + für bestimmte Situationen einfachere Hilfsmittel als Zeiger zur Verfügung. Ganz ohne Zeiger geht es in C++ aber auch nicht - deswegen wird in einem eigenen Kapitel auf Zeiger eingegangen. Der Schwerpunkt von C++ liegt aber in der Objektorientierung – ohne ein Verständnis von objektorientierten Konzepten ist es zum Beispiel unmöglich, voll und ganz von der C++-Standardbibliothek zu profitieren. Als eine Programmiersprache, die das Programmierparadigma der Objektorientierung unterstützt, bietet C++ daher das Arbeiten mit Klassen und Objekten an. Die objektorientierten Konzepte von C++ sind Thema des Buchs Programmieren in C++: Aufbau [http://www.highscore.de/ cpp/aufbau/]. Bis zu Objekten und Klassen ist es noch ein weiter Weg. Voraussetzung für das Verständnis dieser Konzepte ist ein sicherer Umgang mit dem Grundwortschatz von C++ wie eben Kontrollstrukturen und Funktionen. Da Computerprogramme Daten verarbeiten, müssen Sie aber erst lernen, wie Daten in C+ +-Programmen überhaupt gespeichert werden. Dazu schauen wir uns im folgenden Kapitel Variablen an und steigen mit ihnen in die Programmiersprache C++ ein. 7 Kapitel 2. Variablen Variablen als Informationsspeicher Maschinen als Analogie zu Computer-Programmen Der kleinste gemeinsame Nenner aller Computerprogramme ist: Sie verarbeiten Informationen. Ob es sich um einen einfachen Taschenrechner handelt oder um eine komplexe Steuerungssoftware für Flugzeuge – es geht immer um das Verarbeiten von Informationen. Informationsverarbeitung bedeutet im Kleinen, dass Informationen umgewandelt oder kombiniert werden und neue Informationen entstehen. Mit einem Taschenrechner können Sie zum Beispiel zwei Zahlen addieren und erhalten eine neue Zahl als Ergebnis. Wenn Sie sich die Eingabe von zwei Zahlen in einen Taschenrechner Schritt für Schritt vorstellen, stellen Sie fest, dass die erste Zahl, die eingegeben wird, vom Taschenrechner irgendwo gespeichert werden muss, während er auf die Eingabe der zweiten Zahl wartet. Würde die erste eingegebene Zahl nicht gespeichert werden, wüsste der Taschenrechner nach eingegebener zweiter Zahl nicht, was er eigentlich genau addieren soll. Hier kommen nun Variablen ins Spiel. Informationen werden in Programmen in Variablen gespeichert, um sie für eine spätere Verwendung im Programm bereitzuhalten. Der Taschenrechner legt die erste eingegebene Zahl also in genau so einer Variablen ab. Wenn die zweite Zahl eingegeben wurde, kann er auf die Variable zugreifen und die zweite Zahl mit der in der Variablen gespeicherten ersten Zahl addieren. Sie können sich Variablen recht bildlich als Töpfe vorstellen: Wenn Sie in Ihrem Programm eine Zahl speichern müssen, nehmen Sie einen Topf und legen die Zahl dort rein. Müssen Sie in Ihrem Programm eine zweite Zahl speichern, nehmen Sie einfach einen zweiten Topf. Sie können grundsätzlich so viele Variablen erstellen wie Sie brauchen und können beliebig Informationen in ihnen speichern, um sie im späteren Verlauf Ihres Programms abzurufen und zu verwenden. Variablen anlegen Variablen in der Praxis Um Informationen in Programmen speichern zu können - egal, ob diese in C++ oder einer anderen Programmiersprache geschrieben sind – benötigen sie spezielle Töpfe: Variablen. In C++ gehen Sie wie folgt vor, um eine Variable anzulegen, sprich um einen Topf herbeizuzaubern. int i; Mit dieser Zeile zaubern Sie einen Topf namens i herbei, in dem Sie eine Zahl aufbewahren können. Variablen werden immer nach dem gleichen Schema angelegt: Zuerst geben Sie den Datentyp an gefolgt von einem Variablennamen. Im obigen Beispiel ist int der Datentyp und i der Variablenname. In C++ ist jeder Variable ein ganz bestimmter Datentyp zugeordnet. Der Datentyp legt fest, welche Art von Information Sie in der entsprechenden Variable speichern können. So brauchen Sie in C++, um eine Zahl speichern zu können, eine Variable, die auch tatsächlich Zahlen speichern kann. Um eine Zeichenkombination, also beispielsweise ein Wort, speichern zu können, brauchen Sie hingegen eine Variable, die Zeichenkombinationen speichern kann. Variablen besitzen also in C++ einen Datentyp, der ganz genau festlegt, welche Art von Information aufgenommen werden kann. Sie können also zum Beispiel kein Wort in der oben angelegten Variablen i speichern, weil der Datentyp int nur Zahlen zulässt. 8 Variablen Am Ende einer Anweisung folgt in C++ immer ein Semikolon. Passen Sie auf, das Semikolon nicht zu vergessen, was vor allem Anfängern in C++ häufig passiert. Informationen in Variablen speichern Mit Variablen und Zahlen jonglieren Wie man Variablen anlegt, wissen Sie jetzt. Sie können nun jederzeit soviele Variablen anlegen, wie Sie möchten. int int int int i; j; eine_andere_Variable; NochEineVariable; Variablen werden benötigt, um Informationen zu speichern. Bisher haben Sie Variablen nur erstellt, ohne in ihnen Informationen zu speichern. Der Speichervorgang selbst sieht wie folgt aus. int i; i = 3; Nachdem Sie eine Variable vom Typ int angelegt haben, können Sie in dieser Variablen eine Information speichern. Die Art der Information muss natürlich zum Typ der Variablen passen. int-Variablen können Zahlen speichern. Also sollten Sie nicht versuchen, in Variablen dieses Datentyps Zeichenketten abzulegen. Wenn Sie es dennoch versuchen, wird sich der Compiler mit einer Fehlermeldung bei Ihnen beschweren. Um beispielsweise die Zahl 3 in der Variablen i zu speichern, geben Sie den Variablennamen i an, dann das Gleichheitszeichen, dann die Zahl 3. Vergessen Sie auch hier das Semikolon nicht am Ende der Anweisung. Achten Sie auch darauf, dass eine derartige Zuweisung immer von rechts nach links stattfindet: Rechts steht das, was Sie zuweisen bzw. speichern wollen. Links steht das, wohin gespeichert werden soll. Rechts steht die Quelle, links das Ziel. Achten Sie auch darauf, dass der Datentyp ausschließlich bei der Definition der Variable angegeben wird. Zugriffe auf die Variable wie eben die Zuweisung eines Wertes finden nur über den Variablennamen statt. Informationen zwischen Variablen austauschen Informationen kopieren Anstatt Zahlen einer int-Variablen zuzuweisen, können Sie auch den Wert einer Variablen einer anderen zuweisen. Oder anders ausgedrückt: Sie kopieren die Information in einer Variablen, indem Sie sie nochmal in einer zweiten Variablen speichern. int i = int j = i; 3; j; i; Zuerst legen Sie wieder eine Variable i vom Typ int an. Sie weisen dieser Variablen in einem zweiten Schritt eine Information zu, nämlich die Zahl 3. Danach legen Sie eine zweite Variable vom Typ int an. Anstatt dieser neuen Variablen j wieder eine Zahl zuzuweisen, verwenden Sie auf der rechten Seite des Gleichheitszeichens die Variable i. Da in i die Zahl 3 gespeichert ist, ist nach dieser Zuweisung nun auch in der Variablen j die Zahl 3 gespeichert. 9 Variablen Wenn Sie die Werte zweier Variablen austauschen wollen, müssen Sie sich einer dritten Variable bedienen. Mal angenommen, Ihr Programm sieht wie folgt aus. int i = int j = i; 3; j; 5; Sie möchten nun, dass Ihr Programm den Wert der Variablen i in j speichert und dass umgekehrt der Wert von j in i gespeichert wird. Eine direkte Zuweisung von einer Variablen an die andere ist nicht möglich. Denn damit würde eine Zahl verloren gehen. Eine Lösung könnte wie folgt aussehen. int i; i = 3; int j; j = 5; int zwischenspeicher; zwischenspeicher = i; i = j; j = zwischenspeicher; In einer zusätzlichen Variablen namens zwischenspeicher, die natürlich auch vom Typ int ist, wird der Wert von i gespeichert. Danach kann der Wert von j der Variablen i zugewiesen werden. j und i enthalten nun den gleichen Wert. Glücklicherweise steht in der Variablen zwischenspeicher der vorherige Wert von i noch zur Verfügung. Und der wird nun in einem letzten Schritt nach j kopiert. Der Austausch der beiden Werte ist perfekt: i enthält nun die Zahl 5 und j die Zahl 3. Da der Austausch von Werten in zwei Variablen eine häufigere Aufgabe in Programmen darstellen kann, stellt hier die C++-Standardbibliothek eine Funktion zur Verfügung, die diese Aufgabe übernimmt. #include <iostream> #include <algorithm> int main() { int i = 3; int j = 5; std::swap(i, j); std::cout << "i: " << i << ", j: " << j << std::endl; } Die Funktion std::swap tauscht einfach die Werte der Variablen, die als Parameter an die Funktion übergeben werden. Wenn Sie sich über das zusätzliche #include wundern, mit dem die Header-Datei algorithm eingebunden wird: In dieser Header-Datei befindet sich die Definition von std::swap. Intrinsische Datentypen In C++ eingebaute Datentypen Sie haben bis jetzt lediglich den Datentyp int kennengelernt. Dieser Datentyp eignet sich hervorragend, um Zahlen zu speichern. Bei int handelt es sich um einen intrinsischen Datentyp. Intrinsische Datentypen sind Datentypen, die Sie ohne vorherige Bekanntmachung in Ihrem C++-Programm verwenden können. Das heißt, wenn Sie int schreiben, weiß C++ automatisch, was Sie meinen. int gehört also zum Grundwortschatz von C++. Eine andere Bezeichnung für intrinsische Datentypen lautet primitive Datentypen. 10 Variablen Neben int bietet C++ noch ein paar weitere intrinsische Datentypen. Zu diesen zählen unter anderem short, char und float. Sie werden die intrinsischen Datentypen im Folgenden noch genauer kennenlernen. Wenn es intrinsische Datentypen gibt, gibt es natürlich auch nicht-intrinsische Datentypen. Nichtintrinsische Datentypen sind Datentypen, die Sie nicht ohne weiteres in C++ verwenden können. Denn diese Datentypen kennt C++ nicht. Sie gehören nicht zum Grundwortschatz – genau deswegen sind sie ja nicht-intrinsisch. Um nicht-intrinsische Datentypen in C++ verwenden zu können, müssen Sie sie vor ihrer Verwendung bekannt machen. Sie müssen wissen, wie nicht-intrinsische Datentypen bekannt gemacht werden, da Sie als C++-Programmierer später fast nur mit nicht-intrinsischen Datentypen arbeiten. Die von C++ bereitgestellten primitiven Datentypen sind zwar wichtig, aber im sprichwörtlichen Sinne primitiv. Der vielleicht am häufigsten eingesetzte nicht-intrinsische Datentyp ist std::string. Strings sind Variablen, die Zeichenkombinationen bzw. Zeichenketten speichern können. Denn C++ selber stellt keinen Datentypen zur Verfügung, um einfach mit Zeichenkombinationen arbeiten zu können. Um eine Variable vom Typ std::string anzulegen, gehen Sie wie folgt vor. #include <string> std::string s; Der Vorgang der Variablendefinition sollte Ihnen bekannt vorkommen: Zuerst der Datentyp std::string, dann getrennt durch ein Leerzeichen der Name der Variable - in diesem Fall also s. Da der Datentyp std::string nicht-intrinsisch ist, muss er bekannt gemacht werden. Dies erfolgt über die Header-Datei string, die mit #include eingebunden wird. Der Datentyp std::string ist Bestandteil der C++-Standardbibliothek. Daher gehört er auch zum Namensraum std und wird über std:: angesprochen. Dieser Datentyp ist enorm wichtig, da erst über ihn ein komfortables Speichern von Zeichenketten - also mehreren Buchstaben, Wörtern oder Sätzen - möglich ist. Sie finden kaum ein Programm in der Praxis, das std::string nicht verwendet. char c; short s; int i; long l; float f; double d; bool b; Oben sehen Sie alle gültigen Datentypen in C++, die Sie ohne Deklaration verwenden können. Dies sind die intrinsischen Datentypen in C++ - also die Datentypen, die C++ automatisch erkennt. Die intrinsischen Datentypen unterscheiden sich darin, dass sie jeweils unterschiedlich viel Speicherplatz reservieren. Ein char belegt 1 Byte, ein short reserviert 2 Bytes, ein int 4 Bytes und ein long ebenfalls 4 Bytes. Das trifft jedenfalls auf 32-Bit-Prozessoren zu. Auf 64-Bit-Prozessoren haben die Datentypen eventuell andere Speicherdimensionen. Ein long belegt auf 64-Bit-Prozessoren also beispielsweise 8 Bytes. float und double sind Datentypen, die Fließkommazahlen speichern können. Während die eben genannten Datentypen ausschließlich Ganzzahlen speichern können, sind die beiden Datentypen float und double auf Fließkommazahlen ausgelegt. Ein float speichert hierbei eine Fließkommazahl in 4 Bytes, ein double in 8 Bytes. Wie Sie eventuell aus dem Buch Allgemeine Grundlagen der Programmierung [http:// www.highscore.de/grundlagen/] wissen, besteht 1 Byte aus 8 Bits. Bits sind die kleinsten Informationseinheiten im Computer, die entweder auf den Wert 0 oder auf den Wert 1 gesetzt sind. Bits werden in der Mathematik durch das binäre Zahlensystem repräsentiert. 11 Variablen Sie wissen nun, dass jeder Datentyp in C++ eine bestimmte Speichergröße besitzt. Sie können nun jeweils errechnen, was jeweils in welchem Datentyp für Zahlen gespeichert werden können. Ein char besteht aus 1 Byte - das heißt, es können bis zu 28, also 256 unterschiedliche Informationen, sprich 256 unterschiedliche Zahlen in einem char gespeichert werden. Nachdem ein short aus 2 Bytes besteht, ist klar, dass in diesem Datentyp die Bandbreite der möglichen zu speichernden Zahlen größer ist. Es stehen 16 Bits zur Verfügung, daher können bis zu 65.536 Zahlen in einem short gespeichert werden. Die Bandbreite für int liegt dementsprechend bei 4.294.967.296 Werten - wie auch für long, da auch dieser Datentyp auf 32-Bit-Prozessoren aus 4 Bytes besteht. Eines wurde bisher aber noch nicht berücksichtigt: Die bisher kennengelernten Datentypen speichern sowohl positive als auch negative Zahlen. Das Vorzeichen wird hierbei jeweils durch ein Bit repräsentiert. Wenn wir also nun nochmal char betrachten: Es stehen nicht 8 Bits zur Verfügung, sondern eigentlich nur 7 Bits. Das erste Bit steht für das Vorzeichen, wobei ein nicht gesetztes Bit positiv bedeutet, ein gesetztes Bit negativ. Wenn also ein Bit für die Speicherung einer Zahl verloren geht und dieses Bit stattdessen das Vorzeichen darstellt, verschiebt sich die Bandbreite in den negativen Bereich. Ein char kann 256 verschiedene Informationen speichern, also zum Beispiel alle Zahlen zwischen 0 und einschließlich 255. Wenn nun ein Bit für die Speicherung einer Zahl verloren geht und stattdessen das Vorzeichen darstellt, heißt das, dass ein char nun Zahlen von -128 bis +127 speichern kann. Was für char gilt, gilt natürlich auch für die anderen intrinsischen Datentypen: Ein short kann 65.536 verschiedene Informationen speichern, nämlich Zahlen von -32.768 bis +32.767. Ein int sowie ein long kann Zahlen zwischen -2.147.483.648 und +2.147.483.647 speichern. unsigned unsigned unsigned unsigned char c; short s; int i; long l; Durch das Voranstellen des Schlüsselwortes unsigned kann die Bedeutung des Vorzeichen-Bits aufgehoben werden. Die Bandbreite der entsprechenden Variablen verschiebt sich daraufhin in den positiven Bereich: Ein char kann dann Zahlen von 0 bis 255 speichern, ein short Zahlen von 0 bis 65.535 und ein int wie auch ein long Zahlen von 0 bis 4.294.967.295. In der Praxis hat unsigned eine geringe Bedeutung und wird wenig verwendet. Es ist ein Überbleibsel aus der Zeit, als Speicherplatz noch sehr kostbar war und es notwendig war, Einfluss darauf zu nehmen, ob das Vorzeichen-Bit tatsächlich das Vorzeichen speichern soll oder besser als zusätzliche Speicherposition für Zahlen zur Verfügung steht. Lediglich für Variablen, für die negative Werte partout keinen Sinn machen, kann dies explizit mit unsigned verdeutlicht werden. In der Praxis greift der C++-Programmierer, wenn er Zahlen speichern möchte, fast immer auf den Datentyp int zu. Dieser Datentyp bietet eine Bandbreite, die normalerweise ausreicht und keine Probleme verursachen sollte. Dass in der Variablen eventuell nur kleine Zahlen gespeichert werden, die auch in Variablen vom Datentyp short oder char passen würden, und damit Speicherplatz verschwendet wird, kümmert den C++-Programmierer normalerweise nicht. Es gibt genügend andere Probleme, über die man sich den Kopf zerbrechen muss, so dass man zum Speichern von Zahlen fast standardmäßig auf den Datentyp int zugreift. float f; double d; f = 1.2; d = 1e10; Die Datentypen float und double ermöglichen ebenfalls die Speicherung von Zahlen - und zwar von Kommazahlen. Auch wenn für Sie als Programmierer alle Zahlen einfach nur Zahlen sind, unterscheidet C++ nicht nur unterschiedliche Bandbreiten, sondern auch Ganz- und Kommazahlen. Wollen Sie in einer Variablen eine Kommazahl speichern, dann muss diese Variable vom Typ float oder double sein. Diese beiden Typen unterscheiden sich darin, dass float Kommazahlen mit einfacher Genauig- 12 Variablen keit, double Kommazahlen mit doppelter Genauigkeit speichert. Neben der Genauigkeit besitzen die Datentypen auch unterschiedliche Bandbreiten: float kann Zahlen von ±3.4*1038, double Zahlen von ±1.7*10308 aufnehmen. Beim Angeben von Kommazahlen innerhalb von Quellcode - beispielsweise bei einer Zuweisung an eine Variable - dürfen Sie kein Komma angeben, sondern müssen den Punkt als Trennzeichen verwenden. #include <iostream> int main() { float f, g; f = 1000.43; g = 1000; std::cout << f - g << std::endl; } Was heißt einfache und doppelte Genauigkeit? Genauigkeit bedeutet, wie genau ein Bruch bzw. der Nachkommaanteil in einer Variablen von einem bestimmten Datentyp gespeichert werden kann. Während Zahlen analog sind und es zwischen zwei Zahlen unendlich viele andere Zahlen gibt, arbeitet der Computer digital: Es können nur ganz konkrete Zustände gespeichert werden. Es können auch nur ganz konkrete Zahlen gespeichert werden. Führen Sie obiges Programm einfach aus und sehen Sie sich an, welches Ergebnis Ihr Computer ausgibt. Die Zahl 0.43, also das Ergebnis der Berechnung im Beispiel, kann im Datentyp float nicht genau gespeichert werden - der Computer verwendet eine Annäherung. Wenn Sie anstatt dem Datentyp float den Datentyp double im Beispielprogramm verwenden, wird das korrekte Ergebnis ausgegeben. Der Datentyp double besitzt doppelte Genauigkeit. Insofern ist in diesem Fall keine Annährung notwendig, sondern das korrekte Ergebnis kann direkt gespeichert werden. Wenn Sie jemals in einem Programm mit Kommazahlen arbeiten, denken Sie daran, dass die Speicherung von Kommazahlen zum Teil nur durch Annäherung erfolgt. Sie müssen speziell für das Arbeiten mit Kommazahlen entwickelte Bibliotheken verwenden, wenn Berechnungen durch die nicht exakte Speicherung von Kommazahlen nicht zu mathematisch falschen Ergebnissen führen dürfen. bool b; b = true; b = false; Der intrinsische Datentyp bool unterscheidet sich von den bisherigen Datentypen insofern, als dass hier nicht seine physikalische Speichergröße im Vordergrund steht, sondern sein logisches Speichervermögen. Ein bool kann den Wert true und den Wert false speichern. Wieviel Speicher ein bool belegt, spielt also für den Programmierer eine untergeordnete Rolle. Wichtig ist, dass ein bool genau zwei Informationszustände unterscheidet, nämlich true und false. Im folgenden Beispiel soll Ihnen die Anwendung der verschiedenen Datentypen nähergebracht werden. unsigned char alter; unsigned int kopfhaare; float kontostand; bool lichtschalter; Wenn Sie in Ihrem Programm das Alter einer Person speichern wollen, würde sich der Datentyp unsigned char empfehlen: Das Alter kann nicht negativ sein, und älter als 255 Jahre dürfte bei weitem niemand werden. 13 Variablen Um die Anzahl an Kopfhaaren zu speichern, müsste bereits auf den Datentypen int zugegriffen werden. Da man nicht weniger als 0 Kopfhaare haben kann, könnte ebenfalls unsigned verwendet werden. Der nächstkleinere Datentyp short kann hingegen nicht verwendet werden: Die Obergrenze von short liegt bei 65.536 – bei bis zu 150.000 Haaren, die man auf dem Kopf haben kann, ist das nicht ausreichend. Um einen Kontostand zu speichern, bietet sich float an. In einer float-Variable können positive wie auch negative Kommazahlen gespeichert werden. Das einzige Problem mit float ist, dass Berechnungen wie bereits gezeigt unter Umständen nicht die tatsächlich erwarteten Ergebnisse liefern, sondern Annäherungen. Bei einem Lichtschalter wiederum bietet sich der Datentyp bool an. Der Lichtschalter ist entweder an oder aus – die zwei Zustände lassen sich ideal in einer bool-Variablen speichern. Wie in diesem Abschnitt bereits angesprochen wird oft auch einfach nur der Datentyp int verwendet, wenn Ganzzahlen gespeichert werden müssen. Niemand kann zwar mehrere Millionen Jahre alt werden, und ein negatives Alter gibt es auch nicht - trotzdem werden Sie in der Praxis oft einfach nur die Verwendung von int sehen, auch wenn ein anderer Datentyp scheinbar geeigneter wäre. Der Datentyp char Buchstaben als Zahlen Der Datentyp char kann wie Sie bereits wissen bis zu 256 unterschiedliche Zahlen speichern. char wird von C++-Programmierern jedoch normalerweise nicht verwendet, um Zahlen zu speichern, sondern Buchstaben. Der Datentyp std::string, der Zeichenketten speichert (also mehrere Buchstaben), basiert intern auf dem Datentyp char. Da stellt sich die Frage, wie in char Buchstaben gespeichert werden können. Buchstaben werden intern im Computer durch Zahlen repräsentiert. Das heißt, jedem Buchstaben oder genauer jedem Zeichen steht eine ganz bestimmte Zahl gegenüber. Das Auflösen von Buchstaben in die entsprechende Zahl geschieht mit Hilfe von Tabellen. Es gibt eine ganze Reihe an Tabellen, die angeben, wie Buchstaben in Zahlen aufgelöst werden und umgekehrt. Die wichtigste Tabelle ist der ASCII-Code. Denn jedes Betriebssystem kennt diese Tabelle, um Buchstaben in Zahlen umzuwandeln und zurück. Im Folgenden sehen Sie einen Auszug aus der ASCII-Tabelle. Tabelle 2.1. Auszug aus der ASCII-Tabelle ASCII-Code Zeichen ... 58 : 59 ; 60 < 61 = 62 > 63 ? 64 @ 65 A 66 B 67 C ... Der ASCII-Code definiert 128 Zeichen bzw. in seiner erweiterten Form 256 Zeichen. Jedem dieser Zeichen ist eine Zahl zugeordnet. Wenn Sie sich erinnern: Ein char kann genau 256 unterschiedliche 14 Variablen Zahlen speichern. Ein char stellt also von seiner Speichergröße her den idealen Datentyp dar, um Zeichen aus dem ASCII-Code zu repräsentieren. Wenn Sie in C++ den Datentyp char verwenden, geht C++ automatisch davon aus, dass Sie keine Zahl speichern möchten, sondern einen Buchstaben. Führen Sie einfach folgendes Beispielprogramm aus. #include <iostream> int main() { char c; c = 65; std::cout << "c: " << c << std::endl; } Ihnen wird nicht die Zahl 65 auf den Bildschirm ausgegeben, sondern der Buchstabe A. Was ist passiert? C++ geht davon aus, dass Sie in Variablen des Datentyps char Buchstaben speichern. Das heißt, C++ hat den Wert in der Variablen c als ASCII-Code interpretiert. Gemäß der ASCII-Tabelle entspricht die Zahl 65 dem Großbuchstaben A. Deswegen hat das Programm Ihnen auch A auf den Bildschirm ausgegeben. char c; c = 'A'; Anstatt Buchstaben umständlich über den ASCII-Code zu speichern, können Sie den Buchstaben natürlich auch direkt angeben. Dazu müssen Sie das Zeichen jedoch in einfache Anführungszeichen setzen. Möchten Sie Ihr C++-Programm dazu zwingen, den Wert in der Variablen c nicht als Buchstabe, sondern als Zahl zu interpretieren, schreiben Sie folgendes. #include <iostream> int main() { char c; c = 65; std::cout << "c: " << static_cast<int>(c) << std::endl; } Mit Hilfe von static_cast wird die Variable c temporär in den Datentyp int umgewandelt. Wie Sie bereits erfahren haben ist der Datentyp int der Datentyp, der oft standardmäßig für die Speicherung von Zahlen verwendet wird. Ein int wird in C++ daher auch niemals als Buchstabe interpretiert, sondern immer als Zahl. Wenn Sie sich fragen, wo static_cast herkommt: Es handelt sich hierbei um einen sogenannten Cast-Operator, der zum Grundwortschatz von C++ gehört. Deswegen müssen Sie ihn auch nicht zuerst über eine Header-Datei bekannt machen, sondern können ihn direkt verwenden - der Compiler weiß, was Sie meinen. Arrays Ein weites Feld Ein Array ist ein Datentopf, der genaugenommen aus mehreren Variablen vom gleichen Typ besteht. Die einzelnen Variablen in diesem Datentopf werden über Indizes angesprochen - der Variablenname bezieht sich auf das gesamte Array. Eine andere Bezeichnung für Array ist Datenfeld. 15 Variablen Arrays werden typischerweise dann verwendet, wenn viele Informationen vom gleichen Typ gespeichert werden müssen. Stellen Sie sich vor, Sie schreiben eine Adressverwaltung und müssen die Namen von 100 Personen speichern. Bevor Sie nun anfangen, 100 Variablen zu definieren, können Sie ein Array definieren, das aus 100 Stellen besteht. int i[3]; Um ein Array anzulegen wird einfach hinter dem Variablennamen in eckigen Klammern die Größe des Datenfeldes angegeben. Obiges Datenfeld besteht also genaugenommen aus drei Variablen vom Typ int. Die drei Elemente im Array i werden jedoch nicht über einen Namen angesprochen - der Variablenname i bezieht sich ja auf das gesamte Array. Um auf einzelne Elemente in diesem Array zuzugreifen, muss mit einem Index gearbeitet werden. int i[3]; i[0] = 10; i[1] = 15; i[2] = 20; Die erste Variable sprich die erste Stelle in einem Array besitzt immer den Index 0 - der Index startet in C++ immer bei 0. Um auf das letzte Element im Array zuzugreifen, wird der Index Feldgröße minus 1 verwendet. Wenn das Array wie oben also 3 Stellen umfasst, besitzt die erste Stelle den Index 0, die zweite Stelle den Index 1 und die dritte und letzte Stelle den Index 2. Das Arbeiten mit Arrays kann brandgefährlich sein. Es ist Aufgabe des Programmierers, dafür zu sorgen, dass verwendete Indizes tatsächlich gültig sind. Wenn Ihr Programm fehlerhaft arbeitet und einen Index verwendet, der über die Feldgröße hinausgeht, kann dies zu unvorhersehbaren Folgen wie beispielsweise einem Programmabsturz führen. Das Arbeiten mit Arrays erfordert also besondere Aufmerksamkeit. Im Zusammenhang mit dem Datentyp char kann man ganz besondere Arrays erstellen. Sie erinnern sich sicher noch daran, dass der Datentyp char vorrangig verwendet wird, um Buchstaben zu speichern. Wenn Sie nun ein Array vom Typ char erstellen, sollten sich mehrere Buchstaben hintereinander speichern lassen. char c[5]; c[0] c[1] c[2] c[3] c[4] = = = = = 'H'; 'a'; 'l'; 'l'; 'o'; Im obigen Beispiel greifen Sie über den Index auf die einzelnen Stellen im Array c zu und weisen verschiedene Buchstaben zu. Wie Sie unschwer erkennen können bilden die Buchstaben im Array das Wort Hallo. #include <iostream> int main() { char c[6]; c[0] = 'H'; c[1] = 'a'; c[2] = 'l'; c[3] = 'l'; c[4] = 'o'; c[5] = '\0'; std::cout << c << std::endl; 16 Variablen } Obiges Programm gibt das Wort Hallo auf den Bildschirm aus. Dazu wird das Array c über << an std::cout weitergegeben. Damit std::cout weiß, wann die Zeichenkette eigentlich aufhört, wird ein merkwürdiges Zeichen benötigt, das Null-Zeichen heißt. Null-Zeichen schließen für gewöhnlich Zeichenketten ab, die in Arrays vom Typ char gespeichert sind. Wenn Sie das Null-Zeichen nicht angeben, greift std::cout auf die nächste Stelle zu, die sich hinter dem Array befindet. std::cout überprüft also nicht, ob es über das Array hinaus auf Speicher zugreift, der gar nicht mehr zum Array gehört, und somit einen ungültigen Index verwendet. std::cout gibt alles auf den Bildschirm aus, bis es auf das Null-Zeichen trifft. Das Null-Zeichen befindet sich in der ASCII-Tabelle an allererster Stelle. Sie können also statt der Angabe des Zeichens in einfachen Anführungszeichen auch einfach die entsprechende Zahl aus der ASCII-Tabelle angeben - in diesem Fall also 0. Genau daher hat das Null-Zeichen seinen Namen. #include <iostream> int main() { char c[6]; c[0] = 'H'; c[1] = 'a'; c[2] = 'l'; c[3] = 'l'; c[4] = 'o'; c[5] = 0; std::cout << c << std::endl; } Die Arrays, die Sie bis jetzt kennen, sind eindimensionale Arrays. Das heisst, das Array besteht aus genau einer Aneinanderreihung mehrerer Variablen. char c[2][6]; c[0][0] c[0][1] c[0][2] c[0][3] c[0][4] c[0][5] c[1][0] c[1][1] c[1][2] c[1][3] c[1][4] = = = = = = = = = = = 'H'; 'a'; 'l'; 'l'; 'o'; '\0'; 'W'; 'e'; 'l'; 't'; '\0'; Dieses Array c ist nun zweidimensional. Es besteht aus genau zwei Aneinanderreihungen von jeweils sechs Variablen. Während Sie sich eindimensionale Arrays auch einfach als Zeile vorstellen können, besitzt ein zweidimensionales Array die Form einer Tabelle. Um auf einzelne Stellen in diesem zweidimensionalen Array zuzugreifen, müssen nun auch jeweils zwei Indizes angegeben werden. Im obigen Beispiel werden die Wörter Hallo und Welt zugewiesen, jeweils wieder abgeschlossen mit dem Null-Zeichen. Achten Sie auch darauf, dass das Wort Welt aus einem Buchstaben weniger besteht als das Wort Hallo, so dass das Null-Zeichen eine Stelle früher gesetzt ist. Dies ist kein Problem - Hauptsache, das Null-Zeichen ist überhaupt gesetzt. #include <iostream> 17 Variablen int main() { char c[2][6]; c[0][0] = c[0][1] = c[0][2] = c[0][3] = c[0][4] = c[0][5] = c[1][0] = c[1][1] = c[1][2] = c[1][3] = c[1][4] = std::cout 'H'; 'a'; 'l'; 'l'; 'o'; '\0'; 'W'; 'e'; 'l'; 't'; '\0'; << c[0] << " " << c[1] << std::endl; } Um die beiden Wörter auf den Bildschirm auszugeben, müssen jeweils eindimensionale Arrays an std::cout übergeben werden. Dies erfolgt wie im Beispiel angegeben. Wenn es zweidimensionale Arrays gibt, gibt es auch dreidimensionale Arrays. Genaugenommen kann ein Array beliebig viele Dimensionen besitzen - wobei sich natürlich irgendwann die Frage stellt, wie übersichtlich das Array noch ist. Auch hört die bildliche Vorstellungskraft spätestens beim vierdimensionalen Array auf. Mehrdimensionale Arrays werden aber analog zu zweidimensionalen Arrays erstellt: Es wird für jede neue Dimension eine Zahl zwischen eckigen Klammern angegeben. Auch der Zugriff auf ein Element im Array erfolgt über Angabe von Indizes für jede Dimension. #include <iostream> int main() { char c[10]; std::cout << "Groesse des Arrays: " << sizeof(c) << std::endl; } Um die Größe eines Arrays zu ermitteln, steht der Operator sizeof zur Verfügung. Dieser Operator errechnet die Speichergröße eines Arrays. Es kommt hierbei auf die Anzahl der Elemente im Array und den Datentypen des Arrays an. Das Array c besteht aus 10 Elementen vom Datentyp char. char belegt 1 Byte im Speicher - somit liefert sizeof im obigen Beispiel 1 * 10, also 10 zurück. Es spielt für den Operator sizeof keine Rolle, ob und wenn ja welche Werte im Array stehen. Auch das Null-Zeichen ist völlig egal. sizeof ermittelt die Speichergröße eines Arrays, und die ist unabhängig von den gespeicherten Werten. sizeof wird häufig dann verwendet, wenn eine Funktion auf ein Array zugreift und in einem Parameter die Größe des Arrays erwartet. Anstatt die Größe als Zahl anzugeben lässt man sie vom Compiler mit Hilfe von sizeof automatisch errechnen. Das hat den Vorteil, dass im Falle einer Größenänderung des Arrays automatisch alle Funktionen die neue berichtigte Größe bekommen. Man muss also nicht den gesamten Code aktualisieren und all die Stellen suchen, an denen die Größe des Arrays als Zahl angegeben wurde. Schauen Sie sich dazu folgendes Beispielprogramm an. #include <iostream> int main() { 18 Variablen std::cout << "Geben Sie Ihren Vor- und Nachnamen ein: " << std::flush; char name[40]; std::cin.getline(name, sizeof(name)); std::cout << "Sie heissen " << name << "." << std::endl; } Im Einführungskapitel haben Sie gesehen, wie mit std::cin Eingaben vorgenommen werden können. In diesem Fall wird zwar auch auf std::cin zugegriffen. Dies geschieht aber auf eine Art und Weise, die Sie noch nicht kennen. std::cin bietet eine Methode namens getline an. Diese Methode, die im obigen Beispielprogramm aufgerufen wird, erwartet zwei Parameter: Ein Array vom Typ char und die Größe dieses Arrays. Indem die Größe des Arrays nicht als Zahl, sondern mit sizeof(name) angegeben wird, errechnet der Compiler die Größe automatisch. Sollte das Programm später angepasst werden und die Größe des Arrays geändert werden, muss nicht zusätzlich der Parameter für getline aktualisiert werden. getline speichert die gesamte Eingabe bis zu einem abschließenden Enter in dem Array, das als Parameter angegeben ist. Sollte die Eingabe größer sein als das Array Speicherplatz bietet, werden die restlichen Zeichen verworfen und nicht gespeichert. Da getline die Eingabe im Array mit einem Null-Zeichen automatisch abschließt, können im obigen Beispielprogramm bis zu 39 Zeichen von der Eingabe gespeichert werden. Wenn Sie sich fragen, was der Unterschied zwischen dem Lesen einer Eingabe mit getline ist und der Methode, die in den Beispielprogrammen im ersten Kapitel vorgestellt wurde: Ein Leerzeichen ist für getline ein ganz normales Zeichen wie jedes andere auch. getline bricht also eine Eingabe nicht ab, wenn es auf ein Leerzeichen trifft. Daher kann im obigen Beispielprogramm sowohl der Vor- als auch der Nachname gleichzeitig eingelesen werden, auch wenn diese wie üblich durch ein Leerzeichen getrennt werden. Während C-Programmierer übrigens tatsächlich mit Arrays vom Typ char arbeiten müssen, um Zeichenketten wie Wörter oder Sätze in ihren Programmen verwenden zu können, brauchen Sie dies als C++-Programmierer glücklicherweise nicht. Als C++-Programmierer verwenden Sie stattdessen den bereits erwähnten nicht-intrinsischen Datentyp std::string und brauchen sich normalerweise nicht mit Arrays vom Typ char herumzuschlagen, nur um ein paar Buchstaben zu speichern. Da es aber wie eben gesehen durchaus Code gibt, der Arrays erwartet, kommen Sie um die Anwendung von Arrays vom Typ char nicht unbedingt herum. Auch wenn in diesem Abschnitt besonders häufig Arrays vom Typ char Verwendung fanden: Sie können selbstverständlich auch Arrays mit einem anderen Datentyp definieren. Der Umgang mit Arrays eines anderen Datentypen ist aber insofern einfach als dass Sie sich dort nicht um dieses ominöse NullZeichen kümmern müssen - das gibt es nur bei Arrays vom Typ char. 19 Kapitel 3. Operatoren Verarbeiten von Daten in Variablen Nach Informationsspeicherung die Informationsverarbeitung Im letzten Kapitel haben Sie viel über Variablen gelernt und wissen nun ausreichend über Variablen Bescheid. Sie wissen, dass es verschiedene Datentypen gibt – sowohl intrinsische als auch nicht-intrinsische. Sie wissen, wie man Variablen anlegt, Werte in ihnen speichert und Werte zwischen Variablen austauscht. Sie haben gesehen, dass Buchstaben als Zahlen gespeichert werden und die Zuordnung von Zahlen zu Buchstaben und anderen Zeichen in Code-Tabellen stattfindet. Und zuguterletzt haben Sie sogar Arrays kennengelernt. Sie wissen nun, dass man zum Speichern von Informationen in Programmen ganz einfach Variablen benötigt. Mit der Informationsspeicherung alleine ist es jedoch nicht getan. Computerprogramme speichern ja nicht einfach Daten, sie verarbeiten sie. Und genau hier kommen die Operatoren ins Spiel: Mit ihnen ist es möglich, auf Variablen zuzugreifen und Informationen, die in Variablen gespeichert sind, zu verarbeiten. int i; i = 3; Schauen Sie sich obiges Beispiel aus dem vorherigen Kapitel nochmal genau an. Sie wissen, dass int ein Datentyp ist, dass i der Name einer Variablen ist, dass Anweisungen in C++ bekanntlich mit einem Semikolon abgeschlossen werden - was aber ist dieses Gleichheitszeichen? Das Gleichheitszeichen ist einer der vielen Operatoren, die es in C++ gibt. Es handelt sich hier um den Zuweisungsoperator. Ohne diesen Operator wäre es gar nicht möglich, Werte in Variablen zu speichern. Sie haben den Zuweisungsoperator bereits häufig angewandt und damit Zahlen oder Buchstaben in Variablen abgelegt. Dies ist auch die eigentliche Funktion des Zuweisungsoperators: Werte, die vom Programmierer direkt angegeben sind oder die in anderen Variablen gespeichert sind, in eine Variable zu übertragen und dort abzulegen - sprich den Wert zuzuweisen. Neben dem Zuweisungsoperator gibt es eine ganze Reihe an Operatoren in C++. Zum Teil sind sie selbsterklärend, zum Teil für Sie wahrscheinlich im Moment völlig unverständlich, weil Sie sich derzeit noch keinen Einsatzbereich vorstellen können. Daher ist dieses Kapitel erfahrungsgemäß ein eher einfaches Kapitel. Vieles wird Ihnen sofort einleuchten, anderes im Moment zu technisch und zu unbedeutend sein. Sie sollen trotzdem in diesem Kapitel einen Überblick über alle Operatoren bekommen, die es in C++ gibt, weil es letztendlich nicht allzu viele sind und Sie somit alle Operatoren wenigstens einmal gesehen haben. Das wichtigste ist sowieso nicht, dass Sie jeden Operator auswendig kennen, sondern am Ende des Kapitels verstanden haben, was der Sinn und Zweck von Operatoren ist und wie sie grundsätzlich angewandt werden. Arithmetische Operatoren Die vier Grundrechenarten Arithmetische Operatoren sind +, -, *, / und %. Sie werden für die aus der Mathematik bekannten Grundrechenarten verwendet. Es handelt sich wie aus der Mathematik bekannt ausschließlich um binäre Operatoren. Binäre Operatoren sind Operatoren, die jeweils genau zwei Werte oder Variablen benötigen. Auf beiden Seiten von binären Operatoren muss also jeweils ein Wert oder eine Variable stehen - ansonsten meldet der Compiler einen Fehler. 20 Operatoren Die arithmetischen Operatoren ermöglichen die Ausführung der vier Grundrechenarten: Mit + können Werte oder Variablen addiert werden, mit - subtrahiert werden, mit * multipliziert und mit / dividiert werden. Der Operator % ist der Modulo-Operator: Er führt ebenso wie / eine Division aus, gibt jedoch als Ergebnis den ganzzahligen Restwert der Division zurück. int a, b, c; a b c c c c c = = = = = = = 6; 4; a + a a * a / a % b; b; b; b; b; Obige Code-Zeilen demonstrieren die Anwendung von arithmetischen Operatoren: Auf beiden Seiten - also rechts und links der Operatoren - steht jeweils eine Variable. Achten Sie auch darauf, dass normalerweise bei einer Informationsverarbeitung mit Hilfe von Operatoren fast immer auch der Zuweisungsoperator in der gleichen Zeile steht. Andernfalls würde beispielsweise eine Addition durchgeführt werden, das Ergebnis der Addition aber gar nicht gespeichert werden. Der C++-Compiler würde hier nicht meckern, weil es sich um gültigen C++-Code handelt - nur haben Sie nicht viel von einer Addition, wenn Sie das Ergebnis nirgendwo speichern. Während die Operatoren +, - und * wie von der Mathematik gewohnt arbeiten, muss auf / und % näher eingegangen werden. Im obigen Beispiel handelt es sich bei a, b und c um Variablen vom Typ int. Bei einer Division zweier Variablen vom Typ int und der Speicherung des Ergebnisses in einer Variablen vom Typ int geht der Nachkommaanteil verloren. Während also die Division von 6 und 4 eigentlich 1,5 ergibt, wird in c lediglich die Zahl 1 gespeichert. Es findet hierbei auch keine Auf- oder Abrundung statt - der Nachkommaanteil geht einfach verloren, weil er nicht in einer Variablen vom Typ int gespeichert werden kann. Was passiert bei der Modulo-Operation? Das Programm dividiert 6 und 4. Das ganzzahlige Ergebnis lautet hierbei 1, wobei ein Restwert von 2 übrigbleibt. Der Restwert wird von der Modulo-Operation zurückgegeben, so dass in der Variablen c 2 gespeichert wird. Logische Operatoren Verknüpfen von Wahrheitswerten Logische Operatoren ermöglichen es, Wahrheitswerte zu verknüpfen. Die logischen Operatoren && und || sind binäre Operatoren, der logische Operator ! ist ein unärer Operator. Unäre Operatoren erwarten nur einen Operanden, während binäre Operatoren zwei Operanden erwarten. bool b1, b2, r; b1 = true; b2 = false; r = b1 && b2; r = b1 || b2; r = !b1; Der Operator && ist das logische UND. Dieser Operator gibt als Ergebnis den Wahrheitswert true zurück, wenn genau beide Operanden true sind. Ist auch nur einer der beiden Operanden false, gibt das logische UND als Ergebnis false zurück. Das || ist das logische ODER. Das logische ODER gibt als Ergebnis den Wahrheitswert true zurück, wenn mindestens einer der beiden Operanden true ist. Sind beide Operanden false, gibt das logische ODER als Ergebnis false zurück. 21 Operatoren Der unäre logische Operator ! gibt false zurück, wenn der Operand true ist, und gibt true zurück, wenn der Operand false ist. Dieser Operator heißt NICHT-Operator. Er dreht den Wahrheitswert des Operanden einfach um. Wie bei den arithmetischen Operatoren muss auch bei den logischen Operatoren das Ergebnis explizit gespeichert werden, wenn es nicht verloren gehen soll. Die letzte Zeile im obigen Beispiel, in der der logische Operator ! angewandt wird, liefert als Ergebnis den Wahrheitswert false zurück. Die Variable b1 ist nach dieser Zeile jedoch noch immer auf true gesetzt – der Wert der Variablen b1 ändert sich nicht! Bitweise Operatoren Bits setzen, löschen und verschieben Die bitweisen Operatoren &, |, ~, ^, << und >> ermöglichen das Setzen, Löschen und Verschieben von Bits. Im Buch Allgemeine Grundlagen der Programmierung [http://www.highscore.de/grundlagen/] wird auf die Bedeutung von &, |, ~ und ^ ausführlich eingegangen. Es handelt sich hierbei um die bitweisen UND-, ODER-, NICHT- und EXKLUSIVES-ODER-Operatoren. Im Rahmen dieses Buchs werden die bitweisen Operatoren nicht näher vorgestellt, da sie lediglich für sehr gezielte Operationen zum Verarbeiten von Bits benötigt werden, die zum Erlernen der Programmiersprache C++ nicht entscheidend sind. Wenn Ihnen die Operatoren << und >> aus den bisherigen Beispielprogrammen bekannt vorkommen: Sie haben mit diesen Operatoren tatsächlich gearbeitet gehabt und Variablen in die Standardausgabe std::cout "geschoben". Warum die bitweisen Operatoren im Zusammenhang mit std::cout keine Bits verschieben, sondern Informationen auf den Bildschirm ausgeben - also eine völlig andere Bedeutung haben – werden Sie noch in diesem Kapitel erfahren. Vergleichsoperatoren Werte und Variablen vergleichen Vergleichsoperatoren ermöglichen wie bereits der Name sagt den Vergleich von Werten und Variablen. Sie können mit den Operatoren ==, !=, >, <, >= und <= auf Gleichheit, Ungleichheit, Größer oder Kleiner oder Größer-Gleich bzw. Kleiner-Gleich überprüfen. Es handelt sich bei allen Vergleichsoperatoren um binäre Operatoren. Vergleichsoperatoren liefern als Ergebnis einen Wahrheitswert zurück - also entweder true oder false. Das Ergebnis hängt davon ab, ob der Vergleich richtig ist oder nicht. int a, b; bool r; a b r r r r r r = = = = = = = = 5; 10; a == b; a != b; a > b; a < b; a >= b; a <= b; Vergleichsoperatoren werden vor allem in Kontrollstrukturen benötigt, wenn abhängig von bestimmten Bedingungen Code ausgeführt werden muss oder nicht. Kontrollstrukturen lernen Sie im folgenden Kapitel kennen. 22 Operatoren Achten Sie darauf, dass Sie für eine Zuweisung = schreiben, für eine Überprüfung auf Gleichheit jedoch ==. Gerade Anfängern in den Programmiersprachen C und C++ passiert es immer wieder, dass sie auf Gleichheit überprüfen wollen, jedoch das zweite Gleichheitszeichen vergessen und daher eine Zuweisung schreiben. Kombinierte Zuweisungsoperatoren Programmierer sind faule Leute Kombinierte Zuweisungsoperatoren sind Zuweisungsoperatoren, die mit anderen Operatoren kombiniert sind. Sinn und Zweck ist letztendlich eine abgekürzte Schreibweise für kürzeren, übersichtlicheren Code. int a, b; a = 5; b = 10; a = a + b; Was geschieht im obigen Programm? Es werden die Variablen a und b mit Hilfe des arithmetischen Operators + addiert, woraufhin die Summe mit Hilfe des Zuweisungsoperators = in der Variablen a gespeichert wird. Dies lässt sich mit einem kombinierten Zuweisungsoperator auch wie folgt schreiben. int a, b; a = 5; b = 10; a += b; Achten Sie auf die letzte Code-Zeile. Diese Zeile führt die gleiche Berechnung aus wie im vorherigen Beispiel: b wird zu a hinzuaddiert, und das Ergebnis wird in a gespeichert. Genauso wie mit Hilfe des arithmetischen Operators + ein kombinierter Zuweisungsoperator += gebildet werden kann, können mit anderen Operatoren folgende kombinierte Zuweisungsoperatoren gebildet werden: -=, *=, /=, %=, &=, |=, ^=, <<= und >>=. Inkrement- und Dekrement-Operator Programmierer sind richtig faul Sehen Sie sich folgenden Code an. int a, b; a = 5; b = 1; a += b; Die Variable a wird in diesem Fall um den Wert 1 erhöht; man könnte auch sagen inkrementiert. Um Variablen lediglich um den Wert 1 zu erhöhen, gibt es den speziellen Inkrement-Operator ++. Es handelt sich hierbei um einen unären Operator. Folgender Code macht also das gleiche wie obiger Code. int a; 23 Operatoren a = 5; ++a; So wie es einen Inkrement-Operator gibt, gibt es auch einen Dekrement-Operator. Mit -- wird der Wert einer Variablen um 1 verringert. Inkrement- als auch Dekrement-Operatoren können auch hinter einer Variablen angegeben sein. Ob sie vor oder hinter einer Variablen stehen kann entscheidend sein. Betrachten Sie folgendes Beispiel. int a, b; a = 5; b = ++a; b = a++; Nachdem obiger Beispiel-Code zu Ende gelaufen ist, ist in der Variablen b der Wert 6, in der Variablen a der Wert 7 gespeichert. Was ist passiert? Steht der Inkrement-Operator vor der Variablen, wird die Variable erhöht und der um 1 erhöhte Wert zurückgegeben. Steht der Inkrement-Operator hinter der Variablen, wird die Variable erhöht, jedoch der vorherige noch nicht um 1 erhöhte Wert zurückgegeben. In beiden Fällen erhält also im obigen Beispiel die Variable b den Wert 6 zugewiesen. Was für den Inkrement-Operator gilt, gilt natürlich auch für den Dekrement-Operator. Auch hier kann es entscheidend sein, ob der Operator vor oder hinter der Variablen angegeben ist. Spielt es für die Programmlogik keine Rolle, ob Sie den Operator vor oder hinter die Variable stellen, dann stellen Sie ihn davor - der Operator arbeitet unter Umständen schneller als wenn er hinter der Variablen steht. Präzedenz-Tabelle Reihenfolge der Operator-Ausführung Betrachen Sie folgenden Code und raten Sie, welches Ergebnis in der Variablen r gespeichert wird. int a, b, c, r; a b c r = = = = 2; 3; 4; a + b * c; Speichert C++ in der Variablen r den Wert 20 oder 14? C++ hält sich an die Punkt-vor-Strich-Regel und errechnet für die Variable r den Wert 14. Wie sieht es aber im folgenden Beispiel aus? bool a, b, c, r; a b c r = = = = false; true; true; a && b != c; Über den logischen Operator && und den Vergleichsoperator != werden drei Variablen vom Typ bool verknüpft. Was wird nun für ein Wert in der Variablen r gespeichert? In welcher Reihenfolge Operatoren ausgeführt werden, legt in jeder Programmiersprache die Präzedenz-Tabelle fest. In dieser ist angegeben, welche Priorität jeder Operator besitzt. Je höher die Priorität, umso eher wird der Operator ausgeführt. Nur weil der Operator * eine höhere Priorität als der Operator + besitzt, kommt im ersten Beispiel tatsächlich 14 raus. Wären die Prioritäten anders defi- 24 Operatoren niert, könnte stattdessen auch zuerst + und danach * ausgeführt werden - überhaupt kein Problem für eine Programmiersprache. Die Ausführungsreihenfolge wird einfach definiert und fertig. Tabelle 3.1. Präzedenz-Tabelle von C++ Bezeichnung Operatorsymbol Priorität Bewertungsreihenfolge Klammern () [] 14 Von links nach rechts Komponentenauswahl . -> 14 Von links nach rechts Arithmetische Negation - 13 Von rechts nach links Logische Negation ! 13 Von rechts nach links Bitlogische Negation ~ 13 Von rechts nach links Inkrement ++ 13 Von rechts nach links Dekrement -- 13 Von rechts nach links 12 Von links nach rechts +- 11 Von links nach rechts Shift-Operatoren << >> 10 Von links nach rechts Vergleichsoperatoren > >= < <= 9 Von links nach rechts == != 8 Von links nach rechts & 7 Von links nach rechts ^ 6 Von links nach rechts | 5 Von links nach rechts && 4 Von links nach rechts || 3 Von links nach rechts Zuweisungsoperatoren = += -= *= /= %= >>= 2 <<= &= ^= |= Von rechts nach links Sequenzoperator Von rechts nach links Arithmetische Operato- * / % ren Bit-Operatoren Logische Operatoren , 1 Anhand dieser Tabelle können Sie nun auch erkennen, ob der Operator && oder der Operator != zuerst ausgeführt wird - es ist der Vergleichsoperator. Daher wird im vorherigen Beispiel in der Variablen r das Ergebnis false gespeichert. Die beiden Operatoren mit der höchsten Priorität haben wir noch gar nicht kennengelernt. Betrachten Sie folgendes modifizierte Beispiel. int a, b, c, r; a b c r = = = = 2; 3; 4; (a + b) * c; Nun wird zuerst die Addition ausgeführt und dann die Multiplikation. Das heißt, r erhält nun als Ergebnis 20. Die Klammern sind ebenfalls ein Operator, und zwar der Operator mit der höchsten Priorität. Das bedeutet, durch entsprechende Klammerung können wir jederzeit die Auswertungsreihenfolge von Operatoren ändern. Setzen Sie Klammern auch dann, wenn Sie nicht sicher sind, ob die von Ihnen verwendeten Operatoren tatsächlich in der Reihenfolge ausgeführt werden, wie Sie es sich vorstellen. Die Klammern schaden nicht, erzwingen aber genau die Ausführungsreihenfolge, die Sie gerne hätten. Die beiden in der Präzedenz-Tabelle als Komponentenauswahl aufgeführten Operatoren . und -> ignorieren wir erstmal. Diese Operatoren sind auch als Zugriffsoperatoren bekannt. Der Zugriffsope- 25 Operatoren rator -> wird im Zusammenhang mit Zeigern und Objekten benötigt. Der Zugriffsoperator . wird uns beschäftigen, wenn wir Strukturen in einem späteren Kapitel kennenlernen. Stehen mehrere Operatoren mit gleicher Priorität hintereinander, so erfolgt die Auswertungsreihenfolge der Reihe nach - entweder von links nach rechts oder von rechts nach links. Auch hier hilft nur der Blick in die Präzedenz-Tabelle. Über den Daumen gepeilt gilt: Binäre Operatoren werden von links nach rechts ausgeführt, unäre Operatoren von rechts nach links. Kontextabhängigkeit Plus ist nicht immer Plus Welche Bedeutung ein Operator hat, hängt ganz entscheidend davon ab, welchen Typ seine Operanden haben. Auf diese Besonderheit wurden Sie bereits aufmerksam gemacht, als der bitweise Operator << kurz vorgestellt wurde. Laut der Beschreibung in diesem Kapitel verschiebt er Bits. In den Beispielprogrammen, die Sie bisher gesehen haben, hat er aber eine ganze andere Funktion: Dort gibt er Daten auf die Standardausgabe aus. #include <iostream> int main() { int i = 2; std::cout << (i << 2) << std::endl; } Im obigen Beispielprogramm wird der Operator << in zwei unterschiedlichen Situationen verwendet. In den runden Klammern verschiebt er Bits in der Variablen i, und zwar um zwei Stellen nach links. Außerhalb der runden Klammern gibt er Daten auf die Standardausgabe aus. Die Bedeutung des Operators << hängt vom Typ seiner Operanden ab. Da in den Klammern eine Variable vom Typ int verarbeitet wird, kommt er seiner ursprünglichen Bedeutung nach und verschiebt Bits. Außerhalb der Klammern wird er jedoch in Zusammenhang mit std::cout eingesetzt. Und std::cout basiert auf einem Datentypen, für den der Operator << eine neue Bedeutung erhalten hat. Die in diesem Kapitel vorgestellten Operatoren besitzen eine Standardfunktion für intrinsische Datentypen. Diese haben Sie in diesem Kapitel kennengelernt. Wenden Sie jedoch die Operatoren im Zusammenhang mit nicht-intrinsichen Datentypen an, haben die Operatoren eine völlig andere Bedeutung oder können eventuell gar nicht angewandt werden. Welche Bedeutung Operatoren im Zusammenhang mit nicht-intrinsischen Datentypen haben, hängt ganz allein von der Definition der nicht-intrinsischen Datentypen ab. Wenn Sie später eigene Datentypen erstellen werden, werden Sie sehen, wie Sie für Ihre eigenen Datentypen Funktion für Operatoren programmieren. Dies wird im Buch Programmieren in C++: Aufbau [http://www.highscore.de/cpp/aufbau/] ausführlich vorgestellt. Der Datentyp, auf dem std::cout basiert, ist so programmiert, dass er einen Operator << akzeptiert und damit arbeiten kann. Die Funktionsweise des Operators sieht hierbei so aus, dass der rechts vom Operator stehende Operand genommen und an die Standardausgabe weitergereicht wird. Obwohl der Operator also vom Aussehen mit dem bitweisen Operator identisch ist, besitzt er eine völlig andere Funktion. Das Definieren von Funktionen für Operatoren im Zusammenhang mit nicht-intrinsischen Datentypen ist eine der Stärken von C++ - eine Spracheigenschaft, die C- oder Java-Programmierern nicht zur Verfügung steht. Sehen Sie sich folgendes Beispielprogramm an, um einen Eindruck zu bekommen, welchen Vorteil das Anpassen von Operatoren für nicht-intrinsische Datentypen hat. #include <iostream> #include <string> 26 Operatoren int main() { std::string a, b, c; a = "Hallo, "; b = "Welt"; c = a + b; std::cout << c << std::endl; } Hier werden zwei Variablen a und b, beide mit nicht-intrinsischen Datentyp std::string, mit einem + verknüpft. Das +, so haben Sie es in diesem Kapitel gelernt, addiert Zahlen. Nachdem hier jedoch links und rechts vom + keine Zahlen stehen, sondern Variablen vom Typ std::string, stellt sich die Frage, was die Bedeutung von + in diesem Zusammenhang ist. Möglicherweise haben Sie eine Idee? Der Plus-Operator ist für Variablen vom Typ std::string so definiert worden, dass Zeichenketten verknüpft und aneinandergehängt werden können. Das obige Beispielprogramm gibt demnach Hallo, Welt auf den Bildschirm aus. Der Vorteil der Neudefinition von + für Variablen vom Typ std::string ist, dass der Code einfacher zu lesen und zu verstehen wird. Die Tatsache, dass das + in diesem Zusammenhang zu einer Verknüpfung von Zeichenketten führt, ist für die meisten Programmierer einleuchtend und hilft, die Lesbarkeit des Codes zu erhöhen. Das Ändern der Funktionsweise von Operatoren im Zusammenhang mit intrinsischen Datentypen ist in C++ übrigens nicht möglich. Wenn Sie also den Operator + anwenden, um zwei Variablen vom Typ int zu verknüpfen, dann wird wie in diesem Kapitel vorgestellt immer automatisch eine Addition ausgeführt. Da gibt es keine Ausnahme. Aufgaben Übung macht den Meister 1. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe von drei Zahlen auffordert. Das Programm soll den Wert 10 zur ersten eingegebenen Zahl hinzuaddieren, das Ergebnis mit der zweiten eingegebenen Zahl multiplizieren und dann durch die dritte eingegebene Zahl dividieren. Die Berechnung soll hierbei innerhalb einer einzigen Code-Zeile erfolgen. Das Ergebnis soll auf den Bildschirm ausgegeben werden. 2. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe einer vierstelligen Zahl auffordert. Das Programm soll daraufhin die Quersumme der vierstelligen Zahl errechnen und das Ergebnis auf den Bildschirm ausgeben. 27 Kapitel 4. Kontrollstrukturen Verzweigungen Unterschiedlichen Code ausführen Verzweigungen ermöglichen in Abhängigkeit einer Bedingung eine unterschiedliche Code-Ausführung. Verzweigungen werden in C++ mit Hilfe des Schlüsselwortes if erstellt. Betrachten Sie folgendes Beispiel. #include <iostream> int main() { int i; std::cout << "Geben Sie eine Zahl ein: " << std::flush; std::cin >> i; if (i < 100) { std::cout << "Sie haben eine Zahl kleiner als 100 eingegeben." << std::endl; } } Der Anwender wird aufgefordert, eine Zahl einzugeben. Diese wird über die Standardeingabe eingelesen und in der Variablen i gespeichert. Nun kommt die if-Kontrollstruktur ins Spiel. Hinter dem Schlüsselwort if wird innerhalb einer runden Klammer eine Bedingung überprüft. Derartige Überprüfungen finden normalerweise mit Hilfe von Vergleichsoperatoren statt. In diesem Fall wird überprüft, ob die in i gespeicherte Zahl kleiner als 100 ist. Ist dies der Fall - wird also von der Bedingung true als Ergebnis zurückgegeben - so wird der Anweisungsblock zwischen den geschweiften Klammern hinter dem if-Schlüsselwort ausgeführt. Der grundsätzliche Aufbau einer if-Kontrollstruktur sieht wie folgt aus. if (BEDINGUNG) { ANWEISUNGSBLOCK } Die Anweisungen im Anweisungsblock werden genau dann und genau einmal ausgeführt, wenn die zu überprüfende Bedingung true ergibt. Ergibt die zu überprüfende Bedingung false, wird der Anweisungsblock übersprungen, und die Programmausführung setzt hinter dem Anweisungsblock fort. #include <iostream> int main() { int i; std::cout << "Geben Sie eine Zahl ein: " << std::flush; std::cin >> i; if (i < 100) { std::cout << "Sie haben eine Zahl kleiner als 100 eingegeben." << std::endl; 28 Kontrollstrukturen } else { std::cout << "Sie haben eine Zahl groesser gleich 100 eingegeben." << std::endl; } } Eine if-Kontrollstruktur lässt sich zu einer if-else-Kontrollstruktur ausbauen. Hinter dem ifAnweisungsblock wird ein Schlüsselwort else gesetzt, dem wiederum ein Anweisungsblock in geschweiften Klammern folgt. Der Anweisungsblock hinter else wird genau dann und genau einmal ausgeführt, wenn die zu überprüfende Bedingung hinter dem vorherigen if im Ergebnis false ergab. Der schematische Aufbau einer if-else-Kontrollstruktur sieht demnach wie folgt aus. if (BEDINGUNG) { ANWEISUNGSBLOCK } else { ANWEISUNGSBLOCK } Der else-Block ist, wie Sie nun wissen, optional und kann jederzeit weggelassen werden. Es ist möglich, if-else-Kontrollstrukturen zu verschachteln. Betrachten Sie folgendes Beispiel. #include <iostream> int main() { int i; std::cout << "Geben Sie eine Zahl ein: " << std::flush; std::cin >> i; if (i < 100) { std::cout << "Sie haben eine Zahl kleiner als 100 eingegeben." << std::endl; } else { if (i < 200) { std::cout << "Sie haben eine Zahl kleiner als 200 eingegeben." << std::endl; } else { std::cout << "Sie haben eine Zahl groesser gleich 200 eingegeben." << std::endl; } } } Innerhalb des else-Anweisungsblockes befindet sich eine zweite if-else-Kontrollstruktur. Diese überprüft, ob die eingegebene Zahl in i kleiner als 200 ist und zeigt, wenn dies der Fall ist, eine 29 Kontrollstrukturen entsprechende Meldung auf dem Bildschirm an. Andernfalls wird die Meldung ausgegeben, dass die eingegebene Zahl größer oder gleich 200 ist. Die Verschachtelung von if-else-Kontrollstrukturen kann recht häufig notwendig sein. Eine kompaktere Schreibweise des Codes ist daher vorzuziehen. #include <iostream> int main() { int i; std::cout << "Geben Sie eine Zahl ein: " << std::flush; std::cin >> i; if (i < 100) { std::cout << "Sie haben eine Zahl kleiner als 100 eingegeben." << std::endl; } else if (i < 200) { std::cout << "Sie haben eine Zahl kleiner als 200 eingegeben." << std::endl; } else { std::cout << "Sie haben eine Zahl groesser gleich 200 eingegeben." << std::endl; } } Das Beispiel funktioniert genauso wie vorher. Es wird zuerst überprüft, ob die eingegebene Zahl kleiner als 100 ist. Wenn nicht, wird überprüft, ob die eingegebene Zahl kleiner als 200 ist. Ist dies auch nicht der Fall, werden die Anweisungen hinter else ausgeführt. Innerhalb einer Verzweigung können beliebig viele else if-Überprüfungen stattfinden. Beginnen muss eine Verzweigung jedoch immer mit if. Der else-Block ist wie bereits erwähnt optional und kann jederzeit weggelassen werden. Wenn er jedoch verwendet wird, darf hinter else keine nochmalige Überprüfung einer Bedingung mit else if erfolgen. Wenn der Anweisungsblock nur aus einer einzigen Anweisung besteht, können Sie die geschweiften Klammern weglassen. Gerade Anfängern in C++ wird jedoch empfohlen, immer die geschweiften Klammern zu setzen, um nicht versehentlich Anweisungen außerhalb eines if-, else if- oder else-Anweisungsblockes zu schreiben. Eine besondere Form der if-else-Kontrollstruktur ist die switch-case-Anweisung. #include <iostream> int main() { int i; std::cout << "Geben Sie eine Zahl von 1 bis 3 ein: " << std::flush; std::cin >> i; switch (i) { case 1: 30 Kontrollstrukturen { std::cout << "Sie haben 1 eingegeben." << std::endl; break; } case 2: { std::cout << "Sie haben 2 eingegeben." << std::endl; break; } case 3: { std::cout << "Sie haben 3 eingegeben." << std::endl; break; } default: { std::cout << "Sie haben keine Zahl von 1 bis 3 eingegeben." << std::endl; break; } } } Der switch-case-Anweisung wird hinter dem Schlüsselwort switch in runden Klammern eine Variable übergeben. Diese Variable - und das ist eine große Beschränkung von switch-caseAnweisungen - muss von einem intrinsischen Datentyp sein. Es darf sich hierbei auch um kein Array handeln. Hinter der runden Klammer, die tatsächlich nur eine Variable enthält und keine Bedingung, folgt der Anweisungsblock, der wie gewöhnlich zwischen zwei geschweiften Klammern steht. Innerhalb dieses Anweisungsblocks stehen nun mehrere case-Anweisungen. Hinter dem Schlüsselwort case wird ein Wert angegeben, auf den die Variable überprüft werden soll, die in Klammern hinter switch angegeben ist. Hinter dem Wert folgt ein Doppelpunkt. Indem mehrere case-Schlüsselwörter mit unterschiedlichen Werten innerhalb des Anweisungsblocks der switch-case-Anweisung angegeben werden, kann die Variable in Klammern auf mehrere Werte hin überprüft werden. Letztendlich entspricht dies nichts anderem als einer if-else-Kontrollstruktur, die in mehreren else if-Anweisungen eine Variable auf Gleichheit mit unterschiedlichen Werten überprüft. Hinter dem Doppelpunkt einer case-Anweisung folgt wiederum ein Anweisungsblock. Es können die geschweiften Klammern in diesem Fall auch weggelassen und die Anweisungen direkt untereinander geschrieben werden. Der Anweisungsblock wird nämlich nicht durch eine geschlossene geschweifte Klammer beendet, sondern erst durch das Schlüsselwort break. Wenn Sie das Schlüsselwort break vergessen, wird am Ende eines Anweisungsblockes hinter einem case der darauffolgende Anweisungsblock hinter dem nächsten case durchlaufen. Dies kann ab und zu sinnvoll sein, handelt sich aber oft um einen Programmierfehler. Trifft die Code-Ausführung auf break, wird die switch-case-Kontrollstruktur abgebrochen, und die Programmausführung setzt hinter der Kontrollstruktur fort. Innerhalb einer switch-case-Kontrollstruktur beginnen normalerweise alle Zweige mit einem case-Schlüsselwort. Sie können jedoch auch mit dem Schlüsselwort default einen eigenen Abschnitt einleiten. Hinter default wird direkt der Doppelpunkt angegeben. Der Anweisungsblock hinter default wird immer dann ausgeführt, wenn kein case zutreffend war. default entspricht also dem else innerhalb einer if-else-Kontrollstruktur. Ebenso wie der else-Anweisungsblock ist auch der default-Anweisungsblock optional. Möchten Sie einen Anweisungsblock ausführen, wenn verschiedene Werte zutreffen, können Sie folgendes schreiben. 31 Kontrollstrukturen #include <iostream> int main() { int i; std::cout << "Geben Sie eine Zahl von 1 bis 3 ein: " << std::flush; std::cin >> i; switch (i) { case 1: case 3: { std::cout << "Sie haben eine ungerade Zahl eingegeben." << std::endl; break; } case 2: { std::cout << "Sie haben eine gerade Zahl eingegeben." << std::endl; break; } default: { std::cout << "Sie haben keine Zahl von 1 bis 3 eingegeben." << std::endl; break; } } } Die Meldung, dass eine ungerade Zahl eingegeben wurde, wird nun ausgegeben, wenn die Zahl 1 oder die Zahl 3 vom Anwender eingegeben wurde. Indem hinter case 1: nämlich kein break angegeben wird, wird der darauffolgende Anweisungsblock hinter case 3: ausgeführt - bis die Programmausführung auf ein break trifft und die Kontrollstruktur abbricht. Schleifen Code mehrmals ausführen Während mit Hilfe einer if-Kontrollstruktur unterschiedlicher Code genau einmal ausgeführt werden kann, kann mit Schleifen Code in Abhängigkeit einer Bedingung wiederholt ausgeführt werden. C+ + stellt verschiedene Schleifentypen zur Verfügung. #include <iostream> int main() { int i; std::cout << "Geben Sie eine Zahl groesser gleich 100 ein: " << std::flush; std::cin >> i; while (i < 100) { 32 Kontrollstrukturen std::cout << "Geben Sie eine Zahl groesser gleich 100 ein: " << std::flush; std::cin >> i; } } Eine while-Schleife ist ähnlich wie eine if-Kontrollstruktur aufgebaut: Zuerst kommt das Schlüsselwort, in diesem Fall while, danach wird in runden Klammern eine Bedingung überprüft, dahinter folgt zwischen geschweiften Klammern ein Anweisungsblock. Abhängig von der Bedingung wird der Anweisungsblock ausgeführt, oder aber die Code-Ausführung setzt hinter dem Anweisungsblock fort. Im Gegensatz zum if wird jedoch nach einem Durchlauf des Anweisungsblocks die Bedingung in runden Klammern erneut überprüft. Ist sie wieder true, wird der Anweisungsblock nochmal durchlaufen. Auch nach diesem Durchlauf findet erneut eine Überprüfung der Bedingung statt mit eventuell anschließendem erneuten Durchlauf des Anweisungsblocks. Dieses Spielchen wiederholt sich solange, bis die zu überprüfende Bedingung false ergibt. Erst dann wird der Anweisungsblock übersprungen und das Programm danach fortgesetzt. #include <iostream> int main() { int i; do { std::cout << "Geben Sie eine Zahl groesser gleich 100 ein: " << std::flush; std::cin >> i; } while (i < 100); } Wenn Sie sich das Beispiel eben genau angesehen haben, haben Sie festgestellt, dass zwei Zeilen zweimal angegeben waren - einmal vor der Schleife, einmal innerhalb der Schleife. Wenn Sie anstatt einer while-Schleife eine do-while-Schleife verwenden, können Sie wie im obigen Beispiel Ihren Code vereinfachen. Im Gegensatz zu einer while-Schleife findet bei einer do-while-Schleife die Überprüfung der Bedingung am Ende eines Schleifendurchgangs statt. Das bedeutet, dass der Anweisungsblock der Schleife auf alle Fälle einmal ausgeführt wird. Erst nach dieser einmaligen Ausführung wird entschieden, ob die Schleife nochmal durchlaufen werden muss. Während in einer while-Schleife zuerst die Bedingung und dann die Schleife ausgeführt wird, wird in einer do-while-Schleife zuerst die Schleife durchlaufen und dann die Bedingung überprüft. Beachten Sie, dass in einer do-while-Schleife hinter der Überprüfung der Bedingung ein Semikolon gesetzt werden muss. #include <iostream> int main() { int i; for (int i = 0; i < 3; ++i) { std::cout << "Geben Sie eine Zahl ein: " << std::flush; std::cin >> i; } } 33 Kontrollstrukturen Der dritte Schleifentyp, der in C++ verwendet werden kann, ist eine Zählschleife. Sie wird forSchleife genannt. Die for-Schleife ist ähnlich aufgebaut wie die while-Schleife: Zuerst wird das Schlüsselwort gesetzt, dann folgt eine runde Klammer, danach folgt der Anweisungsblock der Schleife. Im Gegensatz zur while-Schleife besteht die runde Klammer jedoch in einer for-Schleife nicht mehr nur aus der Überprüfung einer Bedingung, sondern aus drei verschiedenen Bereichen. Eine for-Schleife unterteilt die runde Klammer in genau drei Bereiche. Zur Abgrenzung der Bereiche werden Semikolons gesetzt. Der erste Bereich - von der geöffneten runden Klammer zum ersten Semikolon - ist der Initialisierungsbereich. Der zweite Bereich - vom ersten Semikolon zum zweiten Semikolon - ist die Schleifenbedingung. Der dritte Bereich - vom zweiten Semikolon zur geschlossenen runden Klammer - ist die Wertänderung. Die schematische Darstellung der for-Schleife sieht demnach wie folgt aus. for (INITIALISIERUNG; BEDINGUNG; WERTÄNDERUNG) { ANWEISUNGSBLOCK } Trifft die Programmausführung auf eine for-Schleife, wird zuallererst der Initialisierungsbereich ausgeführt. Für gewöhnlich wird hier - daher der Name - eine Variable initialisiert, also auf einen bestimmten Wert gesetzt. Sie können hier auf eine bereits bestehende Variable zurückgreifen oder aber auch eine neue Variable definieren. Ist der Initialisierungsbereich ausgeführt worden, wird die Bedingung überprüft. Diese besteht wie gewohnt meist aus einem Vergleich zweier Werte oder Variablen. Ist die Bedingung wahr - wird also das Ergebnis true zurückgegeben - wird der Anweisungsblock der Schleife ausgeführt. Ist die Bedingung falsch, wird der Anweisungsblock ausgelassen, und die Programmausführung setzt hinter der for-Schleife fort. Wenn die Bedingung wahr war und der Anweisungsblock einmal ausgeführt wurde, wird anschließend der dritte Bereich in den runden Klammern der for-Schleife ausgeführt - die Wertänderung. Hier wird normalerweise die Variable, die im Initialisierungsbereich auf einen bestimmten Wert gesetzt wurde, in- oder dekrementiert. An dieser Stelle kommt daher häufig der Inkrement- oder Dekrement-Operator zum Einsatz. Nachdem die Wertänderung vorgenommen wurde, wird wieder die Bedingung im Schleifenkopf überprüft. Ist sie immer noch wahr, wird der Anweisungsblock erneut ausgeführt. Ist sie falsch, wird die Schleife beendet, und die Programmausführung setzt hinter der Schleife fort. Die for-Schleife wird normalerweise dann eingesetzt, wenn mehr oder weniger klar ist, wie oft der Anweisungsblock ausgeführt werden muss. Bei der while- und do-while-Schleife ist das normalerweise nicht bekannt, so dass in diesem Fall auch keine Variable die Anzahl der Schleifendurchläufe mitzählt, sondern lediglich eine Bedingung überprüft wird. Dennoch lassen sich die Schleifen beliebig austauschen. Den richtigen Schleifentyp für bestimmte Programmiersituationen gibt es nicht. Kontrolltransferanweisungen Abbrechen und Neuausführen In C++ stehen zwei Schlüsselwörter zur Verfügung, um eine laufende Code-Ausführung abzubrechen oder neu zu starten. Ein Schlüsselwort haben Sie bereits kennengelernt: break. Mit break können nicht nur switch-case-Kontrollstrukturen beendet werden, sondern auch alle Schleifentypen. Betrachten Sie folgendes Beispiel. #include <iostream> 34 Kontrollstrukturen int main() { int i; do { std::cout << "Geben Sie eine Zahl groesser gleich 100 ein: " << std::flush; std::cin >> i; if (!std::cin.good()) break; } while (i < 100); } Der Anwender wird innerhalb einer while-Schleife aufgefordert, eine Zahl größer oder gleich 100 einzugeben. Die Schleifenbedingung ist erst dann falsch, wenn er dies getan hat. Solange er eine Zahl kleiner als 100 eingibt, wird die Schleife wiederholt ausgeführt. Nach jeder Eingabe durch den Anwender erfolgt jedoch vor der Überprüfung der Schleifenbedingung innerhalb einer if-Kontrollstruktur ein Test, ob die Standardeingabe std::cin in einem fehlerfreien Zustand ist. Sie ist dies nicht, wenn keine zum Datentyp der Variable i passende Eingabe durch den Anwender vorgenommen wurde. Gibt der Anwender zum Beispiel keine Zahl ein, sondern drückt einfach Enter, speichert std::cin diesen Fehler. Indem für std::cin die Methode good aufgerufen wird, kann überprüft werden, ob der aktuelle Zustand von std::cin fehlerfrei ist. good gibt true oder false zurück. Wenn im obigen Beispielprogramm false zurückgegeben wird, ist die Bedindung aufgrund des NICHT-Operators wahr und die Schleife wird mit break abgebrochen. Das Schlüsselwort break darf ausschließlich innerhalb von switch-case-Anweisungen und Schleifen verwendet werden. Ansonsten meldet der Compiler einen Fehler. Das zweite Schlüsselwort, das in die Kategorie Kontrolltransferanweisungen fällt, ist continue. Betrachen Sie hierzu folgendes Beispiel. #include <iostream> int main() { int i; while (true) { std::cout << "Geben Sie eine negative Zahl ein: " << std::flush; std::cin >> i; if (i >= 0) continue; break; } } Das Schlüsselwort continue darf ausschließlich in Schleifen verwendet werden. Im obigen Beispiel steht es im Anweisungsblock einer while-Schleife. Diese Schleife ist insofern bemerkenswert, als dass die Bedingung immer true ergibt - genau das ist nämlich hinter dem Schlüsselwort while in runden Klammern angegeben. Die Schleife würde demnach unendlich oft ausgeführt werden, wenn es keine andere Abbruchbedingung in der Schleife gibt. Innerhalb des Anweisungsblocks der Schleife wird der Anwender aufgefordert, eine negative Zahl einzugeben. Die Eingabe wird daraufhin in einer if-Kontrollstruktur überprüft, ob es sich tatsächlich 35 Kontrollstrukturen um eine negative Zahl handelt. Ist dies nicht der Fall, wird der Anweisungsblock hinter if ausgeführt, der aus dem Schlüsselwort continue besteht. continue bewirkt, dass der aktuelle Schleifendurchgang abgebrochen wird. Die Code-Ausführung setzt also am Schleifenbeginn fort, es wird demnach die Bedingung erneut überprüft. Nachdem diese im Beispiel immer true ergibt, wird die Schleife neugestartet. Gibt der Anwender hingegen eine negative Zahl ein, wird die Schleife nicht mit continue neugestartet, sondern mit break beendet. Somit ist sichergestellt, dass das Programm nicht in einer Endlosschleife hängenbleibt. Obiges Beispielprogramm demonstriert lediglich die Funktion von continue. Normalerweise könnte die Funktionalität der Anwendung besser und einfacher implementiert werden, ohne auf continue und break zurückgreifen zu müssen. Derartige Kontrolltransferanweisungen sollten nur eingesetzt werden, wenn eine andere Schreibweise das Programm zu kompliziert gestalten würde. In der Praxis werden continue und break jedenfalls nicht sehr häufig verwendet. Aufgaben Übung macht den Meister 1. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe einer vierstelligen Zahl auffordert. Das Programm soll daraufhin die Quersumme der Zahl mit Hilfe einer Schleife errechnen und das Ergebnis dann auf den Bildschirm ausgeben. 2. Erweitern Sie das Programm aus Aufgabe 1 derart, dass die Quersummer einer beliebigen Zahl errechnet wird. 3. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe einer Zahl auffordert. Das Programm soll daraufhin errechnen, ob es sich um eine Primzahl handelt. Ist dies der Fall, soll eine entsprechende Meldung auf den Bildschirm ausgegeben werden. Andernfalls soll die Meldung ausgegeben werden, dass es sich um keine Primzahl handelt. 4. Entwickeln Sie eine C++-Anwendung, die die kleinste und größte Zahl von vier vorgegebenen Zahlen ermittelt, die in einem Array vom Typ int gespeichert sind. Das Programm soll auf den Bildschirm die größte und die kleinste Zahl aus dem Array ausgeben. Testen Sie Ihr Programm, indem Sie im Array verschiedene Zahlen speichern und die größte und kleinste Zahl sich jeweils an verschiedenen Stellen im Array befindet. 36 Kapitel 5. Funktionen Definition Funktionen erstellen Eine Funktion ist eine Ansammlung von Anweisungen, der ein Name gegeben wird und die jederzeit im Programm über diesen Namen aufgerufen und ausgeführt werden kann. Während mit den bisher kennengelernten Variablen, Operatoren und Kontrollstrukturen bereits recht leistungsfähige Programme möglich sind, können mit Funktionen Anweisungen getrennt in Pakete gelegt und Programme übersichtlicher gestaltet werden. Um Funktionen in Programmen nutzen zu können, müssen sie erstmal definiert werden. Die Definition ist immer der erste Schritt, um eine Funktion nutzen zu können. In all Ihren Beispielprogrammen haben Sie bereits immer eine Funktion definiert gehabt. Betrachten Sie hierfür folgendes Beispiel. int main() { } Sie sehen eine Funktionsdefinition, wie sie in jedem C++-Programm vorkommt. Die Funktion main ist die Funktion, mit der ein Programm startet. Eine Funktionsdefinition beginnt immer mit dem Datentyp des Rückgabewertes. Der Rückgabewert ist das Ergebnis, das die Funktion liefert. Eine Funktion muss nicht unbedingt ein Ergebnis liefern. Sie muss dann mit dem Schlüsselwort void definiert werden. void bedeutet einfach, dass die Funktion kein Ergebnis liefert. Die Funktion main muss mit dem Datentyp int für den Rückgabewert definiert werden - dies schreibt der C++-Standard vor. Hinter dem Datentyp des Rückgabewertes wird der Funktionsname angegeben. Wie bei Variablen auch können beliebige Funktionsnamen vergeben werden, die - ebenfalls wie bei Variablen - jedoch darauf hindeuten sollten, welchen Sinn und Zweck die Funktion hat. Kryptische und sinnlose Funktionsnamen erschweren nur die Übersicht im Programm. Hinter dem Funktionsnamen werden zwei runde Klammern geschrieben, zwischen denen eine Parameterliste angegeben werden kann. Im obigen Beispiel sind die runden Klammern hinter main leer - die Funktion main erwartet demnach keine Parameter. Parameter sind das Gegenstück zum Rückgabewert: So wie jede Funktion ein Ergebnis zurückgeben kann, kann eine Funktion Parameter als Eingabe akzeptieren. Innerhalb der Funktion wird dann mit den Parametern gearbeitet, und am Ende kann ein Ergebnis zurückgeliefert werden. Mit den runden Klammern und der optionalen Parameterliste ist der Funktionskopf abgeschlossen. Dahinter folgt der Anweisungsblock, der wie gewohnt zwischen zwei geschweiften Klammern steht. Im Anweisungsblock können nun Variablen, Operatoren und Kontrollstrukturen untergebracht werden, um die Funktion zu implementieren. Was auf keinen Fall innerhalb des Anweisungsblocks stehen darf ist eine Definition einer anderen Funktion. Während Datentyp des Rückgabewerts, Funktionsname und optionale Parameterliste als Funktionskopf bezeichnet werden, kann der Anweisungsblock der Funktion Funktionsrumpf genannt werden. Um einen Wert oder eine Variable als Ergebnis einer Funktion zurückzugeben, muss auf das Schlüsselwort return zugegriffen werden. Betrachten Sie folgendes Beispiel. int main() 37 Funktionen { return 0; } Der Rückgabewert wird hinter return angegeben. Im obigen Beispiel wird also die Zahl 0 von der Funktion main als Ergebnis zurückgegeben. Dies ist insofern kein Problem als dass die Funktion main ja so definiert ist, dass sie einen Rückgabewert besitzt, der vom Typ int ist. Die Zahl 0 stimmt mit dem Datentyp int überein, so dass das Beispiel einwandfrei kompiliert. Funktionen, die nicht mit void definiert sind, sondern einen Rückgabewert liefern, müssen auch mit return einen Wert zurückgeben. Wenn Sie return nicht angeben, meckert der Compiler. Entweder fügen Sie dann ein return ein oder aber Sie ändern die Definition der Funktion dahingehend, dass Sie den Datentyp des Rückgabewerts auf void setzen. Eine Ausnahme gibt es: Der C++-Standard definiert die Funktion main derart, dass diese Funktion aber auch nur diese - keinen Wert mit return zurückgeben muss. Wenn Sie für main kein return angeben, gibt diese Funktion automatisch den Wert 0 zurück. Aus diesem Grund wird in allen Beispielprogrammen in diesem Buch für main kein Wert mit return zurückgegeben. Die praktische Bedeutung der Rückgabewerte von main ist beschränkt. Nachdem mit dieser Funktion ein C++-Programm startet, wird auch nach Ablauf der Funktion das Programm beendet. Der Rückgabewert von main kann daher nicht direkt im Programm verwendet werden, sondern wird vom Betriebssystem übernommen. Über Betriebssystemfunktionen kann der Rückgabewert eines Programms abgefragt werden. Es hat sich eingebürgert, dass der Rückgabewert 0 ein fehlerfreies Programmende bedeutet und ein anderer Wert als 0 ein Programmabruch aufgrund eines Fehlers. Während der Rückgabewert für main keine so bedeutende Rolle spielt, kann er für Funktionen, die innerhalb des Programms aufgerufen werden, umso bedeutender sein - vor allem, wenn der Funktion Parameter übergeben werden und sie mit den übergebenen Parametern ein Ergebnis errechnet. int add(int a, int b) { int r = a + b; return r; } Sie sehen oben die Definition einer Funktion, die einen Rückgabewert vom Typ int liefert und zwei Parameter vom Typ int erwartet. Parameter werden zwischen den runden Klammern im Funktionskopf angegeben. Werden mehrere Parameter definiert, müssen diese mit Komma getrennt werden. Die Definition von Parametern besteht lediglich aus der Definition von Variablen: Zuerst wird der Datentyp angegeben, dann der Variablenname. Tatsächlich sind Parameter technisch nichts anderes als Variablen. Innerhalb der Funktion werden sie ebenfalls wie Variablen verwendet und so wie oben beispielsweise mit dem Plus-Operator addiert. Die Funktion add ist demnach so definiert, dass sie zwei Parameter als Eingabe erwartet, die übergebenen Werte addiert und die Summe als Ergebnis zurückgibt. Aufruf Funktionen verwenden Ist eine Funktion definiert, kann sie aufgerufen werden. Im folgenden Beispiel wird auf die Funktion add zurückgegriffen. #include <iostream> int add(int a, int b) { 38 Funktionen int r = a + b; return r; } int main() { int x = 10, y = 5, result; result = add(x, y); std::cout << x << " + " << y << " = " << result << std::endl; } Der Funktionsaufruf von add erfolgt in der Funktion main: Es wird der Funktionsname angegeben, gefolgt von den runden Klammern, in denen zwei Variablen als Parameter übergeben werden. Die Funktion add ist so definiert, dass sie zwei Parameter erwartet. Deswegen müssen auch beim Aufruf zwei Variablen in Klammern angegeben werden. So wie Parameter in der Parameterliste durch Komma getrennt werden, werden auch mehrere Variablen beim Aufruf einer Funktion durch Komma getrennt. Der Rückgabewert von add wird per Zuweisungsoperator = in der Variablen result gespeichert. Wichtig ist, dass die Variable result den gleichen Typ besitzt wie der Rückgabewert der Funktion. Dies ist im Code-Beispiel der Fall: Sowohl result als auch der Rückgabewert von add sind vom Datentyp int. Im obigen Beispielprogramm geschieht also folgendes: Die zwei Variablen x und y werden der Funktion add als Parameter übergeben. Der Wert der ersten Variable wird in den ersten Parameter der Funktion kopiert, der Wert der zweiten Variable in den zweiten Parameter der Funktion. Konkrekt bedeutet dies, dass die Zahl 10 der Variablen x in die Variable a der Funktion add kopiert wird, und der Wert 5 der Variablen y in die Variable b der Funktion add. Innerhalb der Funktion add wird mit den übergebenen Werten gearbeitet: Es wird die Summe gebildet, in der Variablen r gespeichert und dann als Ergebnis der Funktion zurückgegeben. In der Funktion main wird daraufhin der Rückgabewert von add in der dort definierten Variablen result gespeichert. Im letzten Schritt werden die Variablen x, y und result an die Standardausgabe weitergereicht und auf den Bildschirm ausgegeben. Wenn eine Funktion einen Rückgabewert liefert, kann sie überall dort aufgerufen werden, wo ein Wert von dem Datentyp erwartet wird, den die Funktion zurückgibt. Das heißt, es ist nicht unbedingt notwendig, erst den Rückgabewert einer Funktion in einer Variablen zwischenzuspeichern und dann diese Variable weiterzugeben. Betrachten Sie hierzu folgendes Beispiel. #include <iostream> int add(int a, int b) { int r = a + b; return r; } int main() { int x = 10, y = 5; std::cout << x << " + " << y << " = " << add(x, y) << std::endl; } In der Funktion main wird nun keine Variable result mehr definiert. Der Aufruf von add findet jetzt an der Stelle statt, an der vorher die Variable result den von add gespeicherten Rückgabewert an die Standardausgabe weitergereicht hat. Das Beispiel funktioniert so wie oben einwandfrei: 39 Funktionen Der Rückgabewert von add, ein Wert vom Datentyp int, wird direkt an die Standardausgabe weitergereicht. Nachdem der Rückgabewert nicht anderweitig im Programm benötigt wird, kann also die unnötige Variablendefinition von result entfallen. So wie der Rückgabewert einer Variablen direkt weitergegeben werden kann, kann beispielsweise auch das Ergebnis einer Addition direkt weitergereicht werden, ohne vorher in einer Variablen gespeichert zu werden. #include <iostream> int add(int a, int b) { return a + b; } int main() { int x = 10, y = 5; std::cout << x << " + " << y << " = " << add(x, y) << std::endl; } Werfen Sie einen Blick auf die Definition von add. Die Addition der Parameter a und b findet nun direkt hinter dem Schlüsselwort return statt: a und b werden addiert, und die Summe wird direkt ohne unnötige Speicherung in einer anderen Variablen als Rückgabewert mit return weitergereicht. Das Programm um noch einen Schritt vereinfacht sieht wie folgt aus. #include <iostream> int add(int a, int b) { return a + b; } int main() { std::cout << 10 << " + " << 5 << " = " << add(10, 5) << std::endl; } Anstatt die Zahlen 10 und 5 in Variablen zu speichern werden sie nun direkt an den Stellen angegeben, an denen sie verwendet werden sollen. In diesem Fall werden also auch der Funktion add Werte direkt übergeben. Auch diese werden wie gewohnt in die Variablen a und b der Funktion add kopiert. Referenzvariablen Verweise auf andere Variablen In den bisherigen Beispielprogrammen in diesem Kapitel wurden Werte, die als Parameter an Funktionen weitergegeben wurden, kopiert. Sehen Sie sich folgendes Beispielprogramm an, das dies verdeutlicht. #include <iostream> void set(int a) { a = 10; 40 Funktionen } int main() { int x = 5; set(x); std::cout << x << std::endl; } In diesem Beispiel wird eine Funktion set definiert, die kein Ergebnis zurückliefert und einen Parameter erwartet. Die Funktion ist so definiert, dass die Variable a aus der Parameterliste auf den Wert 10 gesetzt wird. In der Funktion main wird eine Variable x definiert und mit dem Wert 5 initialisiert. Als nächstes erfolgt ein Funktionsaufruf von set, dem als Parameter die Variable x übergeben wird. In der letzten Zeile des Programms wird x an die Standardausgabe weitergereicht und auf den Bildschirm ausgegeben. Welchen Wert gibt das Programm aus? Obiges Beispielprogramm zeigt am Bildschirm die Zahl 5 an. Innerhalb der Funktion set wird nämlich nicht die Variable x selbst auf den Wert 10 gesetzt, sondern nur die Variable a. In diese wird zwar beim Funktionsaufruf der Wert von x kopiert, es handelt sich aber tatsächlich nur um eine Kopie und um mehr nicht. Die Variable a hat mit der Variable x nichts zu tun. Es ist in C++ möglich, Funktionen so zu definieren, dass Parameter nicht kopiert werden, sondern auf die Originalvariable verweisen. Dies erfolgt mit Referenzvariablen. Um die Funktion set so zu definieren, dass der Wert von x geändert wird, muss lediglich der Referenzoperator & hinzugefügt werden. #include <iostream> void set(int &a) { a = 10; } int main() { int x = 5; set(x); std::cout << x << std::endl; } Wenn Sie den Code mit dem des vorherigen Beispielprogramms vergleichen: Der einzige Unterschied ist, dass vor dem Parameter a in der Funktionsdefinition von set nun der Referenzoperator & angegeben ist. Der Referenzoperator & bewirkt folgendes: Beim Aufruf der Funktion set wird nicht der Wert von x in die Variable a kopiert, sondern a erhält eine Referenz auf x. Das bedeutet, dass der Variablenname a in der Funktion set letztendlich nur ein anderer Name für die Variable x ist, es sich dabei jedoch tatsächlich um ein und dieselbe Variable handelt. Deswegen wird in diesem Beispiel nun tatsächlich die Variable x innerhalb von set auf den Wert 10 gesetzt, und auch 10 vom Programm auf den Bildschirm ausgegeben. Referenzvariablen können übrigens nicht nur in Funktionen verwendet werden. #include <iostream> 41 Funktionen int main() { int x = 5; int &y = x; ++y; std::cout << x << std::endl; } Obiges Programm definiert eine Variable x und eine Referenzvariable y. Die Variable x wird auf den Wert 5 gesetzt, die Referenzvariable auf die Variable x. Dies bedeutet, dass auch über y auf die Variable x zugegriffen werden kann - es handelt sich schlichtweg um einen anderen Namen für x. Daher wird mit dem Inkrementoperator ++ genaugenommen die Variable x um 1 erhöht, wie dann auch die Ausgabe von x auf den Bildschirm beweist: Dort wird die Zahl 6 angezeigt. Referenzvariablen werden in der Praxis nur innerhalb von Funktionsdefinitionen verwendet. Es macht normalerweise keinen Sinn, innerhalb einer Funktion eine Variable auch über andere Namen anzusprechen. Außerdem - und das ist eine große Beschränkung - muss einer Referenzvariablen direkt bei der Definition eine Variable zugewiesen werden, auf die referenziert werden soll. Somit verweist eine Referenzvariable während ihrer Lebensdauer immer auf die gleiche Variable. Mit Referenzvariablen lassen sich Mechanismen aus C, die dort nur über Zeiger möglich sind, einfach in C++ einsetzen. Aufgrund der Beschränkung, dass Referenzvariablen immer auf die gleiche Variable verweisen müssen, muss auch in C++ in bestimmten Situationen auf Zeiger zugegriffen werden. Zeiger haben den Vorteil, dass sie jederzeit mit dem Zugriffsoperator = neu gesetzt werden können und daher während ihrer Lebensdauer auf unterschiedliche Variablen zeigen können. Gültigkeitsbereiche Beschränkter Lebensraum Variablen, die innerhalb einer Funktion definiert werden, heißen lokale Variablen. Auf lokale Variablen kann nur innerhalb der Funktion zugegriffen werden, die die Variablen definiert. Andere Funktionen können auf diese Variablen nicht zugreifen. void a() { int x; } void b() { int x; } Nachdem lokale Variablen nur innerhalb einer Funktion sichtbar sind, können Sie denselben Variablennamen in unterschiedlichen Funktionen verwenden. Im obigen Beispiel wird eine Variable x in der Funktion a und in der Funktion b definiert. Nachdem es sich hierbei um lokale Variablen handelt und die Variablen nur in den jeweiligen Funktionen gelten, würde obiger Code einwandfrei kompilieren. Würden beide Variablendefinitionen in derselben Funktion stehen, würde dies zu einem Fehler führen, weil Variablennamen in einem Gültigkeitsbereich nur ein einziges Mal vergeben werden dürfen. In C++ gibt es beispielsweise im Gegensatz zu Java nicht nur lokale, sondern auch globale Variablen. Globale Variablen werden außerhalb einer Funktion definiert. int x; int main() 42 Funktionen { x = 10; } Im obigen Code-Beispiel wird eine Variable x als globale Variable definiert - die Definition steht außerhalb jeder Funktion. Während lokale Variablen nur einen begrenzten Gültigkeitsbereich besitzen, der sich nur über eine Funktion erstreckt, existieren globale Variablen im gesamten Programm. Das heißt, jede Funktion kann auf die globale Variable zugreifen, und zwar jederzeit. Globale Variablen existieren vom Programmstart an bis zum Ende, während lokale Variablen immer nur während der Ausführung der Funktion existieren. Lokale und globale Variablen lassen sich leicht unterscheiden: Lokale Variablen werden immer innerhalb geschweifter Klammern definiert. Ob diese Klammern einer Funktionsdefinition angehören oder nicht spielt hierbei keine Rolle. Sie können geschweifte Klammern beliebig setzen, um Gültigkeitsbereiche für Variablen festzulegen. int main() { { int y = 10; } { int y = 20; } } Im obigen Beispiel werden innerhalb der Funktion main zwei Variablen mit dem Namen y definiert. Dies ist nur deswegen möglich, weil die Variablen in unterschiedlichen Gültigkeitsbereichen definiert sind. Diese Gültigkeitsbereiche sind durch willkürliche Verwendung der geschweiften Klammern festgelegt worden, die wie Sie sehen nicht unbedingt einen Anweisungsblock einer Funktion oder einer Kontrollstruktur festlegen müssen. Aufgaben Übung macht den Meister 1. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe von zwei Zahlen auffordert. Das Programm soll daraufhin die Potenz der beiden Zahlen errechnen. Die Berechnung soll hierbei in einer eigenen Funktion stattfinden, die die beiden eingegebenen Zahlen vom Anwender als Parameter erwartet und das Ergebnis der Berechnung als Wert zurückgibt. Das Ergebnis der Berechnung soll dann auf den Bildschirm ausgeben werden. 2. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe einer Zahl auffordert. Das Programm soll daraufhin die Reihenfolge der Ziffern in der Zahl umdrehen - beispielsweise soll 1234 in 4321 umgewandelt werden. Das Umdrehen der Reihenfolge der Ziffern soll in einer eigenen Funktion erfolgen, die keinen Rückgabewert besitzt und einen Parameter erwartet. Die Ausgabe der umgedrehten Zahl auf den Bildschirm soll in der Funktion main erfolgen. 3. Für Experten: Schreiben Sie Ihre Lösung zur Aufgabe 2 so um, dass die Umkehrung der Ziffernreihenfolge durch Rekursion stattfindet. Rekursion bedeutet, dass Anweisungen nicht innerhalb einer Schleife wiederholt ausgeführt werden, sondern dass eine Funktion sich wiederholt selbst aufruft. 43 Kapitel 6. Strukturen Allgemeines Beschreibung In Ihren Beispielprogrammen haben Sie bereits eine Menge Datentypen verwendet: int zum Speichern von Zahlen, char zum Speichern von Zeichen, std::string zum Speichern von Wörtern oder Sätzen und so weiter. Was aber, wenn Sie beispielsweise eine Adressverwaltung entwickeln möchten und gerne einen Datentyp verwenden möchten, mit dem Sie Adressen speichern können? Genau für solche Fälle bieten sich Strukturen an. Definition Einen neuen Datentyp erstellen Bisher konnten Sie lediglich Variablen von Datentypen erstellen, die in C++ von Haus aus bekannt sind - die intrinsischen Datentypen - oder die Sie über Header-Dateien mit #include eingebunden und damit bekanntgemacht haben. Eine Struktur ist nun ein benutzerdefinierter Datentyp - ein Datentyp, den Sie selbst erstellen. Dieser Datentyp besteht genaugenommen aus Variablen anderer Datentypen. Sehen Sie sich folgendes Beispiel an, in dem ein Datentyp zur Speicherung einer Adresse erstellt wird. #include <string> struct adresse { std::string Anrede; std::string Vorname; std::string Nachname; std::string Strasse; int Hausnummer; int Postleitzahl; std::string Ort; std::string Land; }; Um einen neuen Datentyp zu erstellen, geben Sie zuerst das Schlüsselwort struct an. Hinter struct folgt der Name des neuen Datentyps. Im obigen Beispiel heißt der neue Datentyp also adresse. Hinter dem Namen werden in geschweiften Klammern nun eine Reihe von Variablen definiert, aus denen der neue Datentyp bestehen soll. Im obigen Code-Beispiel soll also eine Adresse Daten zu Anrede, Vorname, Nachname, Strasse, Hausnummer, Postleitzahl, Ort und Land speichern können. Demnach sind acht Variablen innerhalb der Struktur definiert, die alle den jeweils optimalen Datentyp besitzen, um die gewünschten Daten speichern zu können. Definitionen neuer Datentypen müssen immer außerhalb von Funktionen erfolgen. Das Schlüsselwort struct darf daher nie innerhalb einer Funktion stehen. Beachten Sie außerdem, dass hinter der geschlossenen geschweiften Klammer einer Struktur-Definition ein Semikolon stehen muss. Andernfalls meldet der Compiler einen Fehler. 44 Strukturen Anwendung Variable mit neuem Datentyp anlegen Eine Struktur muss genauso wie eine Funktion zuerst definiert werden, bevor sie angewandt werden kann. Die Definition haben Sie bereits kennengelernt, sie erfolgt über das Schlüsselwort struct. Ist eine Struktur definiert und damit ein neuer Datentyp erstellt worden, können Sie Variablen von diesem neuen Datentyp anlegen. Betrachten Sie hierzu folgendes Beispiel. #include <string> struct adresse { std::string Anrede; std::string Vorname; std::string Nachname; std::string Strasse; int Hausnummer; int Postleitzahl; std::string Ort; std::string Land; }; int main() { adresse MeineAdresse; } Innerhalb der Funktion main wird eine Variable namens MeineAdresse angelegt. Diese Variable erhält als Datentyp die darüber definierte Struktur, nämlich adresse. In dieser Variablen können also nun Daten vom Typ adresse abgelegt werden. Sie können in dieser Variablen beispielsweise Ihre Anschrift speichern. #include <string> struct adresse { std::string Anrede; std::string Vorname; std::string Nachname; std::string Strasse; int Hausnummer; int Postleitzahl; std::string Ort; std::string Land; }; int main() { adresse MeineAdresse; MeineAdresse.Anrede = "Herr"; MeineAdresse.Vorname = "Boris"; MeineAdresse.Nachname = "Schaeling"; MeineAdresse.Strasse = "Schlossallee"; MeineAdresse.Hausnummer = 1; MeineAdresse.Postleitzahl = 12345; 45 Strukturen MeineAdresse.Ort = "Entenhausen"; MeineAdresse.Land = "Deutschland"; } Der Zugriff auf Strukturen erfolgt genaugenommen nie auf die gesamte Struktur selber, sondern auf die einzelnen Bestandteile. Nachdem in der Struktur adresse Variablen wie Anrede, Vorname und Nachname definiert sind, greifen Sie im Code auch auf genau diese Bestandteile zu. Der genaue Zugriff sieht so aus, dass zuerst der Variablenname und dann der Zugriffsoperator angegeben wird. Der Zugriffsoperator ist ., also einfach ein Punkt. Hinter dem Zugriffsoperator wird dann der Name der Variablen angegeben, auf die innerhalb der Struktur zugegriffen werden soll. Eine Struktur stellt demnach lediglich eine Gruppierung verschiedener Variablen dar. Im Code selber wird mit den einzelnen Bestandteilen der Struktur gearbeitet. Für den Zugriff muss zwar jedesmal der Name der Strukturvariablen und der Zugriffsoperator angegeben werden. Trotzdem verhalten sich die einzelnen in der Struktur definierten Variablen so wie gewohnt. Sie können demnach mit diesen über den Zugriffsoperator angesprochenen Variablen genauso arbeiten wie Sie es bisher von anderen Variablen her kennen. Es besteht die Möglichkeit, Strukturen zu verschachteln und eine Strukturvariable als Bestandteil einer anderen Struktur zu definieren. Betrachten Sie folgendes Beispiel. #include <string> struct adresse { std::string Anrede; std::string Vorname; std::string Nachname; std::string Strasse; int Hausnummer; int Postleitzahl; std::string Ort; std::string Land; }; struct bestellung { int Kundennummer; adresse Adresse; std::string Ware; }; Die Struktur bestellung enthält eine Variable Adresse, die vom Typ adresse ist - also vom Typ einer anderen definierten Struktur. Auf diese Weise lassen sich Daten sehr gut strukturieren, was ja genau Sinn und Zweck von Strukturen ist. Der Zugriff auf beispielsweise den Nachnamen einer Adresse innerhalb einer Bestellung sieht wie folgt aus. bestellung MeineBestellung; MeineBestellung.Adresse.Nachname = "Schaeling"; Über den ersten Zugriffsoperator wird auf die Variable Adresse zugegriffen, die Bestandteil des Datentyps bestellung ist. Nachdem Adresse ihrerseits eine Struktur ist, wird über einen zweiten Zugriffsoperator auf den Nachnamen zugegegriffen, der als Bestandteil innerhalb der Struktur adresse definiert ist. Beachten Sie, dass die Verwendung von benutzerdefinierten Datentypen wie Strukturen voraussetzt, dass vor dem Anlegen der Variable in einer Datei der Datentyp mit struct definiert ist. Die Defi- 46 Strukturen nition des Datentyps muss also oberhalb der Variablendefinition erfolgen. Andernfalls meckert der Compiler. Enumerationen Datentyp mit benutzerdefinierten Werten Auch wenn Enumerationen keine Strukturen sind, so sollen sie dennoch in diesem Kapitel näher vorgestellt werden. Sie ermöglichen nämlich ähnlich wie Strukturen eine bessere problem- und lösungsorientierte Programmierung. Während mit dem Schlüsselwort struct Datentypen erstellt werden können, die Variablen zu einer Gruppe zusammenfassen, werden mit dem Schlüsselwort enum ganz bestimmte Werte für einen neuen Datentyp definiert. Sehen Sie sich dazu folgendes Beispiel an. enum land { Deutschland, Oesterreich, Schweiz }; Mit dem Schlüsselwort enum beginnt die Definition einer Enumeration. Hinter enum folgt der Name des neuen Datentyps - im obigen Beispiel lautet er land. Danach folgen wie bei der Struktur geschweifte Klammern, zwischen denen die eigentliche Definition des Datentypen steht. Beachten Sie, dass auch bei einer Enumeration hinter der geschlossenen geschweiften Klammer ein Semikolon angegeben werden muss. Was muss nun zwischen den geschweiften Klammern stehen? Eine Enumeration ist ein Datentyp, der auf ganz bestimmte Werte gesetzt werden kann. Die Werte, die für den neuen Datentyp gültig sind, werden einfach zwischen geschweiften Klammern angegeben und jeweils per Komma getrennt. Für den Datentyp land sind demnach die Werte Deutschland, Oesterreich und Schweiz gültig. Nachdem der Datentyp definiert ist, könnte er wie folgt in einem Programm verwendet werden. enum land { Deutschland, Oesterreich, Schweiz }; int main() { land Land = Deutschland; } Innerhalb der Funktion main wird eine Variable Land definiert, die vom Datentyp land ist. Diese Variable wird mit dem Wert Deutschland initialisiert. Sehen Sie genau hin: Der Wert Deutschland steht nicht in Anführungszeichen. Es handelt sich hierbei nicht um eine Zeichenkette, die der Variablen zugewiesen wird, sondern tatsächlich nur um den Wert Deutschland. Das ist deswegen möglich, weil der Datentyp land ausdrücklich den Wert Deutschland akzeptiert. Intern besteht eine Enumeration aus dem Datentyp int. Im obigen Beispiel ist also Land technisch betrachtet eine int-Variable. Den in einer Enumeration definierten Werten wird einfach intern jeweils eine Zahl zugeordnet. Der erste Wert erhält hierbei die Zahl 0, der zweite die Zahl 1, der dritte die Zahl 2 und so weiter. Wenn Sie möchten, dass die interne Darstellung der Werte nicht bei 0, sondern bei 100 beginnt, so können Sie dies direkt bei der Definition der Enumeration angeben. enum land { Deutschland = 100, Oesterreich, Schweiz }; 47 Strukturen Der erste Wert wird intern als Zahl 100 gehandhabt. Dem nächsten Wert Oesterreich wird die nächsthöhere Zahl zugewiesen - in diesem Fall also 101. Möchten Sie, dass Oesterreich eine andere Zahl erhält, müssen Sie diese wieder explizit angeben. enum land { Deutschland = 100, Oesterreich = 150, Schweiz }; Die Tatsache, dass eine Enumeration intern lediglich ein int ist, bedeutet, dass einer Variablen vom Typ einer Enumeration auch jede beliebige Zahl zugewiesen werden kann. enum land { Deutschland, Oesterreich, Schweiz }; int main() { land Land = static_cast<land>(0); } Ob Sie der Variablen Land im obigen Beispiel die Zahl 0 zuweisen oder den Wert Deutschland spielt keine Rolle - das Ergebnis ist das gleiche. Sie müssen jedoch die Zahl 0 explizit mit static_cast in einen Wert vom Typ land umwandeln, da land ein anderer Datentyp ist als int. Sie können Land jede beliebige Ganzzahl zuweisen - selbst dann, wenn die zugewiesene Zahl keinem Wert der Enumeration entspricht. Daher kompiliert auch folgendes Programm fehlerfrei. enum land { Deutschland, Oesterreich, Schweiz }; int main() { land Land = static_cast<land>(100); } Variablen vom Typ einer Enumeration können nicht nur als lokale oder globale Variablen definiert werden, sondern selbstverständlich auch innerhalb einer Struktur Verwendung finden. Aufgaben Übung macht den Meister 1. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe einer Bestellung auffordert. Der Anwender muss hierbei der Reihe nach die bestellte Ware, die Lieferanschrift, die Kreditkartennummer und das Gültigkeitsdatum der Kreditkarte eingeben. Die Daten sollen hierbei in geeigneten Variablen unter Zuhilfenahme benutzerdefinierter Strukturen und Enumerationen gespeichert werden. Lieferanschrift und Kreditkartendaten sollen jeweils in einer eigenen Struktur gespeichert werden. Die Bestellung wird ebenfalls in einer eigenen Struktur gespeichert, die sich der anderen beiden Strukturen bedient. Außerdem soll das Land, das in der Lieferanschrift gespeichert wird, vom Typ einer Enumeration sein. 2. Erweitern Sie die C++-Anwendung aus Aufgabe 1 dahingehend, dass der Anwender bis zu maximal zehn Bestellungen vornehmen kann, die jedoch alle an die gleiche Lieferanschrift gesendet und alle über die gleiche Kreditkarte abgerechnet werden. 48 Strukturen 3. Erweitern Sie die C++-Anwendung aus Aufgabe 2 dahingehend, dass Sie eine Funktion definieren, die keinen Rückgabewert liefert und einen einzigen Parameter erwartet. Diese Funktion rufen Sie auf, nachdem der Anwender seine Bestellung vorgenommen hat, und übergeben ihr die gesamte Bestellung. Die Funktion soll daraufhin die bestellten Waren, die Lieferadresse und die Kreditkartendaten auf den Bildschirm ausgeben. 49 Kapitel 7. Zeiger Allgemeines Anfänger mögen keine Zeiger Wenn Sie jemanden fragen, der momentan die Programmiersprache C oder C++ erlernt, was er von Zeigern hält, wird er sich entweder lautstark über Zeiger beschweren oder über Sie, weil Sie es gewagt haben, ihn an das Thema Zeiger zu erinnern. Viele Einsteiger in C und C++ halten Zeiger für einen der am schwierigsten zu erlernenden Bestandteile dieser Programmiersprachen und zerbrechen sich den Kopf darüber, Sinn und Zweck und die Funktionsweise von Zeigern zu verstehen. Woran liegt das? Wer Zeiger verstehen will, muss grundlegende Kenntnisse zur Funktionsweise des Computers besitzen. Sie müssen kein Experte für Chiptechnologien sein. Da Zeiger irgendwas mit Variablen zu tun haben, müssen Sie aber wissen, wie Variablen intern im Computer verwaltet werden. Es reicht nicht mehr aus, sich wie im Kapitel 2, Variablen unter Variablen irgendwelche Töpfe vorzustellen, sondern wir müssen einen zweiten Schritt machen, um Zeiger in unserem Variablen-Modell unterzubringen. Selbst wenn Sie als Hardware-Experte wissen, wie ein Computer intern funktioniert, verzweifeln Sie möglicherweise dennoch an Zeigern. Im Zusammenhang mit Zeigern fallen immer wieder Begriffe wie Variable, Adresse, Adressoperator, Verweisoperator und so weiter, die alle eine ganz klare Bedeutung haben und scharf getrennt werden müssen. Zu allem Überfluss sehen der Adress- und der Verweisoperator auch noch anderen Operatoren in C++ zum Verwechseln ähnlich. Einsteiger haben häufig Schwierigkeiten, den Zusammenhang zwischen diesen Begriffen zu verstehen und die Operatoren klar auseinanderzuhalten - und kommen so nur allzu leicht durcheinander. Um die interne Arbeitsweise des Computers im Hinblick auf Variablen nachvollziehen zu können und gleichzeitig Ordnung in das Begriffschaos zu bringen, bevor es ausbricht, wäre ein einfaches, einleuchtendes Modell hilfreich, das die Komplexität hinter Zeigern durchschaubar macht. Im nächsten Abschnitt steigen wir genau mit einem derartigen Modell in die Zeigerprogrammierung ein. Variablen-Modell Identifikation von Variablen auf zwei Wegen Um uns an Zeiger heranzutasten, beginnen wir mit einem kurzen Rückblick über Variablen. Im Kapitel 2, Variablen hatten Sie Variablen kennengelernt. Sie hatten gelernt, dass Variablen verwendet werden, um in einem Programm Informationen zu speichern. Unter Variablen haben Sie sich ganz einfach Töpfe vorstellen können, wobei jeder einzelne Topf jeweils eine Information speichern kann. Folgende Grafik zeigt vier Variablen in Form von Töpfen. Um jeweils auf eine bestimmte Variable zugreifen zu können, ist es wichtig, Variablen identifizieren zu können. Dies geschieht über Variablennamen. In obiger Grafik heißen die vier Töpfe A, B, C und 50 Zeiger D. Die Variablennamen sind auf den Töpfen aufgeklebt, um auf diese Weise die Töpfe identifizieren zu können. Nachdem Sie gelernt hatten, was Variablen sind, wurden Sie mit Datentypen vertraut gemacht. Ein Datentyp gibt an, welche Art von Information in einer Variablen gespeichert werden kann. Je nach Datentyp kann eine Variable zum Beispiel nur ganze Zahlen speichern, Wahrheitswerte oder Zeichenketten. Es gibt in C++ also keine Variablen, die jede beliebige Art von Information speichern können, sondern nur strengtypisierte Variablen: In dem Moment, in dem Sie eine Variable anlegen - sprich einen Topf herbeizaubern - müssen Sie über die Angabe des Datentypen gleichzeitig festlegen, welche Art von Information in der Variable gespeichert werden können soll. Die Grafik berücksichtigt nun Datentypen. Es handelt sich bei den vier Variablen nun nicht mehr um die gleichen Töpfe, die gleiche Informationen speichern können, sondern jeder Topf ist unterschiedlich und kann unterschiedliche Informationen speichern. In der Grafik wird dies durch die unterschiedlichen Farben der Töpfe dargestellt. Um die Bedeutung der Farbe zu verdeutlichen, ist außerdem über den Töpfen jeweils ein anderer Datentyp angegeben: Variable A ist vom Typ int und kann Ganzzahlen speichern. Variable B ist vom Typ bool und kann Wahrheitswerte speichern. Variable C besitzt den Datentyp std::string aus dem C++-Standard. Der Datentyp std::string ermöglicht die Speicherung beliebiger Zeichenketten, also zum Beispiel von Wörtern, Sätzen oder ganzen Büchern. Die Variable D basiert letztendlich auf einem Datentyp namens adresse, der vom Programmierer zum Beispiel in Form einer Struktur selbst erstellt werden müsste. Werden die Datentypen wie in der Grafik angegebenen den vier Töpfen zugewiesen, bedeutet das zum Beispiel, dass die Information 169 - also die Ganzzahl 169 - im blauen Topf gespeichert werden könnte, nicht aber in den anderen drei Töpfen. Die Ganzzahl 169 ist nur mit dem Datentypen int kompatibel, nicht mit den Datentypen bool, std::string und adresse. int A; bool B; std::string C; adresse D; A = 169; Damit Sie vor lauter Töpfen den eigentlichen C++-Code nicht vergessen, sehen Sie oben, wie die in der Grafik angegebenen vier Variablen in C++ angelegt werden und in der Variablen A die Zahl 169 gespeichert wird. Der Datentyp adresse müsste selbstverständlich vom Programmierer zuerst definiert werden, bevor er zur Definition der Variablen D verwendet werden könnte. Auch für den Datentyp std::string gilt, dass er erst bekannt gemacht werden muss, bevor er verwendet werden kann. Bis jetzt sollte Ihnen alles bekannt vorkommen. Sehen Sie sich nun nochmal obige Grafik mit den vier farbigen Töpfen an. Jeder Topf kann über seinen Namen, hier jeweils in Form eines Buchstabens, identifiziert werden. Gucken Sie sich die Grafik genau an. Gibt es noch eine andere Möglichkeit, Töpfe zu identifizieren? 51 Zeiger Es gibt sie! Töpfe stehen an ganz bestimmten Positionen, und über die Positionsnummer kann ebenfalls ein Topf ganz klar identifiziert werden. So steht in der Grafik Topf A an Position 0, Topf B an Position 1, Topf C an Position 2 und Topf D an Position 3. Die Positionsnummer ist jeweils unter den Töpfen in der Grafik angegeben. Wenn Sie also nun die Zahl 169 in einem Topf speichern wollen, können Sie sie entweder in den Topf A ablegen oder aber in den Topf, der an Position 0 steht. Egal, ob Sie auf Topf A oder auf den Topf an Position 0 zugreifen, in beiden Fällen würde auf den gleichen blau angestrichenen Topf zugegriffen werden. Es gibt also in C++ (wie auch in C) zwei Möglichkeiten, Variablen eindeutig zu identifizieren: Über ihren Namen und über ihre Position. Dass Variablen tatsächlich über ihre Position identifiziert werden können, liegt an der internen Arbeitsweise des Computers. Wann immer Sie eine Variable anlegen, muss der Computer Speicherplatz für diese Variable bereitstellen. Denn wenn Sie eine Information in die Variable legen, muss diese Information ja vom Computer tatsächlich irgendwo gespeichert werden. Speicherplatz wird für Variablen im RAM reserviert. RAM steht für Random Access Memory. Die Abkürzung RAM kennen Sie möglicherweise aus Anzeigen von Computerherstellern: Die Größe des RAMs, gemessen in Megabyte, ist eine der technischen Eckdaten, auf die beim Kauf von Computern geachtet werden muss. So sind heutige Computer zum Beispiel häufig mit rund 1000 Megabyte RAM ausgerüstet - je mehr, desto besser. Wenn Sie eine Variable anlegen, wird für diese vom Computer im RAM Speicherplatz reserviert. Mal angenommen, Sie legen eine Variable vom Typ int wie folgt an. int A; Eine int-Variable ist 4 Byte groß. In Ihrem rund 1000 Megabyte großen RAM werden also mit obiger Zeile 4 Byte für die Variable A reserviert. Während Sie als Programmierer praktischerweise mit dem Variablennamen A arbeiten können, um auf die reservierten 4 Byte zuzugreifen und dort eine Ganzzahl abzulegen, muss der Computer sich jedoch merken, wo genau in den 1000 Megabyte des RAMs eigentlich die 4 Byte für die Variable A liegen. Und das geschieht über die Position der 4 Byte im RAM. Sie sehen, das Modell mit den Töpfen und den Positionsnummern ist nicht aus der Luft gegriffen. So wie die vier Töpfe über die Buchstaben A, B, C und D oder aber über ihre Positionsnummer identifiziert werden können, können auch Variablen im RAM über den Variablennamen oder aber über ihre Position identifiziert werden. Im Allgemeinen zieht man den Variablennamen zur Identifikation vor, weil man ihn sich nicht nur besser merken kann als irgendeine Nummer, sondern weil er auch viel aussagekräfter ist. Warum man dennoch Positionsnummern verwendet und manchmal sogar verwenden muss, und was Positionsnummern eigentlich mit Zeigern zu tun haben, werden Sie im Laufe dieses Kapitels erfahren. Wenn im nächsten Abschnitt auf Begriffe wie Adressen und Zeiger eingegangen wird, halten Sie sich immer unser Variablen-Modell mit den vier Töpfen vor Augen. Das Modell soll Ihnen helfen, die Zusammenhänge nicht aus den Augen zu verlieren. Wenn das klappt, ist für Sie auch das etwas knifflige Thema dieses Kapitels rund um Zeiger kein Problem. 52 Zeiger Adressen und Zeiger Positionsnummern in Variablen speichern und über sie auf Variablen zugreifen Sehen Sie sich nochmal die Grafik mit den vier Töpfen und Positionsnummern an. Jeder Topf kann über einen Namen und über seine Positionsnummer identifiziert werden. Wie Sie nun wissen gilt dies ebenfalls für Variablen. Wo bekommen Variablen ihren Namen und ihre Positionsnummer her? Den Variablennamen vergeben Sie. Sie als Programmierer legen bei der Definition einer Variablen nicht nur den Datentyp fest, sondern auch immer den Variablennamen. Sie haben also die alleinige Kontrolle über Variablennamen und können Variablen nennen, wie Sie wollen. Die Positionsnummer einer Variablen wird hingegen vom Computer vergeben. Wie Sie im vorherigen Abschnitt erfahren haben, reserviert der Computer im RAM Speicherplatz für eine Variable. Wo genau im RAM dieser Speicherplatz reserviert wird, entscheidet ganz allein der Computer. Sie als Programmierer können die Positionsnummer für Variablen jedenfalls nicht vergeben. Die Programmiersprachen C und C++ ermöglichen es Ihnen jedoch, die Positionsnummer einer Variablen im RAM abzurufen. Während Sie also eine Positionsnummer nicht selber vergeben können, können Sie so die Positionsnummer einer Variablen immerhin in Erfahrung bringen. Dies geschieht mit Hilfe des Adressoperators &. Das & heißt deswegen Adressoperator, weil in den Programmiersprachen C und C++ genaugenommen nicht von Positionsnummern die Rede ist, sondern von Adressen. Variablen besitzen also jeweils eine ganz bestimmte Adresse, und diese Adresse können Sie mit dem Adressoperator & abfragen. In diesem Kapitel wird auch weiterhin häufig von Positionsnummern die Rede sein, weil dies unserem praktischen Vorstellungsvermögen etwas mehr entgegenkommt als der Begriff Adresse. Möglicherweise wundern Sie sich, dass der Adressoperator & genauso aussieht wie der Operator für die bitweise UND-Verknüpfung, der im Kapitel 3, Operatoren vorgestellt wurde. Tatsächlich kann das & sowohl Adressoperator als auch bitweise UND-Verknüpfung bedeuten. Es ist nur aus dem Zusammenhang im Quellcode erkennbar, ob das & an einer bestimmten Stelle im Code nun tatsächlich den Adressoperator meint oder aber die bitweise UND-Verküpfung. Grundsätzlich gilt: Die bitweise UND-Verknüpfung ist ein binärer Operator, der links und rechts vom & einen Wert oder eine Variable erwartet. Beim Adressoperator steht links vom & niemals ein Wert oder eine Variable. Sie werden nach einiger Übung die beiden Operatoren aber automatisch auseinanderhalten können. #include <iostream> int main() { int A; std::cout << &A << std::endl; } 53 Zeiger Im obigen Code-Beispiel wird eine Variable vom Typ int angelegt. Der Variablenname ist vom Programmierer auf A gesetzt worden. Wenn das Programm gestartet wird, reserviert der Computer für die Variable A im RAM Speicherplatz. Es werden genau 4 Byte reserviert, da der Datentyp der Variablen A int ist und ein int in C++ per Definition immer 4 Byte groß ist. Wo jedoch diese 4 Byte im RAM genau reserviert werden, können wir im Code nicht festlegen. Das entscheidet allein der Computer. Nachdem die Variable A definiert und der Speicherplatz im RAM reserviert ist, geben wir in der nächsten Zeile die Positionsnummer der Variablen A auf die Standardausgabe aus. Um auf die Positionsnummer einer Variablen zugreifen zu können, muss man den Adressoperator & dem Variablennamen voranstellen. Auf diese Weise wird die Positionsnummer des 4-Byte-Blocks im RAM auf den Monitor ausgegeben. Der Compiler erkennt im Zeichen & den Adressoperator, weil vor ihm kein Wert und keine Variable steht, wie es die bitweise UND-Verknüpfung verlangen würde. Die Positionsnummer hängt davon ab, an welcher Stelle der Computer die int-Variable im RAM angelegt hat. Es ist nicht möglich, die Positionsnummer vorherzusagen. Wenn Sie das Programm mehrmals oder auf verschiedenen Computern starten, stellen Sie fest, dass bei jedem Programmstart unter Umständen eine andere Positionsnummer ausgegeben wird. Sie haben als Programmierer keinerlei Einfluß auf die Position des reservierten Speichers einer Variablen im RAM. Nachdem Sie die Positionsnummer der Variablen A auf den Monitor ausgegeben haben, werden Sie im nächsten Code-Beispiel die Positionsnummer zuerst in einer anderen Variablen speichern und dann ausgeben. Positionsnummern scheinen bisher nur ganzzahlige Werte gewesen zu sein. Um also eine Positionsnummer in einer Variablen speichern zu können, könnte man auf die Idee kommen, dieser Variablen zum Beispiel den Datentyp short oder int zu geben - denn diese Datentypen sind ja mit ganzzahligen Werten kompatibel. Leider funktioniert das nicht. Und um zu verstehen, warum das nicht funktioniert, wird es jetzt etwas knifflig. Aber immer schön der Reihe nach. #include <iostream> int main() { int A; int* a = &A; std::cout << a << std::endl; } Wie im vorherigen Code-Beispiel wird wieder eine Variable A vom Typ int angelegt. In der zweiten Zeile wird ebenfalls wieder über den Adressoperator & auf die Positionsnummer der Variablen A im RAM zugegriffen. Diese Positionsnummer wird nun jedoch nicht mehr auf den Monitor ausgegeben, sondern einer anderen Variablen namens a zugewiesen. Der Datentyp der Variablen a sieht hierbei etwas ungewöhnlich aus. Das int kennen Sie ja. Was aber soll int* bedeuten? 54 Zeiger Werfen wir wieder einen Blick auf unser Variablen-Modell mit den vier Töpfen. Mit dem Adressoperator & erhalten wir jeweils die Positionsnummer einer Variablen. Die Positionsnummern sind unterhalb der vier Töpfe A, B, C und D angegeben. Wenn Sie sich die Positionsnummern ansehen, stellen Sie fest, dass es sich hierbei scheinbar ausschließlich um Ganzzahlen handelt. Das macht auch Sinn, da eine Variable entweder an der Position 0 oder an der Position 1 steht, nicht aber an der Position 0,5. Wenn man nun die Positionsnummer einer Variablen, die man mit dem Adressoperator & ermittelt, in einer anderen Variablen speichern will - genau das wollen wir im obigen Code-Beispiel ja tun - könnte man meinen, dass der geeignete Datentyp für die Speicherung von Positionsnummern zum Beispiel int ist - alle Positionsnummern sind ja schließlich Ganzzahlen. Leider ist das nun nicht so einfach. Es stimmt zwar, dass alle Positionsnummern Ganzzahlen sind. Der Datentyp einer Positionsnummer hängt jedoch davon ab, welchen Datentyp die Variable hat, die an der Position steht. Sehen wir uns zum Beispiel Topf A an. Topf A hat den Datentyp int. Die Positionsnummer von Topf A ist 0. 0 ist eigentlich eine Ganzzahl, und Ganzzahlen können ja eigentlich in Variablen vom Typ int gespeichert werden. Da unsere 0 aber keine gewöhnliche Ganzzahl ist, sondern eine Positionsnummer, hängt der Datentyp unserer 0 mit dem Datentyp der Variablen zusammen, die an der Position 0 steht - also mit dem Datentyp des Topfes A. Der Datentyp von Topf A ist int. Indem nun hinter diesem Datentypen ein * gesetzt wird, erhalten Sie den Datentyp für die Positionsnummer 0. Eine Variable a, die in unserem Beispiel die Positionsnummer 0 speichern soll, muss den Datentyp int* erhalten, da an der Position 0 eine int-Variable steht. Das ist der Grund, warum in obiger Grafik unterhalb des Topfes a, der die Positionsnummer 0 von Topf A speichert, der Datentyp int* angegeben ist. Wie sieht es mit Topf B aus? Topf B hat den Datentyp bool. Die Positionsnummer ist 1. 1 ist zwar eine Ganzzahl. Da in diesem Fall 1 aber eine Positionsnummer ist, kann nicht einfach der Datentyp int zur Speicherung dieser Positionsnummer verwendet werden. Stattdessen ist es wieder notwendig, den Datentyp der Variablen anzusehen, der an Position 1 steht. An Position 1 steht eine bool-Variable. Also ist der Datentyp, den wir brauchen, um die Positionsnummer 1 zu speichern, bool*. Die Variable b, die die Positionsnummer 1 erhält, besitzt den Datentyp bool*. Um in einer Variablen die Positionsnummer 2 zu speichern, muss in unserem Beispiel diese Variable den Datentyp std::string* erhalten. Für den Topf D müsste der Datentyp einer Variablen zur Speicherung der Positionsnummer 3 adresse* lauten. #include <iostream> int main() { int A; int* a = &A; std::cout << a << std::endl; } Werfen wir wieder einen Blick auf das Code-Beispiel. Dort ist eine Variable A vom Typ int definiert worden. Die Positionsnummer wird in der nächsten Zeile mit dem Adressoperator & ermittelt. Um diese Positionsnummer in einer Variablen a speichern zu können, muss diese Variable den richtigen Datentyp besitzen. Auch wenn Positionsnummern grundsätzlich immer Ganzzahlen sind, ist es nicht richtig, der Variablen a den Datentyp int zu geben. Denn der Datentyp einer Positionsnummer hängt vom Datentyp der Variablen ab, die an der jeweiligen Position steht. Der Variablen a wird die Positionsnummer von A zugewiesen. An der Positionsnummer von A steht logischerweise die Variable A. Die Variable A hat den Datentyp int. Daraus folgt, dass a den Datentyp int* haben muss. So schwer ist das nicht, oder? In der letzten Zeile wird der Wert in a, also die Positionsnummer von A, auf die Standardausgabe ausgegeben. Das Programm macht also letztendlich das gleiche wie das Programm zuvor, speichert die Positionsnummer jedoch zuerst in der Variablen a, bevor sie ausgegeben wird. 55 Zeiger Vergessen Sie nicht, dass die tatsächlich ausgegebene Zahl jeweils unterschiedlich sein kann, da bei jeder Programmausführung allein der Computer darüber bestimmt, wo genau im RAM der Speicher für die Variable A reserviert wird. Vielleicht fragen Sie sich, warum eigentlich der Datentyp von Variablen, die Positionsnummern speichern, nicht einfach int sein kann? Positionsnummern sind immer Ganzzahlen, und trotzdem muss man scheinbar umständlicherweise immer auf den Datentyp der Variablen achten, um dessen Positionsnummer sich alles dreht, und das * dahintersetzen. Warum Sie Positionsnummern nicht in Variablen vom Typ int speichern können, werden Sie anhand des folgenden Code-Beispiels verstehen. #include <iostream> int main() { int A; int* a = &A; *a = 169; std::cout << a << std::endl; std::cout << A << std::endl; } Das Code-Beispiel wurde um zwei Zeilen ergänzt. Nachdem in der Variablen a die Positionsnummer der Variablen A gespeichert wurde, wird in der nächsten Zeile irgendwie auf a zugegriffen und die Zahl 169 zugewiesen. Wenn Sie genau hingucken, sehen Sie, dass die Zahl 169 nicht a zugewiesen wird, sondern *a. Es taucht also schon wieder das ominöse * auf. Was soll das *a aber bedeuten? Die Variable a speichert die Positionsnummer von A. Wie Sie von unserem Variablen-Modell zu Beginn des Kapitels wissen, kann man auf Variablen zugreifen, indem man sie entweder über ihren Namen identifiziert oder indem man über die Positionsnummer auf die Variable zugreift. Letzteres ist genau das, was im Code-Beispiel passiert. Indem der Variablen a das * vorangestellt wird, greift man auf die Variable zu, die an der Positionsnummer steht, die in a gespeichert ist - also auf A. Die Zahl 169 wird also tatsächlich der Variablen A zugewiesen. Wenn Sie das nicht glauben, führen Sie obiges Beispiel aus. Die letzte Zeile gibt den Inhalt der Variablen A auf den Monitor aus, nämlich 169. Die folgenden beiden Code-Zeilen wären also im obigen Programm völlig identisch. In beiden Fällen würde das gleiche passieren: Die Zahl 169 wird der Variablen A zugewiesen. A = 169; *a = 169; Warum ist es nun wichtig, dass der Datentyp der Variablen a sich nach dem Datentyp der Variablen richtet, dessen Positionsnummer gespeichert wird? Der Grund ist ganz einfach: Woher sollte sonst der Computer wissen, wie groß der reservierte Speicherblock ist, dessen Positionsnummer Sie im Code verwenden? Folgender Code wird Ihnen das näher erläutern. int A; int* a = &A; *a = 169; short S; short* s = &S; *s = 169; Im obigen Code werden zwei Variablen A und S angelegt - die eine vom Typ int, die andere vom Typ short. Es werden zwei Variablen a und s definiert, denen mit dem Adressoperator die Positionsnummern von A und S zugewiesen werden. Dann wird über die Verwendung des * und der Positionsnummern in a und s auf die Variablen A und S zugegriffen und dort jeweils der Wert 169 gespeichert. Die Variable a besitzt den Datentyp int*, die Variable s besitzt den Datentyp short*. Beide Variablen speichern eine Ganzzahl, nämlich eine Positionsnummer. Obwohl beide Variablen a und s also 56 Zeiger eigentlich den gleichen Typ haben könnten - nämlich zum Beispiel int - darf das nicht der Fall sein. Denn wenn Sie mit *a auf eine Variable zugreifen und mit *s auf eine andere Variable, dann greifen Sie im ersten Fall auf einen 4-Byte-Block im RAM zu, im zweiten Fall auf einen 2-Byte-Block. Denn die Variable a speichert die Positionsnummer einer int-Variablen, und die Variable s speichert die Positionsnummer einer short-Variablen. Ein int ist per Definition 4 Byte groß, ein short 2 Byte. Wenn die Variablen, die Positionsnummern speichern, nicht die richtigen Datentypen besitzen würden - also gemäß den Datentypen der Variablen, deren Positionsnummern gespeichert werden - weiß der Computer nicht, wie groß eigentlich der reservierte Speicherblock an der Position ist, auf die Sie mit *a und *s zugreifen. Die Größe des jeweils reservierten Speicherblocks hängt vom Datentypen ab, und dieser muss mit in die Definition von Variablen einfließen, die Positionsnummern speichern. Das ist der Grund für die auf den ersten Blick merkwürdigen Datentypen mit dem *. Das * wird übrigens Verweisoperator genannt. Und hier kommt auch endlich der Begriff des Zeigers ins Spiel: Denn über den Verweisoperator lassen sich Zeiger ganz einfach erkennen. Der Datentyp eines Zeigers besitzt immer auch den Verweisoperator. Sie sehen also, dass in den Beispielen soeben bereits Zeiger eingesetzt wurden. Der Verweisoperator bedeutet, dass eine Variable eine Positionsnummer enthält und über diese Positionsnummer auf eine andere Variable verweist. Man könnte auch sagen auf eine andere Variable zeigt - daher der Begriff Zeiger. Indem der Verweisoperator wie im obigen Beispiel vor die Variable a gestellt wird, wird über die Variable a und die in ihr gespeicherte Positionsnummer auf die Variable A verwiesen bzw. auf die Variable A gezeigt. Die Zahl 169 wird deswegen tatsächlich in A gespeichert. Würden Sie den Verweisoperator weglassen, würde 169 in a gespeichert werden - Sie würden also die Positionsnummer in a ändern. a = 169; *a = 169; Um sicherzugehen, dass Sie *a verstanden haben, sehen Sie sich obige beiden Zeilen an. In der ersten Zeile wird einer Variablen a die Zahl 169 zugewiesen. a ist also der Name eines Topfes, in dem die Zahl 169 landet. In der zweiten Zeile wird hingegen auf eine Variable zugegriffen, die an der Positionsnummer steht, die in der Variable a gespeichert ist. Man spricht davon, dass auf eine Variable zugegriffen wird, auf die a zeigt. Die Zahl 169 landet also in irgendeiner Variablen, deren Positionsnummer vorher in a gespeichert wurde. Die zweite Zeile ändert die Variable a nicht. Sie haben jetzt die beiden Operatoren kennengelernt, die im Zusammenhang mit Zeigern eingesetzt werden: & ist der Adressoperator, um die Positionsnummer einer Variablen zu ermitteln. * ist der Verweisoperator, um über eine Variable, in der eine Positionsnummer gespeichert ist, auf eine entsprechende Variable im RAM zuzugreifen. Die Variable, die die Positionsnummer einer anderen Variable speichert, heißt Zeiger. Sie heißt deswegen so, weil sie über ihren Zahlenwert, den sie speichert, auf eine Position im RAM zeigt, an der eine andere Variable steht. Mehr gibt es zu Zeigern erstmal nicht zu sagen. Im nächsten Abschnitt sollen ein paar Code-Beispiele Ihnen den Einsatz von Zeigern näherbringen, damit Sie sich an die Verwendung der Operatoren & und * gewöhnen. Praxis-Beispiele Mit Adressen und Zeigern rumspielen Nachdem Sie in die Funktionsweise von Zeigern eingeführt wurden, fragen Sie sich möglicherweise, was das ganze eigentlich soll: Zeiger scheinen keinen wirklichen Nutzen zu bieten, da Variablen ja so wie bisher auch über ihren Namen identifiziert werden können - was wesentlich unkomplizierter ist. Folgendes Code-Beispiel soll Ihnen zeigen, wann Zeiger zum Beispiel nützlich sein können. #include <iostream> #include <string> void func(std::string* s) { 57 Zeiger *s = "Hallo, Welt!"; } int main() { std::string str; func(&str); std::cout << str << std::endl; } Im obigen Code-Beispiel wird eine Funktion func definiert, die als einzigen Parameter einen Zeiger auf einen std::string erwartet. Man könnte auch sagen, sie erwartet die Positionsnummer einer Variablen, die den Datentyp std::string besitzt. Beim Aufruf dieser Funktion wird daher innerhalb von main auch nicht direkt die Variable str an func übergeben, sondern &str - also die Positionsnummer von str. Was passiert nun? Die Positionsnummer von str, die mit &str ermittelt wird, wird als Kopie an die Funktion func übergeben und dort im Parameter s gespeichert. Sie erinnern sich an Kapitel 5, Funktionen, in dem wir gesagt haben, dass in C++ standardmäßig Werte an eine Funktion immer als Kopie übergeben werden. Wenn nun die Variable s in der Funktion func die Positiosnummer von str speichert, dann kann über *s auf die Variable zugegriffen werden, deren Positionsnummer in s gespeichert ist - also natürlich auf str. Genau das passiert auch innerhalb von func: Dort wird über *s auf die Variable str zugegriffen und ihr die Zeichenkette "Hallo, Welt!" zugewiesen. Wenn Sie das Beispiel ausführen, sehen Sie, dass tatsächlich diese Zeichenkette in der letzten Zeile auf den Bildschirm ausgegeben wird. Mit Zeigern ist es also möglich, in Funktionen auf Variablen zuzugreifen, die in anderen Funktionen definiert sind. Eine Funktion kann eine Variable verändern, die eigentlich nur in einer anderen Funktion verfügbar ist. Obwohl im obigen Beispiel die Variable str nur innerhalb von main verwendet werden kann, kann durch die Weitergabe der Adresse dieser Variablen an eine andere Funktion diese ebenfalls mit str arbeiten. Aber kennen wir das nicht schon längst? Im Kapitel 5, Funktionen hatten wir doch den Referenzoperator & kennengelernt, mit dem es möglich ist, Werte an Funktionen nicht als Kopie, sondern als Original zu übergeben. In diesem Fall kann eine Funktion ebenfalls eine Variable verändern, die in einer anderen Funktion definiert ist. Obiges Code-Beispiel könnte also auch genauso gut mit Hilfe des Referenzoperators geschrieben werden und sähe dann wie folgt aus. #include <iostream> #include <string> void func(std::string& s) { s = "Hallo, Welt!"; } int main() { std::string str; func(str); std::cout << str << std::endl; } Wenn Sie das Code-Beispiel ausführen, stellen Sie fest, dass es genauso funktioniert. Zeiger zu verwenden, um anderen Funktionen Zugriff auf lokale Variablen zu verschaffen, macht keinen Sinn, da dies mit Referenzvariablen viel einfacher funktioniert. 58 Zeiger Das war nicht immer so. Wenn man sich die Entwicklungsgeschichte der Programmiersprache C+ + ansieht, stellt man fest, dass Zeiger schon sehr lange Bestandteil dieser Programmiersprache sind, Referenzen jedoch erst später eingeführt wurden. Früher gab es also nur die Möglichkeit, Zeiger zu verwenden, wenn man anderen Funktionen Zugriff auf lokale Variablen verschaffen wollte. Heutzutage verwendet man in so einem Fall besser wie eben gesehen Referenzen. Ein anderer Fall, in dem Zeiger verwendet werden könnten, ist eine Funktion, die dem Aufrufer zwei oder mehr Werte als Ergebnis zurückgeben soll. Wie Sie wissen besitzen Funktionen in C++ nur einen einzigen Rückgabewert. Mit Zeigern kann dieses Problem aber gelöst werden. #include <iostream> int func(int* i) { int j = *i; *i = 2 * j; return j / 2; } int main() { int zahl1 = 10, zahl2; zahl2 = func(&zahl1); std::cout << zahl1 << ", " << zahl2 << std::endl; } Die Funktion func erwartet einen Zeiger auf eine Ganzzahl. Diese Ganzzahl wird von func einmal verdoppelt und einmal halbiert. Sowohl der verdoppelte als auch der halbierte Wert sollen dann von func an den Aufrufer dieser Funktion zurückgegeben werden. Der durch 2 geteilte Wert wird wie gewohnt mit return zurückgegeben. Der mit 2 multiplizierte Wert wird hingegen in der Variablen gespeichert, auf die i zeigt. Der Parameter i erhält im obigen Code-Beispiel die Adresse der Variablen zahl1. Somit wird mit *i in der zweiten Zeile in func auf die Variable zahl1 zugegriffen und in dieser Variablen der verdoppelte Wert gespeichert. Wahrscheinlich werden Sie einwenden, dass das Beispiel ohne Probleme mit Referenzen geschrieben werden könnte und man auf Zeiger ganz verzichten könnte - Sie haben Recht. Auch hier würde in modernem C++-Code der Einsatz von Referenzen vorgezogen werden. Aber keine Angst: Sie erlernen den Einsatz von Zeigern nicht nur aus historischen Gründen. Zum einen existiert enorm viel C-Code, auf den Sie als C++-Programmierer möglicherweise zugreifen wollen so bieten zum Beispiel die Betriebssysteme Windows und Linux eine riesige Zahl an C-Funktionen an, die Sie direkt in C++ aufrufen können. Dummerweise kennt C aber keine Referenzen, so dass Sie hier um den Einsatz von Zeigern nicht herumkommen. Zum anderen gibt es auch in C++ Situationen, in denen der Einsatz von Zeigern zwingend ist und Sie Zeiger nicht durch Referenzen ersetzen können. Solche Situationen werden Sie im nächsten Abschnitt kennenlernen. Dynamische Speicherallokation Computer, wir brauchen Speicher Bisher haben Sie in diesem Kapitel eigentlich nur Beispiele kennengelernt, in denen Zeiger ohne Probleme durch Referenzen ersetzt werden können. Jeder C++-Programmierer wendet im Zweifelsfall auch lieber Referenzen als Zeiger an, da sie schlichtweg einfacher einzusetzen sind. In diesem Abschnitt, in dem es um die dynamische Speicherallokation geht, kommen wir um die Anwendung von Zeigern aber nicht herum. 59 Zeiger Die dynamische Speicherallokation wird in C++ wie auch in C dann angewendet, wenn Sie während der Entwicklung Ihres Programms nicht wissen, wieviel Speicher später der Anwender Ihres Programms tatsächlich benötigt. Stellen Sie sich zum Beispiel vor, Sie entwickeln eine Adressverwaltung. In einem ersten Schritt erstellen Sie eine Struktur, die Adressen speichern kann. In einem zweiten Schritt erstellen Sie ein Array vom Typ Ihrer Struktur, um in diesem Array eine bestimmte Anzahl an Adressen speichern zu können. Ihr Code könnte demnach zum Beispiel wie folgt aussehen. #include <string> struct adresse { std::string Name; std::string Ort; std::string Telefonnummer; }; adresse Adressen[100]; Im obigen Code gibt es ein fundamentales Problem: Ihr Array Adressen vom Typ der Struktur adresse besteht aus 100 Elementen, was Sie einfach mal so festgelegt haben. Woher wissen Sie aber, dass der Anwender Ihrer Adressverwaltung tatsächlich 100 Adressen speichern will? Vielleicht möchte er lediglich 5 Adressen speichern. In diesem Fall würde eine unglaubliche Speicherverschwendung stattfinden, weil 95 Elemente in Ihrem Array vom Anwender gar nicht verwendet werden. Vielleicht möchte der Anwender jedoch 150 Adressen speichern. Das lässt Ihre Anwendung aber nicht zu, da nur Platz für 100 Adressen zur Verfügung steht. Die dynamische Speicherallokation hilft nun aus. Denn sie erlaubt es, während der Programmausführung festzustellen, wieviel Speicher vom Programm benötigt wird und beschafft werden muss. Das heißt, Sie als Entwickler geben nicht mehr statisch zum Kompiliervorgang vor, wieviel Speicher reserviert werden muss, sondern Ihr Programm ermittelt dynamisch zur Laufzeit, wieviel Speicher benötigt wird und beschafft werden muss. Ihre Adressverwaltung könnte nun so umgeschrieben werden, dass sie nicht automatisch Platz für 100 Adressen zur Verfügung stellt, sondern den Anwender beim Programmstart fragt, wie viele Adressen er speichern möchte. Nachdem der Anwender eine Zahl eingegeben hat, reservieren Sie exakt die Menge an Speicherplatz, die für die gewünschte Anzahl zu speichernder Adressen benötigt wird. #include <string> #include <iostream> struct adresse { std::string Name; std::string Ort; std::string Telefonnummer; }; int main() { int i; std::cout << "Wie viele Adressen moechten Sie speichern? " << std::flush; std::cin >> i; adresse* Adressen = new adresse[i]; for (int j = 0; j < i; ++j) { 60 Zeiger Adressen[j].Name = "Boris Schaeling"; Adressen[j].Ort = "Entenhausen"; Adressen[j].Telefonnummer = "1234567890"; } delete[] Adressen; } Im obigen Code-Beispiel wird der Anwender zuerst gefragt, wie viele Adressen er speichern möchte. Die Zahl, die der Anwender eingibt, wird in der Variablen i gespeichert. Der Wert in der Variablen i wird dann verwendet, um genau die benötigte Menge an Speicherplatz zu reservieren. Dies geschieht im Code-Beispiel mit folgender Anweisung. adresse* Adressen = new adresse[i]; In dieser Zeile taucht ein Schlüsselwort auf, das Sie bisher noch nicht kennengelernt haben - nämlich new. Es handelt sich hierbei um einen in die Programmiersprache C++ eingebauten Operator, der es ermöglicht, dynamisch Speicher zu reservieren. Das new wird also immer im Zusammenhang mit der dynamischen Speicherallokation verwendet. Der Operator new wird so angewendet, dass Sie hinter new angeben müssen, wie viel Speicher eigentlich reserviert werden soll. Sie bekommen von new einen Zeiger zurück - oder anders gesagt eine Positionsnummer - der auf den reservierten Speicherbereich im RAM zeigt. In unserem Code-Beispiel möchten wir mehrere Variablen vom Typ der Struktur adresse definieren, um in ihnen Adressen speichern zu können. Der Anwender unseres Programms hat hierzu einen Wert eingegeben, den wir in der Variablen i gespeichert haben und der uns sagt, wie viele Adressen genau gespeichert werden können sollen. Wir erstellen nun hinter new ein Array vom Typ adresse, das aus genauso vielen Elementen besteht wie in der Variablen i angegeben. Die Positionsnummer des Speicherbereichs, der daraufhin von new reserviert wird, wird von uns in einer Variablen Adressen gespeichert. Diese Variable Adressen hat den Datentyp adresse*, weil die Positionsnummer, die gespeichert wird, auf einen Speicherbereich zeigt, der Informationen vom Typ adresse speichern kann. Im Code-Beispiel wird daraufhin in einer for-Schleife auf den Speicherbereich zugegriffen, auf den die Variable Adressen zeigt, und den einzelnen Elementen im Array einfach eine Adresse zugewiesen. Sie sehen an der Art des Zugriffs, dass der Zeiger Adressen merkwürdigerweise so angewendet werden kann als würde es sich um ein ganz normales Array handeln. Der Datentyp des Zeigers Adressen ist zwar adresse*, dennoch meckert der Compiler anscheinend nicht, wenn Sie auf Elemente in diesem Speicherbereich mit Adressen[i] zugreifen. Der Grund, warum dies funktioniert, ist, dass Arrays und Zeiger sehr eng miteinander verwandt sind. Man kann in C und C++ aus einem Array einen Zeiger machen und aus einem Zeiger ein Array. Das sind jedoch technische Details, auf die an dieser Stelle nicht weiter eingegangen werden soll, da sie uns vom Thema der dynamischen Speicherallokation weit wegführen würden. Das Code-Beispiel enthält am Ende der Funktion main folgende Zeile. delete[] Adressen; In der dynamischen Speicherallokation geht es nicht nur darum, Speicher zu reservieren, sondern auch immer darum, reservierten Speicher, der nicht mehr benötigt wird, an das Betriebssystem zurückzugeben. So wie mit new Speicher reserviert werden kann, kann mit dem Operator delete reservierter Speicher wieder freigegeben werden. Es ist extrem wichtig, dass wenn immer Sie new verwenden irgendwann in Ihrem Programm der reservierte Speicher mit delete freigegeben wird. Sie fordern mit new vom Betriebssystem Speicher an. Computer besitzen nicht unendlich viel Speicher - es handelt sich hierbei um eine begrenzte Ressource. Wenn Sie immer nur Speicher anfordern und nie Speicher an das Betriebssystem zurückgeben, dann könnte es irgendwann zu Speicherknappheit kommen, und Ihr Programm oder andere parallel laufende Programme, die vom Anwender eingesetzt werden, funktionieren nicht mehr richtig, weil einfach der vorhandene Speicher bereits komplett konsumiert wurde und nichts mehr frei ist. Als 61 Zeiger anständiger Entwickler gehen Sie daher mit Speicher besser vorsichtig um und denken daran, Speicher an das Betriebssystem mit delete zurückzugeben, wenn Sie ihn nicht mehr benötigen. Der Operator delete funktioniert wie folgt: Sie geben hinter delete einen Zeiger an, der auf den Speicherbereich zeigt - oder anders gesagt der die Positionsnummer des Speicherbereichs enthält - der freigegeben werden soll. Mehr müssen Sie nicht tun. Beachten Sie, dass Sie hinter delete die eckigen Klammern [] angeben müssen, wenn Sie zuvor mit new ein Array dynamisch reserviert haben. Sonst lassen Sie die eckigen Klammern einfach weg. Sehen Sie sich folgende Beispiele an, die Ihnen den Unterschied zwischen der dynamischen Speicherallokation von Variablen und von Arrays näher bringen sollen. int *i = new int; *i = 169; delete i; std::string *s = new std::string; *s = "Hallo, Welt!"; delete s; int *a = new int[2]; a[0] = 169; a[1] = 170; delete[] a; std::string *b = new std::string[2]; b[0] = "Hallo"; b[1] = "Welt"; delete[] b; Die Zeiger i und s verweisen jeweils auf Speicherbereiche, die aus einzelnen Variablen bestehen - einmal int, einmal std::string. Bei der Rückgabe des reservierten Speichers wird daher der Operator delete angewendet - ohne eckige Klammern. Die Zeiger a und b hingegen verweisen auf dynamisch reservierte Arrays. Sie müssen daher bei der Rückgabe der Speicherbereiche delete[] verwenden - mit eckigen Klammern. Unter Umständen scheint Ihr Programm auch zu funktionieren, wenn Sie die eckigen Klammern weglassen. Sie finden im Internet auch viel C++-Code, der dynamisch reservierte Arrays mit delete zurückgibt. Dieser Code ist jedoch eindeutig falsch. Dass die Programme dennoch scheinbar funktionieren, ist zwar schön. Dass sie das aber nur unter Umständen tun und nicht generell, ist für professionelle Entwickler sicher nicht zufriedenstellend. Merken Sie sich daher unbedingt, dynamisch reservierte Arrays immer mit delete[] zurückzugeben, auch wenn die leeren eckigen Klammern an dieser Stelle etwas merkwürdig aussehen. Die dynamische Speicherallokation hat ein großes Problem: Man darf als Programmierer nie vergessen, einmal mit new reservierten Speicher irgendwann mit einem delete oder delete[] wieder an das Betriebssystem zurückzugeben. In Programmen, die aus tausenden von Code-Zeilen bestehen und an verschiedenen Stellen Speicher dynamisch reservieren, kann dies allzu leicht passieren. In C++ wurden daraufhin sogenannte smart pointers erfunden, also intelligente Zeiger, die automatisch Speicher freigeben, der zuvor mit new reserviert wurde. Man kann die dynamische Speicherallokation also wie in diesem Abschnitt vorgestellt anwenden, braucht aber nicht irgendwann selbst als Programmierer mit delete oder delete[] den reservierten Speicher zurückgeben - das geschieht vollautomatisch. Es gibt verschiedene Arten von smart pointers. Der C++-Standard enthält bisher nur einen einzigen smart pointer namens std::auto_ptr. Dieser kann in unserem Code-Beispiel nicht verwendet werden, da er Speicherbereiche mit delete freigibt und nicht mit delete[]. In der kommenden Version des C++-Standards, die voraussichtlich 2010 erscheinen wird, sind weitere smart pointers 62 Zeiger enthalten, die es einfacher machen werden, fehlerfreien Code für die dynamische Speicherallokation zu schreiben. Das Arbeiten mit Zeigern wird in C++ immer mehr vereinfacht. Zeiger machen auf den Programmiereinsteiger anfangs einen etwas komplizierten Eindruck. Mit ein wenig Übung gewinnt man jedoch schnell den Überblick und ein Verständnis für die dahinterstehenden Konzepte. Die praktische Anwendung von Zeigern kann trotz detailliertem Verständnis der theoretischen Konzepte sehr fehleranfällig sein - einmal eine falsche Positionsnummer verwendet und man greift auf Speicher zu, der einem gar nicht gehört. Oder einmal ein delete vergessen und man gibt reservierten Speicher nicht ans Betriebssystem zurück. Aufgrund der fehleranfälligen Arbeitsweise mit Zeigern wurden und werden laufend neue Werkzeuge in C++ erfunden, die das Arbeiten mit Zeigern wesentlich vereinfachen und somit zu einem robusteren Code beitragen sollen. In modernem C++-Code finden Sie daher kaum noch Zeiger. Diese verschwinden praktisch hinter Hilfsmitteln, die den Entwickler vor vielen Fehlern schützen, die er beim direkten Umgang mit Zeigern machen könnte. Das Problem ist leider, dass Sie für den Einsatz dieser Hilfsmittel sehr wohl verstehen müssen, was Zeiger eigentlich sind und wie Zeiger funktionieren. Obwohl also der direkte Einsatz von Zeigern immer mehr in den Hintergrund rückt, weil viele Hilfsmittel den Umgang mit Zeigern vereinfachen, ist dummerweise für den Einsatz dieser einfachen Hilfsmittel ein gutes Verständnis von Zeigern unerlässlich. So ist es zwar schön zu wissen, dass smart pointers existieren und die Zeigerproblematik vor dem Programmierer verbergen. Ohne ein detailliertes Verständnis von Zeigern werden Sie smart pointers aber nicht sinnvoll einsetzen können. Aufgaben Übung macht den Meister 1. Erstellen Sie eine C++-Anwendung, in der Sie dynamisch ein eindimensionales Array vom Typ short erstellen. Der Anwender Ihres Programms soll angeben können, aus wie vielen Elementen das Array bestehen soll. Legen Sie abwechselnd in alle Elemente des Arrays die Zahlen 0 und 1 hinein und geben Sie danach den Inhalt des Arrays auf den Bildschirm aus. 63 Kapitel 8. Präprozessor Allgemeines Code-Bearbeitung vor der Kompilierung Der Präprozessor ist ein Programm, das vor dem Compiler-Aufruf automatisch gestartet wird und Quellcode ähnlich wie in einer Textverarbeitung bearbeitet. Mit Hilfe ganz bestimmter Befehle kann beispielsweise Quellcode aus anderen Dateien in die aktuelle Datei eingefügt werden. Es kann jedoch auch Quellcode aus der aktuellen Datei gelöscht werden, so dass der Compiler den entsprechenden Quellcode nicht sieht und auch nicht mitübersetzt. Der Präprozessor wird für gewöhnlich nur von C- und C++-Compilern verwendet. Java und andere Programmiersprachen kennen keinen Präprozessor. Für die Programmierung in C und C++ ist der Präprozessor jedoch ganz entscheidend, da er die Entwicklung sehr vereinfachen kann. Symbolische Konstanten #define und #undefine Symbolische Konstanten können über den Präprozessor-Befehl #define definiert werden. Über #undef kann die Definiton einer symbolischen Konstanten wieder aufgehoben werden. Beachten Sie, dass beide Befehle mit dem #-Zeichen beginnen. Daran können Sie ganz leicht Präprozessor-Befehle erkennen - sie beginnen alle mit einem Hash-Zeichen. #define DEBUG Im obigen Code-Beispiel wird eine symbolische Konstante namens DEBUG definiert. Man hat sich angewöhnt, symbolische Konstanten durchgehend groß zu schreiben, um sie schnell erkennen zu können. Beachten Sie, dass die #define-Anweisung nicht mit einem Semikolon abgeschlossen wird. Um die Definition einer symbolischen Konstanten wieder aufzuheben, verwenden Sie einfach den #undef-Befehl. #undef DEBUG Ist die Definition symbolischer Konstanten eigentlich nur im Zusammenhang mit bedingter Kompilierung sinnvoll, so können symbolische Konstanten jedoch auch als Textersatz definiert werden. Betrachten Sie dazu folgendes Beispiel. #include <iostream> #define MEHRWERTSTEUER 19 int main() { std::cout << "Aktuelle Mehrwertsteuer: " << MEHRWERTSTEUER << "%" << std::endl; } Innerhalb des Quellcodes wird direkt die definierte symbolische Konstante MEHRWERTSTEUER angegeben. Wie ist das möglich? Wenn obiges Code-Beispiel vom Compiler übersetzt wird, wird zuerst der Präprozessor gestartet. Dieses Programm ersetzt nun ganz einfach alle symbolischen Konstanten durch den jeweils definierten 64 Präprozessor Wert. Im Beispiel wird im Quellcode an all den Stellen, an denen MEHRWERTSTEUER steht, die Zahl 19 eingefügt. Erst dann wird die so bearbeitete Quellcode-Datei an den Compiler weitergereicht. Je nachdem, wie umfangreich der Quellcode ist, kann der Einsatz derartiger symbolischer Konstanten sehr hilfreich sein. Bei einer Änderung des Mehrwertsteuersatzes muss lediglich die symbolische Konstante auf einen anderen Wert gesetzt werden, um danach den Quellcode neu zu übersetzen. Es ist also nicht notwendig, den gesamten Quellcode zu durchsuchen und zu überprüfen, an welchen Stellen nun überall andere Mehrwertsteuersätze angegeben werden müssen. Mit #define können nicht nur einfache symbolische Konstanten angelegt werden, sondern auch komplexe Makros. Sehen Sie sich folgendes Beispiel an. #include <iostream> #define SUB(a, b) ((a) - (b)) int main() { std::cout << SUB(10, 5) << std::endl; } Das Programm definiert ein Makro SUB, das zwei Parameter erwartet. Das Makro wird durch den Präprozessor so aufgelöst, dass zwischen die beiden Parameter das Minus-Zeichen gestellt wird. Wenn Sie nun wie im obigen Beispiel das Makro aufrufen und die Werte 10 und 5 als Parameter übergeben, wird als Ergebnis der Subtraktion der Wert 5 auf den Bildschirm ausgegeben. Makros sollten grundsätzlich so definiert werden, dass die Parameter einzeln geklammert und der gesamte Ausdruck ebenfalls nochmal geklammert wird - je mehr Klammern, umso besser. Im folgenden Beispiel sind die Klammern extra weggelassen worden, was zu einem unerwarteten Ergebnis führt. #include <iostream> #define SUB(a, b) a - b int main() { std::cout << SUB(10, 5) * 2 << std::endl; } Anstatt 10 und 5 zu subtrahieren und das Ergebnis mit 2 zu multiplizieren wird zuerst 5 mit 2 multipliziert und dann von 10 subtrahiert. Die Lösung, die erwartet wurde, war 10 - angezeigt wird jedoch 0. Genau derartige Fälle sind der Grund, warum dringend vom Einsatz von Makros abgeraten wird. In der C++-Community im Internet wird vor Makros gewarnt: "Macros are evil". Der Präprozessor ersetzt wie die Suchen/Ersetzen-Funktionen einer Textverarbeitung stupide Makros, ohne weitergehende Überprüfungen durchzuführen. Der Einsatz von Funktionen bietet vor allem, was Makros betrifft, eine viel größere Sicherheit, da hier sogenannte Nebeneffekte, wie es sie bei Makros oft gibt, nicht auftreten können. Bedingte Kompilierung #if, #elif, #else, #endif, #ifdef und #ifndef Symbolische Konstanten, die lediglich mit #define definiert werden, ohne einen zu ersetzenden Wert zu erhalten, haben bisher noch nicht viel Sinn ergeben. Im Zusammenhang mit den Präprozessorbefehlen #if, #elif, #else, #endif, #ifdef und #ifndef kann jedoch in Abhängigkeit einer Definition unterschiedlicher Code an den Compiler weitergegeben werden. Betrachten Sie dazu folgendes Beispiel. 65 Präprozessor #include <iostream> #include <cstdlib> #define DEBUG 0 int main() { char buffer[20]; int number; std::cout << "Geben Sie eine Zahl ein: " << std::flush; std::cin.get(buffer, sizeof(buffer)); #if DEBUG > 0 std::cout << "DEBUG: " << buffer << std::endl; #endif number = std::atoi(buffer); #if DEBUG > 1 std::cout << "DEBUG: " << number << std::endl; #endif } Am Anfang des Programms wird eine symbolische Konstante DEBUG definiert. Diese Konstante kann auf verschiedene Werte gesetzt werden. Je höher der Wert ist, umso mehr Debug-Informationen werden vom Programm ausgegeben. Ist DEBUG auf 0 gesetzt, gibt das Programm keine Debug-Informationen aus. Wird DEBUG auf den Wert 1 gesetzt, wird zusätzlicher Output auf den Bildschirm ausgegeben, um das Programm auf eventuelle Fehler überprüfen zu können. Wird DEBUG auf den Wert 2 gesetzt, werden soviel Debug-Informationen wie möglich ausgegeben. Mit #if kann genauso wie mit dem bekannten C++-Befehl if eine Bedingung überprüft werden. #if kann jedoch nur auf symbolische Konstanten angewandt werden. Wenn die Bedingung wahr ist, wird der nachfolgende Quellcode bis zu einem abschließendem #endif an den Compiler zur Übersetzung weitergereicht. Andernfalls wird der Quellcode entfernt - der Compiler wird ihn nicht zu sehen bekommen. Hier sehen Sie einen ganz entscheidenden Vorteil von symbolischen Konstanten verglichen mit Variablen: Es ist möglich, Quellcode nicht in einem Programm zu verwenden, indem über entsprechende Präprozessoranweisungen der Quellcode nicht an den Compiler weitergereicht wird. Sie könnten das gleiche Programm auch mit Hilfe einer globalen Variablen schreiben, die auf einen bestimmten Wert gesetzt wird. Hier würde nun jedoch der gesamte Quellcode vom Compiler übersetzt werden, selbst wenn Teile des Codes im Programm niemals verwendet werden würden. Das würde das Programm unnötigerweise aufblähen und mit ungenutzten Code vollstopfen. Der schematische Aufbau einer bedingten Kompilierung sieht wie folgt aus. #if BEDINGUNG ANWEISUNGEN #elif BEDINGUNG ANWEISUNGEN #else ANWEISUNGEN #endif Sie sehen, dass wie die bekannte if-Kontrollstruktur auch die bedingte Kompilierung mehrere Bedingungen überprüfen kann. Dies erfolgt per #elif. Genauso wie in der if-Kontrollstruktur können #elif-Zweige weggelassen werden. Auch das abschließende #else ist optional. Anstatt hinter #if eine Bedingung zu überprüfen, kann hinter #ifdef eine symbolische Konstante daraufhin überprüft werden, ob sie überhaupt definiert ist. #include <iostream> 66 Präprozessor #include <cstdlib> #define DEBUG int main() { char buffer[20]; int number; std::cout << "Geben Sie eine Zahl ein: " << std::flush; std::cin.get(buffer, sizeof(buffer)); #ifdef DEBUG std::cout << "DEBUG: " << buffer << std::endl; #endif number = std::atoi(buffer); #ifdef DEBUG std::cout << "DEBUG: " << number << std::endl; #endif } Die symbolische Konstante DEBUG wird nun nicht mehr auf einen Wert gesetzt, sondern einfach nur definiert. Über #ifdef wird nun innerhalb des Quellcodes überprüft, ob es die symbolische Konstante DEBUG gibt. Ist dies der Fall, wird der Quellcode bis zum abschließenden #endif an den Compiler zur Übersetzung weitergereicht. Um nun zu verhindern, dass die entsprechenden CodeZeilen kompiliert werden, muss einfach die #define-Anweisung für DEBUG aus dem Programm gelöscht werden. Während es also mit #if möglich ist, eine symbolische Konstante auf einen bestimmten Wert zu überprüfen, kann mit #ifdef überprüft werden, ob eine symbolische Konstante überhaupt definiert ist. Als Gegenstück zu #ifdef existiert #ifndef. Mit #ifndef wird überprüft, ob eine symbolische Konstante nicht definiert ist. #ifdef und #ifndef sind letztendlich nur Abkürzungen für #if defined und #if !defined. Nachdem es für #elif-Zweige keine Abkürzung gibt, muss für weitergehende Überprüfungen in einer #if-Kontrollstruktur mit der langen Schreibweise gearbeitet werden. Betrachten Sie dazu folgendes Beispiel. #include <iostream> int main() { #if defined(__linux__) std::cout << "Betriebssystem Linux" << std::endl; #elif defined(WIN32) std::cout << "Betriebssystem Microsoft Windows" << std::endl; #elif defined(__APPLE__) std::cout << "Betriebssystem Mac OS X" << std::endl; #else std::cout << "Unbekanntes Betriebssystem" << std::endl; #endif } Während #if defined auch als #ifdef geschrieben werden könnte, gibt es für die #elif defined-Zweige keine abgekürzte Schreibweise. Daher findet man normalerweise immer nur die lange Form in Quellcodes, um bei einer einheitlichen Schreibweise zu bleiben. Obiges Beispiel demonstriert auch gleich einen sehr häufigen Einsatz von #if defined-Überprüfungen. Im Beispiel wird überprüft, ob eine der symbolischen Konstanten __linux__, WIN32 oder 67 Präprozessor __APPLE__ definiert ist. Symbolische Konstanten, die mit zwei Unterstrichen beginnen und enden, werden von Compilern definiert. C++-Compiler auf dem Betriebssystem Linux definieren hierbei eine symbolische Konstante __linux__, C++-Compiler unter Microsoft Windows WIN32 und C++Compiler unter Mac OS X __APPLE__. Auf diese Weise ist es möglich, betriebssystemspezifischen Quellcode zu schreiben und ihn nur dann an den Compiler weiterzugeben und übersetzen zu lassen, wenn der Quellcode auf dem richtigen Betriebssystem kompiliert wird. Fehlermeldung #error Im Zusammenhang mit der bedingten Kompilierung ist es manchmal sinnvoll, eine Fehlermeldung ausgeben zu können, mit der gleichzeitig die Kompilierung abgebrochen wird. Sehen Sie sich dazu folgendes Beispiel an. #include <iostream> int main() { #if defined(WIN32) std::cout << "Betriebssystem Microsoft Windows" << std::endl; #else #error Das Programm muss unter Microsoft Windows kompiliert werden! #endif } Der Präprozessor überprüft, ob eine symbolische Konstante WIN32 definiert ist. Dies ist nur dann der Fall, wenn der Quellcode mit einem Compiler unter Microsoft Windows kompiliert wird. Wird das Programm auf einem anderen Betriebssystem kompiliert, wird mit #error eine Fehlermeldung auf den Bildschirm ausgegeben und die Kompilierung abgebrochen. Dies ist beispielsweise sinnvoll, wenn ein Programm für ein bestimmtes Betriebssystem noch nicht angepaßt wurde und daher eine Kompilierung keinen Sinn ergeben würde. Dateien einfügen #include Die Präprozessor-Anweisung #include haben Sie bereits in vielen Beispielprogrammen selbst verwendet gehabt. Hinter #include wird ein Dateiname angegeben. Der gesamte Inhalt der angegebenen Datei wird vom Präprozessor daraufhin gelesen und in die aktuelle Quellcode-Datei kopiert - und zwar genau an die Stelle gesetzt, an der sich die #include-Anweisung befindet. #include bietet zwei Schreibweisen an. #include <iostream> Der Dateiname wird in spitzen Klammern angegeben, wenn die Datei in den Standard-Include-Verzeichnissen liegt. Compiler lassen sich so konfigurieren, dass sie eine Reihe von Verzeichnissen kennen, in denen wichtige Include-Dateien liegen. Normalerweise werden diese Dateien Header-Dateien genannt. Während vor dem offiziellen C++-Standard die Endung für Header-Dateien gewöhnlich .h war, ist im Standard neu festgelegt worden, dass Header-Dateien gar keine Endung mehr besitzen. Dies betrifft jedoch nur die Header-Dateien innerhalb des C++-Standards. Wie Sie Ihre eigenen Header-Dateien nennen spielt keine Rolle. Die Endung .h wie auch jede andere beliebige Endung ist erlaubt. #include "mein_datentyp.h" 68 Präprozessor Während der Zugriff auf offizielle Header-Dateien normalerweise über spitze Klammern erfolgt, findet der Zugriff auf eigene Header-Dateien, die von Ihnen entwickelt wurden und zum aktuellen Projekt gehören, über Anführungszeichen statt. Derart angegebene Dateien werden im aktuellen Verzeichnis gesucht und nicht in den Standard-Include-Verzeichnissen. Während Sie mit #include eigentlich jede beliebige Datei in eine andere Datei einfügen können, werden diese Präprozessor-Anweisungen in C++ hauptsächlich in Verbindung mit Klassen-Definitionen verwendet. Wann immer Sie auf eine Klasse zugreifen müssen, müssen Sie diese vorher bekannt machen - erst Definition, dann Verwendung. Indem Sie die entsprechende Header-Datei einbinden, fügen Sie die Definition der Klasse in Ihre eigene Datei ein. Daraufhin können Sie diese Klasse innerhalb Ihres Quellcodes verwenden. So sind Sie zum Beispiel immer vorgegangen, wenn Sie in Ihrem Code die Klasse std::string einsetzen wollten - diese mussten Sie erst bekanntmachen. Dies taten Sie immer, indem Sie die Header-Datei string mit #include eingebunden haben. Das Erstellen von Klassen-Definitionen und Header-Dateien wird im Buch Programmieren in C++: Aufbau [http://www.highscore.de/cpp/aufbau/] behandelt. Aufgaben Übung macht den Meister 1. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe zweier Ganzzahlen auffordert. Die beiden eingegebenen Zahlen sollen mit Hilfe einer Funktion addiert werden, die gleichzeitig die Summe auf den Bildschirm ausgibt. Das Programm soll auf den drei fiktiven Betriebssystemen Kuhnix BSE, Luxus und Hasta La Vista lauffähig sein. Kuhnix BSE bietet jedoch nur den Datentyp long an, um Ganzzahlen zu speichern. Luxus hingegen kennt nur den Datentyp char. Und Hasta La Vista kann Ganzzahlen nur in Variablen vom Typ short speichern. Verwenden Sie Präprozessor-Anweisungen in der Art, dass das Programm mit einer entsprechenden #define Anweisung jeweils so übersetzt wird, dass es unter dem jeweiligen Betriebssystem funktioniert. Falls das Programm unter dem nicht unterstützten Betriebssystem Banana Mac kompiliert wird, soll eine Fehlermeldung ausgegeben und die Kompilierung abgebrochen werden. 69 Kapitel 9. Klassen und Objekte Allgemeines Objektorientierte Programmierung Alles, was Sie in den vorherigen Kapiteln kennen gelernt haben, war natürlich bereits Bestandteil der Programmiersprache C++. Dennoch sieht die Entwicklung eines C++-Programms meist anders aus als das, was Sie bisher an Beispielen gesehen haben. Bis jetzt haben Sie große Teile des Grundwortschatzes von C++ kennen gelernt und sie eher rudimentär angewandt. In der Praxis bestehen C++-Programme normalerweise vorrangig aus Klassen. Entwickler konzentrieren sich auf das Erstellen von Klassen, die an die speziellen Bedürfnisse, die die Aufgabenstellung erfordert, angepasst werden. Sind die Klassen erstellt, werden sie als Datentypen für die Definition von Variablen verwendet. Diese Variablen heißen Objekte, und mit diesen wird innerhalb eines Programms dann gearbeitet. Objektorientierte Programmierung Von Architekten und Häusern Die objektorientierte Programmierung ermöglicht das Übernehmen von Objekten aus der Aufgabenstellung hinüber in die Programmentwicklung. Es wird also versucht, in der Entwicklung die gleichen Objekte zu verwenden wie sie in der konkreten Aufgabenstellung vorkommen. Dieses Übernehmen von Objekten eins zu eins aus der Aufgabenstellung in die Entwicklung bedeutet, dass keine Transferleistung zu erbringen ist und nicht zwischen Aufgabenstellung und Aufgabenlösung - also der Programmierung - abstrahiert werden muss. Die objektorientierte Programmierung ermöglicht eine Problemlösung nahe der Aufgabenstellung. Es geht nicht darum, eine Lösung zu entwickeln, die an die Hardware und all ihre Besonderheiten angepasst werden muss. Objektorientierte Programmiersprachen lösen sich von der Hardware und orientieren sich direkt an der Aufgabenstellung. Ein gutes objektorientiertes Programm ist daher eine Anwendung, die möglichst konkret die einzelnen Objekte, die in der Aufgabenstellung vorkommen und die zur Problemlösung miteinander agieren, herausarbeitet und diese nachbildet. Als objektorientierter Programmierer müssen Sie daher sinnvolle Objekte identifizieren und sie innerhalb einer Programmiersprache beschreiben können. Diese Beschreibung muss so umfangreich sein, dass das Objekt auch tatsächlich zur Problemlösung herangezogen werden kann. Gleichzeitig darf die Beschreibung nicht zu umfangreich sein, da sie sonst zu viele unwesentliche Elemente enthält und die Entwicklung erschwert. Im Zusammenhang mit der Objektorientierung ist immer wieder die Rede von Klassen und Objekten. Um was handelt es sich hierbei genau? Stellen Sie sich vor, Sie bauen ein Haus. Der erste Schritt besteht darin, dass Sie einen detaillierten Plan erstellen, der genau beschreibt, wie das Haus später aussehen soll. In diesem Plan müssen alle wesentlichen Beschreibungen des Hauses enthalten sein. Unwesentliche Merkmale müssen weggelassen werden, da dies den Plan nur unübersichtlich machen und den Hausbau erschweren würde. Ist der Plan fertig, kann es losgehen - auf einem von Ihnen gekauften Grundstück wird nun ein Haus gebaut. Dieses Haus sieht nach der Fertigstellung natürlich genauso aus wie im Plan beschrieben. Nachdem Sie noch etwas Kleingeld übrig haben, beschließen Sie, ein zweites Haus zu bauen. Auch für das zweite Haus wird genau der gleiche Plan verwendet wie für das erste Haus. Es sieht dementsprechend dem ersten Haus ähnlich, steht aber natürlich auf einem anderen Grundstück, vielleicht in einer anderen Straße oder sogar in einer anderen Stadt. 70 Klassen und Objekte In der objektorientierten Terminologie entspricht der Plan der Klasse und die gebauten Häuser den Objekten. Während es mehrere Häuser geben kann, die mit Hilfe des gleichen Plans erbaut wurden, so kann es mehrere Objekte vom Typ einer Klasse geben. So wie der Plan lediglich eine Beschreibung eines Hauses ist, ist die Klasse lediglich eine Beschreibung eines Objekts. Weder der Plan alleine noch die Klasse machen jedoch das Haus bzw. das Objekt lebendig. Es handelt sich hierbei lediglich um Beschreibungen. So wie Sie erst in das fertiggebaute Haus einziehen können, können Sie in Ihrem Programm auch nur mit Objekten arbeiten. Sie benötigen zum Erstellen eines Objekts eine Klasse, die das Objekt beschreibt - so wie Sie zum Bau eines Hauses auch erstmal einen Plan benötigen. Andere Analogien sprechen von Art oder Gattung. Wenn mehrere Objekte vom Typ einer Klasse sind, gehören sie derselben Gattung an, sind sie derselben Art. So wie es die Fahrzeugklasse 318 von BMW gibt, gibt es mehrere Fahrzeuge von dieser Fahrzeugklasse - eine Klasse, mehrere Objekte. Die C++-Bibliothek stellt eine Menge an Klassen zur Verfügung, die sich speziell zur Verwaltung von Daten im Speicher eignen. In diesem Fall müssen Sie die Klassen nicht erst entwickeln, sondern können gleich Objekte von diesen Klassen erstellen und mit diesen Objekten Ihre Daten verwalten. Normalerweise ist es jedoch auch immer notwendig, eigene Klassen zu erstellen. Jede Aufgabe ist anders, und während Sie teilweise auf Klassen zurückgreifen können, die von anderen Entwicklern programmiert wurden, so müssen doch fast immer auch selber Klassen entwickelt werden, die zum aktuellen Problem passen. Die Modellierung eines Ausschnitts der Wirklichkeit innerhalb der Programmierung zur gezielten Problemlösung ist das Ziel objektorientierter Sprachen. Ähnlich wie bei Funktionen, Strukturen und Enumerationen gilt auch bei Objekten: Zuerst die Definition, dann die Verwendung. Die Definition ist hierbei die Klasse, die Verwendung das Anlegen eines Objekts. Das heißt auch, ohne Klasse kein Objekt. Klassen und Objekte gehören also untrennbar zusammen. Objekte werden durch Klassen beschrieben und können ohne diese nicht erstellt werden. Praxis-Beispiel Erstellen einer Klasse Im Folgenden soll Ihnen gezeigt werden, wie eine Klasse selber erstellt wird und davon ein Objekt im Programm angelegt wird. In diesem Beispiel geht es um die Abbildung eines Rennwagens, beispielsweise für ein Computerspiel. Um das Beispiel nicht zu unübersichtlich zu gestalten, wird der Rennwagen nur sehr minimal beschrieben. Er soll lediglich eine Möglichkeit zum Beschleunigen und Abbremsen bieten - mehr nicht. class rennwagen { int Kmh; public: rennwagen() : Kmh(0) { } void beschleunigen(int kmh) { Kmh += kmh; } void bremsen() { Kmh -= 50; if (Kmh < 0) { Kmh = 0; 71 Klassen und Objekte } } }; Eine Klasse wird normalerweise mit dem Schlüsselwort class definiert. Hinter class folgt der Name der Klasse, und hinter diesem zwei geschweifte Klammern, zwischen denen die Klassendefinition steht. Beachten Sie, dass hinter der geschlossenen geschweiften Klammer ein Semikolon stehen muss. Klassen bestehen aus unterschiedlichen Merkmalen, nämlich aus Eigenschaften und Methoden. Eigenschaften sind technisch betrachtet nichts anderes als Variablen, und Methoden sind einfach nur Funktionen. Indem für Klassen also Variablen und Funktionen definiert werden, werden Objekte beschrieben. Unser Rennwagen soll beschleunigt und abgebremst werden können. Nachdem es sich hierbei um Vorgänge handelt, werden diese als Methode und nicht als Eigenschaft abgebildet. Methoden sind grundsätzlich auch als Fähigkeiten eines Objekts bekannt. Im obigen Beispiel sehen Sie, dass die Klasse rennwagen zwei Methoden namens beschleunigen und bremsen definiert. Zusätzlich ist eine dritte Methode definiert, die genauso heißt wie die Klasse selber: rennwagen. Es handelt sich hierbei um eine spezielle Methode, die Konstruktor genannt wird, und verwendet wird, um ein Objekt zu initialisieren. In diesem Fall wird eine Eigenschaft Kmh der Klasse rennwagen auf den Wert 0 gesetzt. Das bedeutet, dass beim Erstellen eines Objekts vom Typ rennwagen die Anfangsgeschwindigkeit 0 ist und der Rennwagen steht. Die Eigenschaft Kmh ist selber einfach als Variable innerhalb der Klasse angelegt. Anhand der Eigenschaft Kmh wird nun das Objekt beschleunigt oder abgebremst. Die Methode beschleunigen erwartet einen Parameter vom Typ int und erhöht die Eigenschaft Kmh um diesen entsprechend. Die Methode bremsen verringert den Wert der Eigenschaft hingegeben um 50. Außerdem wird überprüft, ob 0 unterschritten wurde. In diesem Fall wird die Eigenschaft Kmh auf 0 gesetzt, um eine negative Geschwindigkeit zu verhindern. Eine derartige Klassendefinition wird für gewöhnlich in eine Header-Datei gelegt, die normalerweise auch genauso genannt wird wie die Klasse selber. Obige Klasse könnte also in einer Datei namens rennwagen.h gespeichert werden. Sehen Sie sich nun folgendes Programm an, das die Klasse rennwagen verwendet und ein Objekt von dieser erstellt. #include <iostream> #include "rennwagen.h" int main() { rennwagen MeinRennwagen; MeinRennwagen.beschleunigen(250); std::cout << "Rennwagen auf 250 km/h beschleunigt" << std::endl; MeinRennwagen.bremsen(); MeinRennwagen.bremsen(); std::cout << "Rennwagen auf 150 km/h abgebremst" << std::endl; MeinRennwagen.beschleunigen(50); std::cout << "Rennwagen auf 200 km/h beschleunigt" << std::endl; MeinRennwagen.bremsen(); MeinRennwagen.bremsen(); MeinRennwagen.bremsen(); MeinRennwagen.bremsen(); std::cout << "Rennwagen angehalten" << std::endl; } 72 Klassen und Objekte Um ein Objekt anzulegen, definieren Sie einfach eine Variable vom Typ einer Klasse. In diesem Fall wird also ein Objekt namens MeinRennwagen angelegt, das vom Typ rennwagen ist. Um den Datentyp rennwagen einsetzen zu können, müssen Sie diesen über den Präprozessor-Befehl #include bekanntmachen und die entsprechende Header-Datei mit der Klassendefinition einbinden. Die Klasse sieht so aus, dass sie zwei Methoden beschleunigen und bremsen zur Verfügung stellt. Um mit Ihrem Rennwagen loszufahren, rufen Sie einfach die Methode beschleunigen auf und übergeben einen Wert, um den der Rennwagen beschleunigt werden soll - so ist diese Methode ja definiert. Um eine Methode aufzurufen, geben Sie zuerst das Objekt an, für das Sie die Methode aufrufen wollen - in diesem Fall also MeinRennwagen. Danach setzen Sie den Zugriffsoperator ., den Sie bereits von Strukturen her kennen. Hinter dem Punkt geben Sie nun die Methode an, die Sie aufrufen möchten. Der Methodenaufruf erfolgt so wie Sie es von Funktionsaufrufen gewohnt sind: Sie geben zuerst den Namen der Methode an und dahinter die runden Klammern mit möglicher Parameterliste. Nachdem die Methode beschleunigen einen Parameter vom Typ int erwartet, wird der Rennwagen im Beispiel erstmal um 250 km/h beschleunigt. Dies wird auch hinter dem Methodenaufruf zur Kontrolle auf den Bildschirm ausgegeben. Als nächstes wird innerhalb des Programms die Methode bremsen aufgerufen - wiederum unter Angabe des Objektnamens und des Zugriffsoperators. Auf diese Weise wird der Rennwagen um 50 km/h abgebremst. Indem die Methoden einfach mehrmals hintereinander aufgerufen werden, kann der Rennwagen auf beliebige Geschwindigkeiten beschleunigt oder abgebremst werden. Indem Sie nun also die Klasse rennwagen definiert haben, können Sie tatsächlich einen Rennwagen in Ihrer Programmentwicklung verwenden und ihn sogar - so wie Sie es aus der Formel 1 kennen beschleunigen und abbremsen. Die Entwicklung des Programms orientiert sich nicht an irgendwelchen Hardware-Gegebenheiten, sondern an den Objekten, die Sie für die Problemlösung benötigen. 73 Anhang A. Übungen Aufgaben Übung macht den Meister 1. Entwickeln Sie einen Taschenrechner, der die Eingabe zweier Zahlen erwartet und diese addieren, subtrahieren, multiplizieren und dividieren kann. Über eine einzige Präprozessor-Anweisung soll eingestellt werden können, ob nur mit Ganz- oder auch mit Kommazahlen gearbeitet werden kann. 2. Entwickeln Sie eine C++-Anwendung zur Simulation eines Bankautomaten. Der Anwender soll einen Benutzernamen (der die EC-Karte repräsentiert) und eine dazugehörige PIN eingeben, um Zugriff auf sein Konto zu erhalten. Insgesamt soll die Beispiel-Anwendung drei Konten mit unterschiedlichen Benutzernamen und PINs unterstützen. Ist das Einloggen erfolgreich, kann der Anwender zwischen Kontostandanzeige und Auszahlung eines Betrags wählen. Das Programm soll in einer Endlosschleife laufen, um mehrmaliges Einloggen in den Bankautomaten zu ermöglichen. Setzen Sie die drei Konten auf beliebige Werte, um das Programm zu testen, und loggen Sie sich mehrmals mit jedem Benutzer ein, um den Kontostand anzuzeigen und Geld abzuheben. 3. Entwickeln Sie ein C++-Programm, das nach Eingabe zweier Stadtnamen die Entfernung zwischen den beiden Städten in Kilometer ausgibt. Verwenden Sie folgende Daten, um Ihr Programm testen zu können: Die Entfernung zwischen Köln und München beträgt 480 Kilometer, zwischen München und Berlin 530 Kilometer und zwischen Berlin und Köln 500 Kilometer. Die Entfernung soll unabhängig von der Reihenfolge der Eingabe der Stadtnamen ausgegeben werden. 74