Einführung in C++

Werbung
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
Herunterladen