„Ein Programm ist eigentlich nichts weiter als eine Folge von Befehlen, die nacheinander ausgeführt werden. Mächtig wird ein Programm erst dadurch, dass es in Abhängigkeit von einer Bedingung entscheiden kann, ob der eine oder ein anderer Anweisungsblock ausgeführt werden soll.“
In diesem Kapitel meines C/C++ Tutorials lernst du,
was Anweisungen sind,
was Blöcke sind,
was Ausdrücke sind,
wie man auf der Basis von Bedingungen den Code verzweigt,
was Wahrheitswerte sind und wie man darauf reagiert.
Anweisungen
In C++ steuern Anweisungen die Reihenfolge der Ausführung, werten Ausdrücke aus oder bewirken nichts (die Leeranweisung). Alle C++-Anweisungen enden mit einem Semikolon (;), auch die Leeranweisung, die nur aus einem Semikolon besteht.
Eine häufig gebrauchte einfache Anweisung ist die Zuweisung:
x = a + b;
Diese Anweisung bedeutet im Gegensatz zur Algebra nicht x gleich a + b, sondern ist wie folgt zu interpretieren: »Weise den Wert der Summe aus a und b an x zu.« Auch wenn diese Anweisung zwei Dinge bewirkt, handelt es sich um eine einzige Anweisung und hat daher nur ein Semikolon. Der Zuweisungsoperator nimmt die Zuweisung des auf der rechten Seite stehenden Ausdrucks an die linke Seite vor.
Whitespace
Leerzeichen gehören zusammen mit Tabulatoren und den Zeilenvorschüben zu den sogenannten Whitespace-Zeichen. Sie werden im allgemeinen in den Anweisungen ignoriert. Die oben behandelte Zuweisung lässt sich auch wie folgt schreiben:
x=a+b;
oder
x =a
+ b ;
Die zweite Variante ist zwar zulässig, aber kompletter Blödsinn. Durch Whitespace- Zeichen sollen Programme leichter zu lesen und zu warten sein. Man kann damit aber auch einen unleserlichen Code produzieren. C++ stellt die Möglichkeiten bereit, für den sinnvollen Einsatz ist der Programmierer verantwortlich.
Whitespace-Zeichen (Leerzeichen, Tabulatoren und Zeilenvorschübe) sind nicht sichtbar. Werden diese Zeichen gedruckt, bleibt das Papier weiß (white).
Anweisungsblöcke und Verbundanweisungen
An allen Stellen, wo eine einzelne Anweisung stehen kann, ist auch eine Verbundanweisung (auch Block genannt) zulässig. Ein Block beginnt mit einer öffnenden geschweiften Klammer ({) und endet mit einer schließenden geschweiften Klammer (}).
In einem Block ist zwar jede Anweisung mit einem Semikolon abzuschließen, der Block selbst endet aber nicht mit einem Semikolon. Dazu ein Beispiel:
{
temp = a;
a = b;
b = temp;
}
Dieser Codeblock tauscht die Werte der Variablen in a und b aus.
Ausdrücke
Alles, was zu einem Wert ausgewertet werden kann, nennt man in C++ einen Ausdruck . Von einem Ausdruck sagt man, dass er einen Wert zurückliefert. Demzufolge ist die Anweisung 3 + 2;, die den Wert 5 zurückliefert, ein Ausdruck. Alle Ausdrücke sind auch Anweisungen.
Die Unzahl der Codeabschnitte, die sich als Ausdruck entpuppen, mag dich vielleicht überraschen. Hier drei Beispiele:
3.2 // liefert den Wert 3.2
PI // float-Konstante, die den Wert 3.14 zurückgibt
SekundenProMinute // int-Konstante, die 60 liefert
Vorausgesetzt, daß PI eine Konstante mit dem Wert 3.14 und SekundenProMinute eine Konstante mit dem Wert 60 ist, stellen alle drei Anweisungen gültige Ausdrücke dar.
Der Ausdruck
x = a + b;
addiert nicht nur a und b und weist das Ergebnis an x zu, sondern liefert auch den Wert dieser Zuweisung (den Wert in x). Daher ist diese Anweisung ebenfalls ein Ausdruck und kann somit auch auf der rechten Seite eines Zuweisungsoperators stehen:
y = x = a + b;
Diese Zeile wird in der folgenden Reihenfolge ausgewertet:
Addiere a zu b.
Weise das Ergebnis des Ausdrucks a + b an x zu.
Weise das Ergebnis des Zuweisungsausdrucks x = a + b an y zu.
Wenn a, b, x und y ganze Zahlen sind, a den Wert 2 und b den Wert 5 hat, enthält sowohl x als auch y nach Ausführung dieser Anweisung den Wert 7.
Das Listing „expressions.cpp“ demonstriert die Auswertung komplexer Ausdrücke:
// Listing: expressions.cpp
// demonstriert die Auswertung
// komplexer Ausdrücke
#include <iostream>
using namespace std;
int main(void)
{
int a=0, b=0, x=0, y=35;
cout << "a: " << a << " b: " << b;
cout << " x: " << x << " y: " << y << endl;
a = 9;
b = 7;
y = x = a+b;
cout << "a: " << a << " b: " << b;
cout << " x: " << x << " y: " << y << endl;
return 0;
}
Ausgabe:
a: 0 b: 0 x: 0 y: 35
a: 9 b: 7 x: 16 y: 16
Analyse:
Zeile 10 deklariert und initialisiert die vier Variablen.
Die Ausgabe ihrer Werte erfolgt in den Zeilen 12 und 13.
Zeile 15 weist den Wert 9 an die Variable a zu.
Zeile 16 weist den Wert 7 an die Variable b zu.
Zeile 17 summiert die Werte von a und b und weist das Ergebnis x zu. Dieser Ausdruck (x = a + b) ergibt einen Wert (die Summe aus a und b), der wiederum y zugewiesen wird.
Operatoren
Ein Operator ist ein Symbol, das den Compiler zur Ausführung einer Aktion veranlasst. Operatoren verarbeiten Operanden, und in C++ sind alle Operanden Ausdrücke. In C++ gibt es mehrere Arten von Operatoren. Zwei Arten von Operatoren sind:
Zuweisungsoperatoren
mathematische Operatoren
Zuweisungsoperator
Der Zuweisungsoperator (=) bewirkt, dass der Operand auf der linken Seite des Operators den Wert von der rechten Seite des Operators erhält. Der Ausdruck
x = a + b;
weist dem Operanden x den Wert zu, der als Ergebnis der Addition von a und b entsteht.
Einen Operanden, der auf der linken Seite eines Zuweisungsoperators zulässig ist, bezeichnet man als L-Wert (linker Wert). Ein entsprechender Operand auf der rechten Seite heißt R-Wert.
Konstanten sind R-Werte und können nicht als L-Werte vorkommen. Demzufolge ist die Anweisung
x = 35; // OK
zulässig, während die Anweisung
35 = x; // Fehler, kein L-Wert!
nicht erlaubt ist.
Ein L-Wert ist ein Operand, der auf der linken Seite eines Ausdrucks stehen kann. Als R-Wert bezeichnet man einen Operanden, der auf der rechten Seite eines Ausdrucks vorkommen kann. Während alle L-Werte auch als R-Werte zulässig sind, dürfen nicht alle R-Werte auch als L-Werte verwendet werden. Ein Literal ist zum Beispiel ein R- Wert, der nicht als L-Wert erlaubt ist. Demzufolge kann man x = 5; schreiben, 5 = x; jedoch nicht (x kann ein R- und eine L-Wert sein, 5 hingegen ist nur ein R-Wert).
Mathematische Operatoren
C/C++++ kennt die fünf mathematischen Operatoren Addition (+), Subtraktion (-), Multiplikation (*), Division (/) und Modulo-Division (%).
Die zwei Grundrechenarten Addition und Subtraktion arbeiten wie gewohnt, wenn es auch bei der Subtraktion mit vorzeichenlosen Ganzzahlen zu überraschenden Ergebnissen kommen kann, falls das Ergebnis eigentlich negativ ist. Einen Eindruck davon hast du ja schon erhalten, als der Variablenüberlauf beschrieben wurde. Im Listing „suboverflow.cpp“ kannst du nachvollziehen was passiert, wenn du eine große Zahl eines unsigned -Typs von einer kleinen Zahl eines unsigned-Typs subtrahierst:
// Listing: suboverflow.cpp
// Subtraktion und Integer-Überlauf
#include <iostream>
using namespace std;
int main(void)
{
unsigned int difference;
unsigned int bigNumber = 100;
unsigned int smallNumber = 50;
// kein Überlauf
difference = bigNumber - smallNumber;
cout << "Die Differenz betraegt: " << difference;
// Überlauf
difference = smallNumber - bigNumber;
cout << "\nJetzt betraegt die Differenz: " << difference <<endl;
return 0;
}
Ausgabe:
Die Differenz betraegt: 50
Jetzt betraegt die Differenz: 4294967246
Analyse:
Zeile 14 ruft den Subtraktions-Operator auf, und das Ergebnis, das ganz unseren Erwartungen entspricht, wird in Zeile 15 ausgegeben.
Zeile 18 ruft den Subtraktions- Operator erneut auf. Diesmal wird jedoch eine große unsigned-Zahl von einer kleinen unsigned-Zahl subtrahiert. Das Ergebnis wäre eigentlich negativ, doch da es als unsigned-Zahl ausgewertet (und ausgegeben) wird, ist das Ergebnis ein Überlauf. Auf dieses Problem wird noch genauer eingegangen, wenn wir über Operator- Vorrang sprechen.
Integer-Division und Modulo
Die Integer-Division unterscheidet sich etwas von der gewohnten Division. Um genau zu sein: Integer-Division entspricht dem, was du in der zweiten Klasse gelernt habst. Dividiert man 21 durch 4 und betrachtet das Ganze als Integer-Division (wie in der Grundschule mit sieben Jahren) ist das Ergebnis 5 Rest 1.
Um den Rest zu erhalten, bedient man sich des Modulo-Operators (%) und berechnen 21 modulus 4 (21 % 4) mit dem Ergebnis 1. Der Modulo-Operator gibt den Rest einer Ganzzahldivision zurück.
Die Berechnung des Modulus kann recht nützlich sein, wenn du zum Beispiel eine Anweisung bei jeder zehnten Aktion drucken willst. Jede Zahl, deren Modulus 0 ergibt, wenn du deren Modulus mit 10 berechnest, ist ein Mehrfaches von 10. So ist 1 % 10 gleich 1, 2 % 10 gleich 2 und so weiter, bis 10 % 10 den Wert 0 ergibt. 11 % 10 ist erneut 1 und dieses Muster setzt sich bis zum nächsten Mehrfachen von 10, das heißt 20, fort. Diese Technik kommt zur Anwendung, wenn wir später im Detail auf die Schleifen zu sprechen kommen.
Frage:Wenn ich 5 durch 3 teile, erhalte ich 1. Was mache ich falsch?
Antwort: Wenn du einen Integerwert durch einen Anderen teilst, dann erhältst du auch einen Integerwert als Ergebnis. Und im Falle von 5/3 ist dies eben 1.
Um als Rückgabewert eine Bruchzahl zu erhalten, musst du Fließkommazahlen vom Typ float oder double verwenden.
5,0/3,0 ergibt als Bruchzahl: 1,66667
Falls deine Methode Integer als Parameter übernimmt, musst du diese in Fließkommazahlen von Typ float oder double umwandeln.
Um den Typ der Variablen zu ändern, musst du eine Typumwandlung (cast) vornehmen. Damit teilst du dem Compiler im wesentlichen mit, dass du weißt was du tust – und das solltest du auch, denn der Compiler wird sich deinem Willen nicht widersetzen.
In diesem speziellen Fall teilest du dem Compiler mit: »Ich weiß, du denkst, dies ist ein Integer, aber ich weiß, was ich tue, darum behandle diesen Wert als Fließkommazahl«.
Für die Typumwandlung kannst du noch die alte C-Methode oder den neueren vom ANSI-Standard empfohlenen Operator static_cast verwenden. Ein Beispiel dazu findst du im Listing „typecast.cpp“:
// Listing: typecast.cpp
// Typumwandlung von int nach float
#include <iostream>
using namespace std;
// Funktionsprototypen
void intDiv(int x, int y);
void floatDiv(int x, int y);
// Funktion: void intDiv(int x, int y)
void intDiv(int x, int y)
{
int z = x / y;
cout << "z: " << z << endl;
}
// Funktion: void floatDiv(int x, int y)
void floatDiv(int x, int y)
{
float a = (float)x; // Typecast: alter Stil
float b = static_cast<float>(y); // vorzuziehender Stil
float c = a / b;
cout << "c: " << c << endl;
}
int main(void)
{
int x = 5, y = 3;
intDiv(x,y);
floatDiv(x,y);
return 0;
}
Ausgabe:
z: 1
c: 1.66667
Analyse:
Die Zeilen 8 und 9 definieren die Funktionsprototypen der beiden selbst definierten Funktionen.
Die Zeilen 12 bis 16 definieren die Funktion (Methode) intDiv(). Diese druckt einen Integerwert aus.
Die Zeilen 19 bis 26 definieren die Funktion(Methode) floatDiv(). Diese druckt einen Gleitkommawert (float) aus.
In Zeile 21 wird der Variablen x per Typecast im alten Stil der Datentyp float zugeordnet und deren Wert der Gleitkommavariablen a übergeben.
In Zeile 22 wird der Variablen y per Typecast im neuen Stil der Datentyp float zugeordnet und deren Wert der Gleitkommavariablen b übergeben.
In Zeile 23 wird der Gleitkommavariablen c das Ergebnis der Berechnung aus a geteilt durch b übergeben.
Zeile 30 deklariert zwei lokale Integer-Variablen innerhalb von main().
Als Parameter werden in Zeile 32 intDiv und in Zeile 33 floatDiv übergeben.
Zusammengesetzte Operatoren
Häufig muss man einen Wert zu einer Variablen addieren und dann das Ergebnis an dieselbe Variable zuweisen. Im folgenden Beispiel wird der Wert der Variablen meinAlterum 2 erhöht:
int meinAlter = 5;
int temp;
temp = meinAlter + 2; // 5 und 2 addieren und in temp ablegen
meinAlter = temp; // nach meinAlter zurückschreiben
Dieses Verfahren ist allerdings recht umständlich. In C++ kann man dieselbe Variable auf beiden Seiten des Zuweisungsoperators einsetzen und das obige Codefragment eleganter formulieren:
meinAlter = meinAlter +2;
In der Algebra wäre dieser Ausdruck unzulässig, während ihn C++ als »addiere 2 zum Wert in meinAlter, und weise das Ergebnis an meinAlter zu« interpretiert.
Das Ganze läßt sich noch einfacher schreiben, auch wenn es im ersten Moment etwas unverständlich aussieht:
meinAlter += 2;
Der zusammengesetzte Additionsoperator (+=) addiert den R-Wert zum L-Wert und führt dann eine erneute Zuweisung des Ergebnisses an den L-Wert durch. Der Operator heißt »Plus-Gleich«. Die Anweisung ist dann als »meinAlter plus gleich 2« zu lesen. Wenn meinAlter zu Beginn den Wert 4 enthält, steht nach Ausführung dieser Anweisung der Wert 6 in meinAlter.
Weitere zusammengesetzte Operatoren gibt es für Subtraktion (-=), Division (/=), Multiplikation (*=) und Modulo-Operation (%=).
Inkrementieren und Dekrementieren
Am häufigsten hat man den Wert 1 zu einer Variablen zu addieren (bzw. zu subtrahieren). In C++ spricht man beim Erhöhen des Wertes um 1 von Inkrementieren und beim Verringern um 1 von Dekrementieren. Für diese Operationen stehen spezielle Operatoren bereit.
Der Inkrement-Operator (++) erhöht den Wert der Variablen um 1, der Dekrement- Operator (--) verringert ihn um 1. Möchte man die Variable C inkrementieren, schreibt man die folgende Anweisung:
C++; // Beginne mit C und inkrementiere den enthaltenen Wert.
Diese Anweisung ist äquivalent zur ausführlicher geschriebenen Anweisung
C = C + 1;
Das gleiche Ergebnis liefert die verkürzte Darstellung
C += 1;
Präfix und Postfix
Sowohl der Inkrement-Operator (++) als auch der Dekrement-Operator (--) existieren in zwei Spielarten: Präfix und Postfix. Die Präfix-Version wird vor den Variablennamen geschrieben (++mein Alter), die Postfix-Version danach (mein Alter++).
In einer einfachen Anweisung spielt es keine Rolle, welche Version man verwendet. In einer komplexen Anweisung ist es allerdings entscheidend, ob man eine Variable zuerst inkrementiert (oder dekrementiert) und dann das Ergebnis einer anderen Variablen zuweist. Der Präfix-Operator wird vor der Zuweisung ausgewertet, der Postfix- Operator nach der Zuweisung.
Die Semantik von Präfix bedeutet: Inkrementiere den Wert und übertrage ihn dann. Die Bedeutung des Postfix-Operators lautet dagegen: Übertrage den Wert und inkrementiere das Original.
Das folgende Beispiel verdeutlicht diese Vorgänge. Es sei angenommen, dass x eine ganze Zahl mit dem Wert 5 ist. Bei der Anweisung
int a = ++x;
inkrementiert der Compiler den Wert in x (und macht ihn damit zu 6), holt dann diesen Wert und weist ihn an a zu. Daher ist jetzt sowohl a als auch x gleich 6.
Schreibt man anschließend
int b = x++;
weist man den Compiler an, den Wert in x (6) zu holen, ihn an b zuzuweisen und dann den Wert von x zu inkrementieren. Demzufolge ist nun b gleich 6, während x gleich 7 ist. Das Listing „increment.cpp“ zeigt Verwendung und Besonderheiten beider Typen:
// Listing: increment.cpp
// demonstriert die Verwendung der
// Inkrement- u. Dekrement-Operatoren
// in Prä- und Postfix-Notation
#include <iostream>
using namespace std;
int main(void)
{
int meinAlter = 39; // initialisiert zwei Integer-Variablen
int deinAlter = 39;
cout << "Ich bin: " << meinAlter << " Jahre alt.\n";
cout << "Du bist: " << deinAlter << " Jahre alt\n";
meinAlter++; // Postfix-Inkrement
++deinAlter; // Präfix-Inkrement
cout << "Ein Jahr ist vergangen...\n";
cout << "Ich bin: " << meinAlter << " Jahre alt.\n";
cout << "Du bist: " << deinAlter << " Jahre alt\n";
cout << "Noch ein Jahr ist vergangen\n";
cout << "Ich bin: " << meinAlter++ << " Jahre alt.\n";
cout << "Du bist: " << ++deinAlter << " Jahre alt\n";
cout << "Und noch einmal ausgeben.\n";
cout << "Ich bin: " << meinAlter << " Jahre alt.\n";
cout << "Du bist: " << deinAlter << " Jahre alt\n";
return 0;
}
Ausgabe:
Ich bin: 39 Jahre alt.
Du bist: 39 Jahre alt
Ein Jahr ist vergangen...
Ich bin: 40 Jahre alt.
Du bist: 40 Jahre alt
Noch ein Jahr ist vergangen
Ich bin: 40 Jahre alt.
Du bist: 41 Jahre alt
Und noch einmal ausgeben.
Ich bin: 41 Jahre alt.
Du bist: 41 Jahre alt
Programme müssen auf irgendeine Weise die verwendeten Daten speichern. Variablen und Konstanten bieten verschiedene Möglichkeiten, diese Daten darzustellen und zu manipulieren. In C++ dient eine Variable dazu, Informationen zu speichern. Eine Variable ist eine Stelle im Arbeitsspeicher (Hauptspeicher, RAM) des Computers, in der man einen Wert ablegen und später wieder abrufen kann.
Man kann sich den Arbeitsspeicher als eine Reihe von Fächern vorstellen, die in einer langen Reihe angeordnet sind. Jedes Fach – oder Speicherstelle – ist fortlaufend nummeriert. Diese Nummern bezeichnet man als Speicheradressen oder einfach als Adressen. Eine Variable reserviert ein oder mehrere Fächer, in denen dann ein Wert abgelegt werden kann.
Der Name deiner Variablen (zum Beispiel meineVariable) ist ein Bezeichner für eines dieser Fächer, damit man es leicht finden kann, ohne dessen Speicheradresse zu kennen. Wie die folgende Abbildung zeigt, beginnt unsere Variable meineVariable an der Speicheradresse 103. Je nach Größe (= dem Datentyp) von meineVariable kann die Variable eine oder mehrere Speicheradressen belegen:
Speicher reservieren
Wenn man in C++ eine Variable definiert, muss man dem Compiler nicht nur deren Namen, sondern auch den Datentyp der Variablen mitteilen – also, ob es sich zum Beispiel um eine Ganzzahl (Integer) oder ein Zeichen (Buchstaben, Ziffern etc.) handelt. Anhand dieser Information weiß der Compiler, um welche Art Variable es sich handelt und wieviel Platz im Speicher für die Aufnahme des Wertes der Variablen zu reservieren ist.
Jedes »Fach« im Speicher ist ein Byte groß. Wenn die erzeugte Variable vier Bytes benötigt, muss man vier Bytes im Speicher – oder vier Fächer – reservieren. Der Variablentyp (zum Beispiel int für Integer) teilt dem Compiler mit, wie viele Speicherplätze (oder Fächer) für diese Variable benötigt werden.
Da Computer Werte in Bitsund Bytes darstellen und Speicher in Bytes gemessen wird, ist es wichtig, dass du diese Begriffe verstehst und verinnerlichst.
Größe von Integer-Werten
Jeder Variablentyp belegt im Speicher einen bestimmten Bereich, dessen Größe immer gleichbleibend ist, auf verschiedenen Computern aber unterschiedlich groß sein kann. Das heißt, ein Integer-Wert (Datentyp int) nimmt auf der einen Maschine zwei Bytes, auf einer anderen vielleicht vier ein – aber auf ein und demselben Computer ist dieser Platz immer gleich groß.
Eine Variable vom Typ char (zur Aufnahme von Zeichen) ist gewöhnlich ein Byte lang. Eine Ganzzahl vom Typ short belegt auf den meisten Computern zwei Bytes, eine Ganzzahl vom Typ long ist normalerweise vier Bytes lang, und eine Ganzzahl (ohne das Schlüsselwort short oder long) kann zwei oder vier Bytes einnehmen. Die Größe einer Ganzzahl wird vom Computer (16Bit oder 32Bit oder 64Bit) oder vom Compiler bestimmt. Auf einem 64-Bit-PC mit aktuellem C++ Compiler belegen die Ganzzahlen vier Bytes. Dieses Tutorial geht davon aus, dass Ganzzahlen vier Bytes groß sind. Das muss bei dir jedoch nicht so sein. Mit dem Listing „sizes.cpp“ lässt sich die genaue Größe der Typen auf Ihrem Computer bestimmen.
// Listing: sizes.cpp
#include <iostream>
using namespace std;
int main(void)
{
cout << "Groesse eines int:\t\t" << sizeof(int) << " Bytes.\n";
cout << "Groesse eines short int:\t" << sizeof(short int) << " Bytes.\n";
cout << "Groesse eines long int:\t" << sizeof(long int) << " Bytes.\n";
cout << "Groesse eines char:\t\t" << sizeof(char) << " Bytes.\n";
cout << "Groesse eines float:\t\t" << sizeof(float) << " Bytes.\n";
cout << "Groesse eines double:\t" << sizeof(double) << " Bytes.\n";
cout << "Groesse eines bool:\t\t" << sizeof(bool) << " Bytes.\n";
return 0;
}
Hinweis: Der sizeof()-Operator gibt den Speicherbedarf einer Variable oder eines Datentyps zurück.
Ausgabe:
Groesse eines int: 4 Bytes.
Groesse eines short int: 2 Bytes.
Groesse eines long int: 8 Bytes.
Groesse eines char: 1 Bytes.
Groesse eines float: 4 Bytes.
Groesse eines double: 8 Bytes.
Groesse eines bool: 1 Bytes.
Vorzeichenbehaftete Variablen: signed und unsigned
Alle genannten Typen kommen außerdem in zwei Versionen vor: mit Vorzeichen (signed ) und ohne Vorzeichen (unsigned). Dem liegt der Gedanke zugrunde, dass man manchmal zwar negative Zahlen benötigt, manchmal aber nicht. Ganze Zahlen (short und long) ohne das Wort unsigned werden als signed (das heißt: vorzeichenbehaftet) angenommen. Vorzeichenbehaftete Ganzzahlen sind entweder negativ oder positiv, während ganze Zahlen ohne Vorzeichen (unsigned int) immer positiv sind.
Da sowohl für vorzeichenbehaftete als auch vorzeichenlose Ganzzahlen dieselbe Anzahl von Bytes zur Verfügung steht, ist die größte Zahl, die man in einem unsigned int speichern kann, doppelt so groß wie die größte positive Zahl, die man in einem signed int unterbringt. Ein unsigned short int kann Zahlen von 0 bis 65535 speichern. Bei einem signed short int ist die Hälfte der Zahlen negativ. Daher kann ein signed short int Zahlen im Bereich von -32768 bis 32767 darstellen. Sollte dich dieses etwas verwirren, so findest du unter „Tabelle: Datentypen unter C/C++„ eine ausführliche Beschreibung.
Grundlegende Datentypen
In C/C++ gibt es weitere Variablentypen, die man zweckentsprechend in ganzzahlige Variablen (die bisher behandelten Typen), Fließkommavariablen und Zeichenvariablen einteilt.
Die Werte von Fließkommavariablen lassen sich als Bruchzahlen ausdrücken – das heißt, es handelt sich um reelle Zahlen.
Zeichenvariablen(char) nehmen ein einzelnes Byte auf und dienen der Speicherung der 256 möglichen Zeichen und Symbole der ASCII- und erweiterten ASCII-Zeichensätze.
Der ASCII–Zeichensatz ist ein Standard, der die im Computer verwendeten Zeichen definiert. ASCII steht als Akronym für American Standard Code for Information Interchange (amerikanischer Standard-Code für den Informationsaustausch). Nahezu jedes Computer-Betriebssystem unterstützt ASCII. Daneben sind meistens weitere internationale Zeichensätze möglich.
Die in C++-Programmen verwendeten gängigsten Variablentypen sind in der Tabelle „Variablentypen“ aufgeführt. Diese Tabelle zeigt den Variablentyp, den belegten Platz im Speicher (Grundlage ist der Computer des Autors) und den möglichen Wertebereich, der sich aus der Größe des Variablentyps ergibt. Vergleichen Sie dazu die Ausgabe des Programms aus dem Listing „sizes.cpp“.
Tabelle: Variablentypen
Typ
Größe
Wert
bool
1 Byte
true oder false
unsigned short int
2 Byte
0 bis 65,535
short int
2 Byte
-32,768 bis 32,767
unsigned long int
4 Byte
0 bis 4,294,967,295
long int
4 Byte
-2,147,483,648 bis
2,147,483,647
int (16 Bit)
2 Byte
-32,768 bis 32,767
int (32/64 Bit)
4 Byte
-2,147,483,648 bis
2,147,483,647
unsigned int (16 Bit)
2 Byte
0 bis 65,535
unsigned int (32/64 Bit)
4 Byte
0 bis 4,294,967,295
char
1 Byte
256 Zeichenwerte
float
4 Byte
1.2e-38 bis 3.4e38
double
8 Byte
2.2e-308 bis 1.8e308
Variablen definieren
Wenn du eine Variable deklarierst, wird dafür Speicherplatz allokiert (bereitgestellt). Was auch immer zu diesem Zeitpunkt sich in dem Speicherplatz befindet, stellt den Wert dieser Variablen dar. Wie du dieser Speicherposition einen neuen Wert zuweisen kannst, wirst du gleich erfahren.
Eine Variable erzeugt oder definiert man, indem man den Typ, mindestens ein Leerzeichen, den Variablennamen und ein Semikolon eintippt. Als Variablenname eignet sich nahezu jede Buchstaben-/Ziffernkombination, die allerdings keine Leerzeichen enthalten darf. Gültige Variablennamen sind zum Beispiel x, J23qrsnf und meinAlter. Gute Variablennamen sagen bereits etwas über den Verwendungszweck der Variablen aus („sprechende“ Variablennamen) und erleichtern damit das Verständnis für den Programmablauf. Die folgende Anweisung definiert eine Integer-Variable namens meinAlter:
int meinAlter;
Für die Programmierpraxis möchte ich dir nahelegen, wenig aussagekräftige Namen wie J23qrsnf zu vermeiden und kurze, aus einem Buchstaben bestehende Variablennamen (wie x oder i) auf Variablen zu beschränken, die nur kurz, für wenige Zeilen Code benötigt werden (z. B. als Zählvariable). Verwende ansonsten lieber „sprechende“ Namen wie meinAlter oder wie_viele_Katzen. Solche Namen sind leichter zu verstehen, wenn du dich drei Wochen später kopfkratzend nach dem Sinn und Zweck deines Codes fragst.
Groß-/Kleinschreibung, Umlaute und Sonderzeichen
C/C++ beachtet die Groß-/Kleinschreibung und behandelt demnach Großbuchstaben und Kleinbuchstaben als verschiedene Zeichen. Eine Variable namens alter unterscheidet sich von Alter und diese wiederum von ALTER.
Für die Schreibweise von Variablennamen gibt es mehrere Konventionen. Unabhängig davon, für welche du dich entscheidest ist es ratsam, innerhalb eines Programms bei der einmal gewählten Methode zu bleiben.
Viele Programmierer bevorzugen für Variablennamen Kleinbuchstaben. Wenn der Name aus zwei Wörtern besteht (zum Beispiel mein Auto), gibt es zwei übliche Konventionen: mein_auto oder meinAuto. Letztere Form wird auch als Kamel-Notation („CamelCase“) bezeichnet, da die Großschreibung im Wort selbst an einen Kamelhöcker erinnert.
Umlaute und bestimmte Sonderzeichen(äÄ oO üÜ ß \ /) dürfen in Variablennamen nicht vorkommen.
Ungarische Notation
Viele fortgeschrittene Programmierer schreiben Ihren Code in der sogenannten Ungarischen Notation. Dieser Notation liegt der Gedanke zugrunde, dass jede Variable mit einem oder mehreren Buchstaben beginnt, die auf den Typ der Variablen verweisen. So wird ganzzahligen Variablen (Integer) ein kleines ivorangestellt oder Variablen vom Typ long ein kleines l. Andere Notationen verweisen auf Konstanten, globale Variablen, Zeiger und so weiter. Dies ist jedoch für die C-Programmierung von wesentlich größerer Bedeutung als für C++, da C++ die Erzeugung benutzerdefinierter Datentypen unterstützt (siehe »Klassen«), und von sich aus typenstrenger ist.
Schlüsselwörter
In C++ sind bestimmte Wörter reserviert, die man nicht als Variablennamen verwenden darf. Es handelt sich dabei um die Schlüsselwörter, mit denen der Compiler das Programm steuert. Zu den Schlüsselwörtern gehören zum Beispiel if, while, for und main. Im allgemeinen fallen aussagekräftige Name für Variablen nicht mit Schlüsselwörtern zusammen. Eine Liste der C++-Schlüsselwörter findest du im „Anhang: Schlüsselwörter in C/C++„.
Mehrere Variablen gleichzeitig erzeugen
In einer Anweisung lassen sich mehrere Variablen desselben Typs gleichzeitig erzeugen, indem man den Typ schreibt und dahinter die Variablennamen durch Kommata getrennt aufführt. Dazu ein Beispiel:
unsigned int meinAlter, meinGewicht; //Zwei Variablen vom Typ unsigned int
long Flaeche, Breite, Laenge; //Drei Variablen vom Typ long
Wie man sieht, werden meinAlter und meinGewicht gemeinsam als Variablen vom Typ unsigned int deklariert. Die zweite Zeile deklariert drei eigenständige Variablen vom Typ long mit den Namen Flaeche, Breite und Laenge. Der Typ (long) wird allen Variablen zugewiesen, so dass man in ein- und derselben Definitionsanweisung keine unterschiedlichen Typen festlegen kann.
Werte an Variablen zuweisen
Einen Wert weist man einer Variablen mit Hilfe des Zuweisungsoperators (=) zu. Zum Beispiel formuliert man die Zuweisung des Wertes 5 an die Variable Breite wie folgt:
unsigned short Breite;
Breite = 5;
Diese Schritte kann man zusammenfassen und die Variable Breite bei ihrer Definition initialisieren:
unsigned short Breite = 5;
Die Initialisierung sieht nahezu wie eine Zuweisung aus, und bei Integer-Variablen gibt es auch kaum einen Unterschied. Bei der späteren Behandlung von Konstanten werden Sie sehen, das man bestimmte Werte initialisieren muss, da Zuweisungen nicht möglich sind. Der wesentliche Unterschied besteht darin, dass die Initialisierung bei der Erzeugung der Variablen stattfindet.
Ebenso wie Sie mehrere Variable gleichzeitig definieren können, ist es auch möglich, mehr als eine Variable auf einmal zu erzeugen. Betrachten wir folgendes Beispiel:
//Erzeugung von zwei long-Variablen und ihre Initialisierung
long Breite = 5, Laenge = 7;
In diesem Beispiel wird die Variable Breite vom Typ long mit 5 und die Variable Laenge vom Typ long mit dem Wert 7 initialisiert. Sie können aber auch Definitionen und Initialisierungen mischen:
int meinAlter = 39, ihrAlter, seinAlter = 40;
Das Listing „flaeche.cpp“ zeigt ein vollständiges Programm, das du sofort kompilieren kannst. Es berechnet die Fläche eines Rechtecks und schreibt das Ergebnis auf den Bildschirm:
// Listing: flaeche.cpp
// - demonstriert den Einsatz von Variablen
#include <iostream>
using namespace std;
int main(void)
{
unsigned short int Width = 5, Length;
Length = 10;
// eine Variable vom Typ 'unsigned short int' erzeugen und
// mit dem Ergebnis der Multiplikation von Width und Length
// initialisieren:
unsigned short int Area = (Width * Length);
// Werte ausgeben:
cout << "Breite: " << Width << endl;
cout << "Laenge: " << Length << endl;
cout << "Flaeche: " << Area << endl;
return 0;
}
Ausgabe:
Breite: 5
Laenge: 10
Flaeche: 50
Programmanalyse:
Zeile 3 enthält die include-Anweisung für die iostream-Bibliothek, die wir benötigen, um cout verwenden zu können. In Zeile 4 beginnt das Programm.
In Zeile 9 wird die Variable Width als vorzeichenloser short int definiert und mit dem Wert 5 initialisiert. Eine weitere Variable vom gleich Typ, Length, wird ebenfalls hier definiert, aber nicht initialisiert. In Zeile 10 erfolgt die Zuweisung des Wertes 10 an die Variable Length.
Zeile 15 definiert die Variable Area vom Typ unsigned short int und initialisiert sie mit dem Wert, der sich aus der Multiplikation von Width und Length ergibt. In den Zeilen 18 bis 20 erfolgt die Ausgabe der Variablenwerte auf dem Bildschirm. Beachte, dass das spezielle Wort endl eine neue Zeile erzeugt.
Pseudo-Datentypen mit typedef
Es ist lästig, zeitraubend und vor allem fehleranfällig, wenn man häufig unsigned short int schreiben muss. In C++ kann man einen Alias für diese Wortfolge mit Hilfe des Schlüsselwortes typedef (für Typendefinition) erzeugen.
Mit diesem Schlüsselwort erzeugt man lediglich ein Synonym und keinen neuen Typ (letzteres heben wir uns für später auf). Auf das Schlüsselwort typedef folgt ein vorhandener Typ und danach gibt man den neuen Namen an. Den Abschluss bildet ein Semikolon. Beispielsweise erzeugt
typedef unsigned short int USHORT;
den neuen Namen USHORT, den man an jeder Stelle verwenden kann, wo man sonst unsigned short int schreiben würde. Listing „typedef.cpp“ ist eine Neuauflage von Listing „flaeche.cpp“ und verwendet die Typendefinition USHORT anstelle von unsigned short int. Schauen wir uns das am praktischen Beispiel an:
// Listing: typedef.cpp
// Zeigt die Verwendung des Schlüsselworts typedef
#include <iostream>
using namespace std;
typedef unsigned short int USHORT; // mit typedef definiert
int main()
{
USHORT Width = 5;
USHORT Length;
Length = 10;
USHORT Area = Width * Length;
cout << "Breite: " << Width << "\n";
cout << "Laenge: " << Length << endl;
cout << "Flaeche: " << Area <<endl;
return 0;
}
Das Programm macht dasselbe wie sein Vorgänger flaeche.cpp – allerdings haben wir uns per typedef ein wenig Tipparbeit gespart.
Ausgabe:
Breite: 5
Laenge: 10
Flaeche: 50
Wann verwendet man short und wann long?
Neueinsteiger in die C++-Programmierung wissen oft nicht, wann man eine Variable als long und wann als short deklarieren sollte. Die Regel ist einfach: Wenn der in der Variablen zu speichernde Wert zu groß für seinen Typ werden kann, nimmt man einen größeren Typ.
Wie die Tabelle Datentypen unter C/C++ zeigt, können ganzzahlige Variablen vom Typ unsigned short (vorausgesetzt, dass sie aus 2 Bytes bestehen) nur Werte bis zu 65535 aufnehmen. Variablen vom Typ signed short verteilen ihren Wertebereich auf negative und positive Zahlen. Deshalb ist das Maximum eines solchen Typs nur halb so groß.
Obwohl Integer-Zahlen vom Typ unsigned long sehr große Ganzzahlen aufnehmen können (bis 4.294.967.295), hat auch dieser Typ einen begrenzten Wertebereich. Benötigt man größere Zahlen, kann man auf float oder double ausweichen und einen gewissen Genauigkeitsverlust in Kauf nehmen. Variablen vom Typ float oder double können zwar extrem große Zahlen speichern, allerdings sind auf den meisten Computern nur die ersten 7 bzw. 19 Ziffern signifikant. Das bedeutet, dass die Zahl nach dieser Stellenzahl mit Verlust gerundet wird. Alternativ könnte man aber auch den Datentyp unsigned long long verwenden – dieser speichert Werte zwischen 0 und 18446744073709551615.
Schlechter Tipp für Fulderer: Kürzere Variablen belegen weniger Speicher. Heute jedoch ist Speicher billig und das Leben kurz. Deshalb lassen Sie sich nicht davon abhalten, int zu verwenden, auch wenn damit 4 Byte auf Ihrem PC belegt werden.
Ein guter Tipphingegen ist es, vor der Vergabe eines Datentyps für eine Variable genau zu prüfen, wie groß eine zu speichernde Zahl tatsächlich werden könnte.
Bereichsüberschreitung bei Integer-Werten vom Typ unsigned
Die Tatsache, dass Ganzzahlen vom Typ unsigned long nur einen begrenzten Wertebereich aufnehmen können, ist nur selten ein Problem. Aber was passiert, wenn der Platz im Verlauf des Programms zu klein wird?
Wenn eine Ganzzahl vom Typ unsigned ihren Maximalwert erreicht, schlägt der Zahlenwert um und beginnt von vorn. Vergleichbar ist das mit einem Kilometerzähler. Das Listing „ueberlauf.cpp“ demonstriert den Versuch, einen zu großen Wert in einer Variablen vom Typ short int abzulegen:
// Listing: ueberlauf.cpp
// demonstriert den Überlauf einer Variablen
#include <iostream>
using namespace std;
int main(void)
{
unsigned short int smallNumber;
smallNumber = 65535;
cout << "Kleine Zahl: " << smallNumber << endl;
smallNumber++;
cout << "Kleine Zahl: " << smallNumber << endl;
smallNumber++;
cout << "Kleine Zahl: " << smallNumber << endl;
return 0;
}
Ausgabe:
Kleine Zahl: 65535
Kleine Zahl: 0
Kleine Zahl: 1
Programmanalyse:
Zeile 9 deklariert smallNumber vom Typ unsigned short int(auf dem Computer des Autors 2 Bytes für einen Wertebereich zwischen 0 und 65535). Zeile 11 weist den Maximalwert smallNumber zu und gibt ihn in Zeile 12 aus.
Die Anweisung in Zeile 13 inkrementiertsmallNumber, das heißt, addiert den Wert 1. Das Symbol für das Inkrementieren ist ++ (genau wie der Name C++ eine Inkrementierung von C symbolisieren soll). Der Wert in smallNumber sollte nun 65536 lauten. Da aber Ganzzahlen vom Typ unsigned short keine Zahlen größer als 65535 speichern können, schlägt der Wert zu 0 um. Die Ausgabe dieses Wertes findet in Zeile 14 statt.
Die Anweisung in Zeile 15 inkrementiert smallNumber erneut. Es erscheint nun der neue Wert 1.
Bereichsüberschreitung bei Integer-Werten vom Typ signed
Im Gegensatz zu unsigned Integer-Zahlen besteht bei einer Ganzzahl vom Typ signed die Hälfte des Wertebereichs aus negativen Werten. Den Vergleich mit einem Kilometerzähler stellen wir nun so an, dass er bei einem positiven Überlauf vorwärts und bei negativen Zahlen rückwärts läuft. Vom Zählerstand 0 ausgehend erscheint demnach die Entfernung ein Kilometer entweder als 1 oder -1. Wenn man den Bereich der positiven Zahlen verlässt, gelangt man zur größten negativen Zahl und zählt dann weiter herunter bis Null. das Listing „ueberlauf2.cpp“ zeigt die Ergebnisse, wenn man auf die maximale positive Zahl in einem signed short int eine 1 addiert:
// Listing: ueberlauf2.cpp
// demonstriert den Überlauf einer Variablen
#include <iostream>
using namespace std;
int main()
{
short int smallNumber;
smallNumber = 32767;
cout << "Kleine Zahl: " << smallNumber << endl;
smallNumber++;
cout << "Kleine Zahl: " << smallNumber << endl;
smallNumber++;
cout << "Kleine Zahl: " << smallNumber << endl;
return 0;
}
Ausgabe:
Kleine Zahl: 32767
Kleine Zahl: -32768
Kleine Zahl: -32767
Programmanalyse:
Zeile 9 deklariert smallNumber dieses Mal als signed short int (wenn man nicht explizit unsigned festlegt, gilt per Vorgabe signed). Das Programm läuft fast genau wie das vorherige, liefert aber eine andere Ausgabe. Um diese Ausgabe zu verstehen, muss man die Bit-Darstellung vorzeichenbehafteter (signed) Zahlen in einer Integer-Zahl von 2 Bytes Länge kennen.
Analog zu vorzeichenlosen Ganzzahlen findet bei vorzeichenbehafteten Ganzzahlen ein Umschlagen vom größten positiven Wert in den höchsten negativen Wert statt.
Zeichen
Zeichen-Variablen (vom Typ char) sind in der Regel 1 Byte groß und können damit 256 Werte aufnehmen. Eine Variable vom Typ char kann als kleine Zahl (0-255) oder als Teil des ASCII-Zeichensatzes interpretiert werden. ASCII steht für American Standard Code for Information Interchange (amerikanischer Standard-Code für den Informationsaustausch). Mit dem ASCII-Zeichensatz und seinem ISO-Gegenstück (International Standards Organization) können alle Buchstaben, Zahlen und Satzzeichen codiert werden.
Computer haben keine Ahnung von Buchstaben, Satzzeichen oder Sätzen. Alles was sie verstehen, sind Zahlen. Im Grunde genommen kann ein Computer nur feststellen, ob genügend Strom an einem bestimmten Leitungspunkt vorhanden ist. Wenn ja, wird dies intern mit einer 1 dargestellt, wenn nicht mit einer 0. Durch die Kombination von Einsen und Nullen erzeugt der Computer Muster, die als Zahlen interpretiert werden können. Und diese Zahlen können wiederum Buchstaben und Satzzeichen zugewiesen werden
Im ASCII-Code wird dem kleinen »a« der Wert 97 zugewiesen. Allen Klein- und Großbuchstaben sowie den Zahlen und Satzzeichen werden Werte zwischen 1 und 128 zugewiesen. Weitere 128 Zeichen und Symbole sind für den Computer-Hersteller reserviert. In der Realität hat sich aber der erweiterte IBM-Zeichensatz und auf modernen Rechnern der UTF-8 Zeichensatz als Quasi-Standard durchgesetzt.
Zeichen und Zahlen
Wenn du ein Zeichen, zum Beispiel »a«, in einer Variablen vom Typ char ablegst, steht dort eigentlich eine Zahl zwischen 0 und 255. Der Compiler kann Zeichen (dargestellt durch ein einfaches Anführungszeichen gefolgt von einem Buchstaben, einer Zahl oder einem Satzzeichen und einem abschließenden einfachen Anführungszeichen) problemlos in ihren zugeordneten ASCII-Wert und wieder zurück verwandeln.
Die Wert/Buchstaben-Beziehung ist zufällig. Dass dem kleinen »a« der Wert 97 zugewiesen wurde, ist reine Willkür. So lange jedoch, wie jeder (Tastatur, Compiler und Bildschirm) sich daran hält, gibt es keine Probleme. Du solltest jedoch beachten, dass zwischen dem Wert 5 und dem Zeichen »5« ein großer Unterschied besteht. Letzteres hat einen Wert von 53, so wie das »a« einen Wert von 97 hat.
Schreiben wir ein Programm, welches Zeichen anhand ihres ASCII-Codes ausdruckt (Listing „asciicode.cpp“):
// Listing: asciicode.cpp
// druckt Zeichen anhand von Zahlen
#include <iostream>
using namespace std;
int main(void)
{
for (int i = 32; i<128; i++)
cout << (char) i;
return 0;
}
Dieses einfache Programm druckt die Zeichenwerte für die Integer 32 bis 127.
Besondere Zeichen
Der C++-Compiler kennt einige spezielle Formatierungszeichen. Die Tabelle Escape-Codes listet die geläufigsten auf. In deinem Code gibst du diese Zeichen mit einem vorangestellten Backslash (auch Escape-Zeichen genannt) ein. Um zum Beispiel einen Tabulator in deinen Code mit aufzunehmen, würdest du ein einfaches Anführungszeichen, den Backslash, den Buchstaben t und ein abschließendes einfaches Anführungszeichen eingeben.
char tabZeichen = '\t';
Dies Beispiel deklariert eine Variable vom Typ char und initialisiert sie mit dem Zeichenwert \t, der als Tabulator erkannt wird. Diese speziellen Druckzeichen werden benötigt, wenn die Ausgabe entweder auf dem Bildschirm, in eine Datei oder einem anderen Ausgabegerät erfolgen soll.
Ein Escape-Zeichen ändert die Bedeutung des darauf folgenden Zeichens. So ist das Zeichen n zum Beispiel nur der Buchstabe n. Wird davor jedoch ein Escape-Zeichen gesetzt (\), steht das Ganze für eine neue Zeile.
Tabelle: Escape-Codes
Zeichen
Bedeutung
\n
Neue Zeile
\t
Tabulator
\b
Backspace
\“
Anführungszeichen
\‘
Einfaches Anführungszeichen
\?
Fragezeichen
\\
Backslash
Was ist eine Konstante?
Konstanten sind ebenso wie Variablen benannte Speicherstellen. Während sich Variablen aber ändern können, behalten Konstanten – wie der Name bereits sagt – immer ihren Wert. Du musst eine Konstante bei der Erzeugung initialisieren und kannst ihr dann später keinen neuen Wert zuweisen.
Literale Konstanten
C++ kennt zwei Arten von Konstanten: literale und symbolische.
Eine literale Konstante ist ein Wert, den man direkt in das Programm an der Stelle des Vorkommens eintippt. In der Anweisung
int meinAlter = 39;
ist meinAlter eine Variable vom Typ int, während die Zahl 39 eine literale Konstante bezeichnet. Man kann 39 keinen Wert zuweisen oder diesen Wert ändern.
Symbolische Konstanten
Eine symbolische Konstante wird genau wie eine Variable durch einen Namen repräsentiert. Allerdings lässt sich im Gegensatz zu einer Variablen der Wert einer Konstanten nicht nach deren Initialisierung ändern.
Wenn dein Programm eine Integer-Variable namens Studenten und eine weitere namens Klassen enthält, kann man die Anzahl der Studenten berechnen, wenn die Anzahl der Klassen bekannt ist und man weiß, dass 15 Studenten zu einer Klasse gehören:
Studenten = Klassen * 15;
Wenn man später die Anzahl der Stunden pro Klasse ändern möchte, braucht man das nur in der Definition der Konstanten StudentenProKlasse vorzunehmen, ohne dass man alle Stellen ändern muss, wo man diesen Wert verwendet hat.
Es gibt zwei Möglichkeiten, eine symbolische Konstante in C++ zu deklarieren. Die herkömmliche und inzwischen veraltete Methode aus dem Sprachumfang von C erfolgt mit der Präprozessor-Direktiven #define.
Konstanten mit #define definieren
Um eine Konstante auf die herkömmliche Weise zu definieren, gibt man ein:
#define StudentenProKlasse 15
Beachte, dass StudentenProKlasse keinen besonderen Typ (etwa int oder char) aufweist. #define nimmt eine einfache Textersetzung vor. Der Präprozessor schreibt an alle Stellen, wo StudentenProKlasse vorkommt, die Zeichenfolge 15 in den Quelltext.
Da der Präprozessor vor dem Compiler ausgeführt wird, kommt Ihr Compiler niemals mit der symbolischen Konstanten in Berührung, sondern bekommt immer die Zahl 15 zugeordnet.
Konstanten mit const definieren
Obwohl #define nach wie vor funktioniert, gibt es in C++ eine neue, bessere und elegantere Lösung zur Definition von Konstanten:
const unsigned short int StudentenProKlasse = 15;
Dieses Beispiel deklariert ebenfalls eine symbolische Konstante namens StudentenProKlasse , dieses Mal ist aber StudentenProKlasse als Typunsigned short int definiert. Diese Version bietet verschiedene Vorteile. Zum einen lässt sich der Code leichter warten und zum anderen werden unnötige Fehler vermieden. Der größte Unterschied ist der, dass diese Konstante einen Typ hat und der Compiler die zweckmäßige – sprich typgerechte – Verwendung der Konstanten frühzeitig prüfen kann. Es liegt auf der Hand, dass du dieser Lösung den Vorzug geben solltest.
Erinnerung:Konstanten können nicht während der Ausführung des Programms geändert werden. Wenn du gezwungen bist, die Konstante studentsPerClass zu ändern, dann musst du den Code ändern und neu kompilieren.
Aufzählungstypen (Enumeration)
Mit Hilfe von Aufzählungskonstanten (enum) können Sie neue Typen erzeugen und dann Variablen dieser Typen definieren, deren Werte auf einen bestimmten Bereich beschränkt sind. Beispielsweise kann man FARBE als Aufzählung deklarieren und dafür fünf Werte definieren: ROT, BLAU, GRUEN, WEISS und SCHWARZ.
Die Syntax für Aufzählungstypen besteht aus dem Schlüsselwort enum, gefolgt vom Typennamen, einer öffnenden geschweiften Klammer, einer durch Kommata getrennte Liste der möglichen Werte, einer schließenden geschweiften Klammern und einem Semikolon. Dazu ein Beispiel:
enum FARBE { ROT, BLAU, GRUEN, WEISS, SCHWARZ };
Diese Anweisung realisiert zwei Aufgaben:
FARBE ist der Name der Aufzählung, das heißt, ein neuer Typ.
ROT wird zu einer symbolischen Konstanten mit dem Wert 0, BLAU zu einer symbolischen Konstanten mit dem Wert 1, GRUEN zu einer symbolischen Konstanten mit dem Wert 2 usw.
Jeder Aufzählungskonstanten ist ein Integer-Wert zugeordnet. Wenn man nichts anderes festlegt, weist der Compiler der ersten Konstanten den Wert 0 zu und nummeriert die restlichen Konstanten fortlaufend durch. Jede einzelne Konstante lässt sich aber auch mit einem bestimmten Wert initialisieren, wobei die Werte der nicht initialisierten Konstanten immer um 1 höher sind als die Werte ihres Vorgängers. Schreibt man daher
erhält ROT den Wert 100, BLAU den Wert 101, GRUEN den Wert 500, WEISS den Wert 501 und SCHWARZ den Wert 700.
Damit kannst du Variablen vom Typ FARBE definieren, denen dann allerdings nur einer der Aufzählungswerte (in diesem Falle ROT, BLAU, GRUEN, WEISS oder SCHWARZ oder die Werte 100, 101, 500, 501 oder 700) zugewiesen werden kann. Du kannst Ihrer Variablen FARBE beliebige Farbwerte zuweisen, ja sogar beliebige Integer-Werte, auch wenn es keine gültige Farbe ist. Ein guter Compiler wird in einem solchen Fall jedoch eine Fehlermeldung ausgeben. Merke dir unbedingt, dass Aufzählungsvariablen vom Typ unsigned int sind und dass es sich bei Aufzählungskonstanten um Integer-Variablen handelt. Es ist jedoch von Vorteil, diesen Werten einen Namen zu geben, wenn Sie mit Farben, Wochentagen oder ähnlichen Wertesätzen arbeiten. Im Listing „enum.cpp“ findest du ein Programm, das eine Aufzählungskonstante verwendet:
// Listing: enum.cpp
// demonstriert die Verwendung von
// Aufzählungskonstanten
#include <iostream>
using namespace std;
int main(void)
{
// Variable für die Auswahl eines Wochentags deklarieren:
int auswahl;
// Aufzählung definieren:
enum Wochentage
{
Sonntag, Montag, Dienstag,
Mittwoch, Donnerstag, Freitag,
Samstag
};
// Programmtitel ausgeben:
cout << "\nWochentage" << endl;
cout << "----------" << endl;
// Benutzereingabe holen:
cout << "\nGib die Kennziffer eines Wochentags an (0 - 6): ";
cin >> auswahl;
// -- Benutzereingabe auswerten --
switch(auswahl)
{
case Sonntag:
cout << "\nDu hast den Sonntag im Sinn...";
break;
case Montag:
cout << "\nDu hast den Montag im Sinn...";
break;
case Dienstag:
cout << "\nDu hast den Dienstag im Sinn...";
break;
case Mittwoch:
cout << "\nDu hast den Mittwoch im Sinn...";
break;
case Donnerstag:
cout << "\nDu hast den Donnerstag im Sinn...";
break;
case Freitag:
cout << "\nDu hast den Freitag im Sinn...";
break;
case Samstag:
cout << "\nDu hast den Samstag im Sinn...";
break;
default:
cout << "\nFehler: Nur Ziffern 0 bis 6 erlaubt!";
break;
}
if ((auswahl == Sonntag) || (auswahl == Samstag))
cout << "\nDu bist ja schon im Wochenende!\n";
else
cout << "\nOK - nimm dir mal 'nen Tag frei!\n";
return 0;
}
Ausgabe:
bergmann@MB-Workstation:~/Projekte/Cpp-Kurs$ ./enum
Wochentage
----------
Gib die Kennziffer eines Wochentags an (0 - 6): 3
Du hast den Mittwoch im Sinn...
OK - nimm dir mal 'nen Tag frei!
Programmanalyse:
Der Anwender wird gebeten, einen Wert zwischen 0 und 6 einzugeben. Die Eingabe von z. B. »Sonntag« als Tag ist nicht möglich. Das Programm hat keine Ahnung, wie es die Buchstaben in Sonntag in einen der Aufzählungswerte übersetzen soll. Es kann jedoch die numerischen Werte, die der Anwender eingibt, mit einer oder mehreren Aufzählungskonstanten (wie in den Zeilen 15 bis 17 notiert) abgleichen. Die Verwendung von Aufzählungskonstanten verdeutlicht die Absicht des Vergleichs besser. Du hättest das Ganze natürlich auch mit Integer-Konstanten erreichen können – mehr dazu im nächsten Beispiel.
Zeile 11 deklariert eine Integer-Variable, in der die Benutzereingabe gespeichert werden soll
In den Zeilen 13 bis 18 definieren wir eine Aufzählung mit dem Namen Wochentage. Sie beinhaltet die Wochentage als symbolische Konstanten. Da keiner Konstanten explizit ein Wert zugewiesen wurde, beginnt die Aufzählung mit Sonntag = 0 und endet mit Samstag = 6.
Die Zeilen 25 und 26 dienen der Benutzereingabe: Zeile 25 fordert per cout zur Eingabe auf und Zeile 26 weist die Eingabe per cin der Variablen auswahl zu.
Ab Zeile 29 werten wir die Benutzereingabe aus und reagieren darauf. Dazu bedienen wir uns zunächst der Verzweigung mittels switch – case – break. Der switch-Anweisung folgt, in geschweifte Klammern gepackt, ein Anweisungsblock. Er besteht aus mehreren case-Anweisungen, die mittels einer break-Anweisung abgeschlossen werden. Zwischen case und break notiert man weitere auszuführende Anweisungen – in unserem Fall die Ausgabe des gewählten Wochentags.
In Zeile 52 wird eine default-Aktion für switch angegeben. Sie wird ausgeführt, wenn der Benutzer eine Zahl (oder gar ein Zeichen) eingibt, die nicht durch die Fallunterscheidungen der einzelnen case-Anweisungen abgedeckt wird. In unserem vorliegenden Fall gibt default eine Fehlermeldung aus. Die switch-Anweisung eignet sich besonders gut, um auf bestimmte, einzelne Werte zu reagieren.Die switch-Anweisung wertet die symbolischen Konstanten Sonntag (0) bis Samstag (6) aus, indem sie deren hinterlegte numerische Werte für Vergleiche heranzieht.
Ab Zeile 57 benutzen wir eine if – elseVerzweigung, um dem Benutzer, je nach seiner getroffenen Auswahl, noch eine kleine Meldung zukommen zu lassen. Das Schema dieses Instruments zu Fallunterscheidung sieht wie folgt aus:
if (1. Bedingung || 2. Bedingung) // WENN Bedingung 1 ODER Bedingung 2 zutrifft, dann
gib Meldung 1 aus
else // ANDERNFALLS...
gib Meldung 2 aus
Zeile 57 prüft die erste Bedingung: Hat die Variable auswahl den Wert 0 (Sonntag) ODER 6 (Samstag)? Falls Bedingung 1 zutrifft, dann wird in Zeile 58 die Meldung „Du bist ja schon im Wochenende!“ ausgegeben und die Verzweigung wird verlassen. Der else-Zweig wird dabei übersprungen, da die Bedingung ja bereits erfüllt ist. Andernfalls geht es weiter in Zeile 59.
Zeile 59: Der else-Zweig prüft, ob Bedingung 1 nicht erfüllt wurde (weder Sonntag, noch Samstag gewählt). Trifft das zu, so wird in Zeile 60 die Meldung „OK – nimm dir mal ’nen Tag frei!“ ausgegeben.Die if-Anweisung eignet sich gut, wenn mehrere Bedingungen miteinander kombiniert werden müssen. Die Bedingungen können dabei mittels bool’scher Operatoren miteinander verknüpft werden. Die Zeichenfolge „||“ steht in C/C++ für das logische ODER.
Das Programm prüft zuerst per switch-Anweisung, ob ein gültiger Wert eingegeben wurde und arbeitet danach die verknüpften Bedingungen der if-Anweisung ab. Beide Programmsegmente liefern auf jeden Fall eine Meldung.
Auch bei Eingabe einer nicht erlaubten Ziffer ändert sich daran nichts. Das Programm gibt dann zwar eine (gewollte) Fehlermeldung aus, bietet dir aber trotzdem einen Urlaubstag an. 😉
Ich hatte ja eingangs schon erwähnt, dass man das Programm, anstatt eine Aufzählung zu verwenden, auch mit einfachen Integer-Konstanten umsetzen könnte. Das Listing „intchoice.cpp“ zeigt dir in einer abgespeckten Version, wie das funktioniert:
// Listing: intchoice.cpp
// demonstriert die Verwendung von
// Integer-Konstanten
#include <iostream>
using namespace std;
int main(void)
{
// Variable für die Auswahl eines Wochentags deklarieren:
int auswahl;
// Integer-Konstanten für Wochentage definieren:
const int Sonntag = 0;
const int Montag = 1;
const int Dienstag = 2;
const int Mittwoch = 3;
const int Donnerstag = 4;
const int Freitag = 5;
const int Samstag = 6;
// Benutzereingabe holen:
cout << "\nGib die Kennziffer eines Wochentags an (0 - 6): ";
cin >> auswahl;
// äußere Schleife: Fehleingaben abfangen
if ((auswahl > -1) && (auswahl < 7))
// innere Schleife: Eingabe auswerten
if ((auswahl == Sonntag) || (auswahl == Samstag))
cout << "\nDu bist ja schon im Wochenende!\n";
else
cout << "\nOK - nimm dir mal 'nen Tag frei!\n";
else
cout << "Fehler: nur 0 bis 6 erlaubt!" << endl;
return 0;
}
Ausgabe:
# 1. Programmlauf: Ungültige Eingabe
bergmann@MB-Workstation:~/Projekte/Cpp-Kurs$ ./intchoice
Gib die Kennziffer eines Wochentags an (0 - 6): 8
Fehler: nur 0 bis 6 erlaubt!
# 2. Programmlauf: Tag 3 gewählt
bergmann@MB-Workstation:~/Projekte/Cpp-Kurs$ ./intchoice
Gib die Kennziffer eines Wochentags an (0 - 6): 3
OK - nimm dir mal 'nen Tag frei!
Programmanalyse:
In diesem Programm wurde jede Konstante (Sonntag, Montag etc.) einzeln definiert, ein Aufzählungstyp Tage existiert hier nicht. Aufzählungskonstanten haben allerdings den Vorteil, dass sie selbsterklärend sind – die Absicht des Aufzählungstyps Tage ist jedem sofort klar.
Anstatt einen Aufzählungstyp zu verwenden, definieren wir hier in den Zeilen 13 bis 19 einzelne Integer-Konstanten und weisen ihnen einen entsprechenden Wert zu.
Die Zeilen 22 und 23 dienen der Benutzereingabe mittels cout und cin.
Zeile 23: Der Variablen auswahl wird per cin der eingelesene Wert übergeben.
In Zeile 25 beginnt die äußereif-else Schleife – sie dient zum Abfangen unzulässiger Zifferneingaben und endet in Zeile 33. Die Bedingung lauten dieses Mal: WENN auswahl GRÖSSER als -1 UND auswahl KLEINER als 7... (die Zeichenfolge „&&“ steht für das logischen UND in C/C++)
Falls die Bedingung nicht zutrifft (Eingabe einer nicht erlaubten Ziffer), dann wird das Programm beim else in Zeile 32 fortgesetzt und gibt in Zeile 33 eine Fehlermeldung aus. Die äußere if-Schleife ist damit abgeschlossen und das Programm endet in Zeile 35.
Trifft die Bedingung zu, so wird die innere if-Schleife ab Zeile 28 abgearbeitet. Sie prüft, ob einer der Werte für Sonntag oder Samstag vorliegt, verzweigt entsprechend (zu Zeile 29 oder Zeile 31) und endet dann. Somit kann dann auch die äußere if-Schleife geschlossen werden, da alle Bedingungen abgearbeitet sind. Auch in diesem Fall endet das Programm anschließend mit Zeile 35.
Das Beispiel demonstriert auch den Umstand, dass if-else Schleifen ineinander verschachtelt werden können. In diesem Fall wird zunächst die äußere Schleife verarbeitet/ausgewertet und erst danach die Innere.
Zusammenfassung
In diesem Kapitel hast du Variablen und Konstanten für numerische Werte und Zeichen kennengelernt, in denen du in C/C++ während der Ausführung deines Programms Daten speichern kannst. Numerische Variablen sind entweder Ganzzahlen (char, short und long int) oder Fließkommazahlen (float und double). Die Zahlen können darüber hinaus vorzeichenlos oder vorzeichenbehaftet (unsigned und signed) sein. Wenn auch alle Typen auf unterschiedlichen Computersystemen unterschiedlich groß sein können, so wird jedoch mit dem Typ für einen bestimmte Computertyp immer eine genaue Größe angegeben.
Bevor man eine Variable verwenden kann, muss man sie deklarieren. Damit legt man gleichzeitig auch den Datentyp fest, der sich in der Variablen speichern lässt. Wenn man eine zu große Zahl in einer Integer-Variablen ablegt, erhält man ein falsches Ergebnis.
Dieses Kapitel hat auch literale und symbolische Konstanten, sowie Aufzählungskonstanten behandelt und die beiden Möglichkeiten zur Deklaration symbolischer Konstanten aufgezeigt: Die Verwendung von #define und des Schlüsselwortes const.
C++ Funktionen sind kleine Unterprogramme, die Teilprobleme lösen. Funktionen sind ein wichtiges Werkzeug, um den Quelltext eines Programms zu ordnen und wesentliche Algorithmen und zusammenhängende Anweisungsblöcke der main()-Hauptfunktion in einer zusammenhängenden Form auszulagern. Sie werden außerhalb der main()-Hauptfunktion definiert und vereinfachen somit das Verständnis und die Lesbarkeit des Quelltextes.
Deklaration und Definition von Funktionen
Im Grunde bedeutet die Definition einer Funktion nichts Anderes, als eine Code-Block (Anweisungsblock) mit einem Funktionsnamen zu verbinden. Die Definition einer C++ Funktion besteht aus einer Deklaration und einem Anweisungsblock.
Für die Deklarationeiner C++ Funktion sind gewisse Angaben erforderlich und gewisse zusätzliche Spezifikationen möglich (z.B. ‚inline‚ oder ‚virtual‚). Um eine Funktion schließlich zu definieren, muss man zusätzlich noch die zu erledigenden Anweisungen in einem Anweisungsblock zusammenfassen. Die allgemeine Definition einer C++ Funktion ist der formalen Definition einer mathematischen Funktion nicht unähnlich und man könnte sie auch wie folgt definieren: „Eine C++ Funktion ist eine Abbildung von dem Datenraum der Argumentenliste in den Datenraum des Rückgabetyps. Die dabei benutzte Abbildungsvorschrift findet sich in ihrem Anweisungsblock. Formal besitzt sie somit die folgende Struktur:“
'Rückgabe Typ' Funktionsname ('Argumentenliste') { 'Block von Anweisungen' }
Der Name der Funktion: Hier sollte der Programmierer einen Namen wählen, der die im Anweisungsblock definierten Anweisungen präzise und kurz in einem Wort zusammen fasst.
Rückgabe Typ: Welchen Datentyp gibt die Funktion an das Hauptprogramm zurück? Der „Rückgabe Typ“ steht vor dem Funktionsnamen.
Argumentenliste: Die „Argumentenliste“ steht direkt hinter dem Funktionsnamen, im Aufrufoperator „(…)“. Sie spezifiziert die Datentypen der Variablen, die die Funktion zur Berechnung ihrer Aufgabe benötigt.
Der ‚Rückgabe Typ‘kann hierbei einer der C++ Datentypen (z.B. int oder double) oder ein Daten-Array sein. Falls der ‚Rückgabe Typ‘ als void gekennzeichnet wird, gibt die Funktion keine Daten an das Hauptprogramm zurück, und führt nur den ‚Block von Anweisungen‘ aus. Die ‚Argumentenliste‘setzt sich aus einer Liste von Datentypen der formalen Argumente (Parameter) der Funktion zusammen, die jeweils mit einem Komma voneinander getrennt sind.
Soviel zur Theorie – werden wir praktisch:
Funktionen erfüllen also den Zweck, wiederkehrende Aufgaben auszulagern, anstatt jedes Mal den gleichen Code nochmal einzutippen, wenn eine bestimmte Aufgabe ansteht.
Programme werden Zeile für Zeile ausgeführt, in der Reihenfolge, in der du den Quellcode aufgesetzt hast. Bei einem Funktionsaufruf verzweigt das Programm, um die Funktion auszuführen. Ist die Funktion beendet, springt die Programmausführung zurück zu der Zeile in der aufrufenden Funktion, die auf den Funktionsaufruf folgt. Funktionen dürfen andere Funktionen – auch sich selbst (Rekursion) – aufrufen.
Der Funktion main() kommt dabei eine Sonderstellung zu. Normalerweise muss eine Funktion, um etwas leisten zu können, im Verlauf Ihres Programms durch main() oder eine andere Funktion aufgerufen werden. main() ist die Hauptfunktion eines jeden Programms und wird beim Start des Programms vom Betriebssystem aufgerufen. Sie steuert den eigentlichen Ablauf des Programms. Die Hauptfunktion darf sich nicht selbst aufrufen.
Im realen Leben könnte das so aussehen: Stellen wir uns vor, dass du ein Bild von dir selbst zeichnest. Du zeichnest deinen Kopf, die Augen, die Nase – und plötzlich bricht dein Bleistift ab. Du »verzweigst« nun in die Funktion »Bleistift spitzen«. Das heißt, du hörst mit dem Zeichnen auf, stehst auf, gehst zur Spitzmaschine, spitzt den Stift, kehrst an deine Arbeit zurück und setzt sie dort fort, wo du aufgehört hast. Wenn ein Programm eine bestimmte Arbeit verrichtet haben möchte, kann es dafür eine Funktion aufrufen und nach Abarbeitung der Funktion genau an dem Punkt weitermachen, wo es aufgehört hat. Das Listing „demofunction.cpp“ verdeutlicht dieses Konzept:
// Listing: demofunction.cpp
#include <iostream>
// Namensraum für cout:
using namespace std;
// Funktion DemoFunction()
// - gibt eine Meldung aus und gibt danach
// - die Kontrolle zurück an den Aufrufer:
void DemoFunction(void)
{
cout << "In DemoFunction\n";
}
// Funktion main - gibt eine Meldung aus, ruft
// dann DemoFunction auf, gibt danach
// eine zweite Meldung aus:
int main()
{
cout << "In main\n" ;
DemoFunction(); // Verzweigung zur Funktion 'DemoFunction()'
// Wieder in main() nach Abarbeitung der Funktion:
cout << "Zurueck in main\n";
return 0;
}
Hinweis: Bei der Funktion DemoFunction() handelt es sich um eine Funktion ohne Rückgabewert (Datentyp für die Rückgabe: void).
Ausgabe:
./demofunction
In main
In DemoFunction
Zurueck in main
Programmanalyse:
Zeile 2 inkludiert IOSTREAM. Das ist eine Header-Datei, die Teil der Standardbibliothek von C++ ist. Diese Datei enthält Definitionen für die Ein- und Ausgabestreams, wie cin, cout, cerr und clog. Diese Streams sind Instanzen von Klassen wie istream und ostream, die wiederum von der Basisklasse ios abgeleitet sind.
Zeile 5 legt den Namensraum std fest und erspart dir damit Tipparbeit (vgl. std::cout).
Zeile 9 ist der Funktionskopf der Funktion DemoFunction().
Die Zeilen 10 und 12 beginnen und beenden den Anweisungsblock (Funktionsrumpf) der Funktion.
Zeile 11 sorgt für die Ausgabe der Meldung ‚In DemoFunktion‘, wenn die Funktion aufgerufen wird. Danach wird die Funktion ohne Rückgabewert beendet und die Programmkontrolle wieder an den Aufrufer (in diesem Fall: Die main()-Funktion) zurück gegeben.
Die Hauptfunktion main() beginnt in Zeile 17 (Funktionskopf). Ihr Anweisungsblock beginnt in Zeile 18 und endet in Zeile 26.
Zeile 19 gibt die Meldung ‚In main‘ und einen Zeilenvorschub aus.
Zeile 20 ruft die Funktion ‚DemoFunktion()‘ ohne Parameter auf und übergibt ihr die Programmkontrolle, bis die Funktion abgearbeitet ist.
Zeile 23 gibt die Meldung ‚Zurueck in main‘, gefolgt von einem Zeilenvorschub aus. Zu diesem Zeitpunkt liegt die Programmkontrolle bereits wieder bei main().
Zeile 25 gibt den Integer-Wert ‚0‘ (Null) an den Aufrufer (Das ist das Betriebssystem, bzw. die Konsole, in welcher das Programm gestartet wurde) zurück. Das Programm ist damit beendet.
Funktionen sind Arbeitstiere. Um ihren Aufgaben gerecht zu werden, können sie Daten (z. B. das Ergebnis einer Berechnung) über ihren Rückgabetyp an den Aufrufer zurückgeben. Um flexible Ergebnisse liefern zu können, verfügt eine Funktion über eine Parameterliste, mit der ihr Werte übergeben werden können. Soll eine Funktion zum Beispiel zwei Zahlen addieren, stellen die Zahlen die Parameter für die Funktion dar. Ein typischer Funktionskopf sieht folgendermaßen aus:
int summe (int a, int b)
Das Listing „addition.cpp“ zeigt eine Funktion, die zwei ganzzahlige Parameter übernimmt und einen ganzzahligen Wert zurückgibt. Kümmere dich momentan nicht um die Syntax oder die Einzelheiten, wie man mit Integer-Werten (beispielsweise int x) arbeitet. Wir kommen darauf zurück.
// Listing: addition.cpp
#include <iostream>
using namespace std;
// Funktion: int Add(int x, int y)
// - addiert zwei an die Funktion übergebene
// - Zahlen und gibt das Ergebnis an den
// - Aufrufer zurück:
int Add(int x, int y)
{
cout << "\nFunktion 'Add()', erhalten: " << x << ", " << y << "\n";
return(x + y); // Addieren und zurückgeben
}
// --- Haupt-Funktion --
int main(void)
{
int summand1, summand2, summe;
cout << "Zahlen addieren:\n";
cout << "================\n";
cout << "Ich bin in 'main()'!\n";
cout << "Gib die erste Zahl ein: ";
cin >> summand1;
cout << "Gib die zweite Zahl ein: ";
cin >> summand2;
// Berechnung:
// Der Variablen 'summe' wird der Rückgabewert
// der Funktion 'Add()' zugewiesen:
summe = Add(summand1, summand2); // Funktionsaufruf und Wertzuweisung
// Ausgabe des Ergebnisses:
cout << "\nZurueck in 'main()'!\n";
cout << summand1 << " + " << summand2 << " = " << summe << endl;
cout << "\nProgramm beendet.\n";
return 0;
}
Hinweis: Bei der Funktion ‚int Add(int x, int y)‘ handelt es sich um eine Funktion mit Rückgabewert und Parameterliste.
Compiliere das Programm (<F5> in Geany oder Konsole):
g++ -Wall -s addition.cpp -o addition
Starte es anschließend und gib nach Aufforderung zwei Ganzzahlen ein (z. B. 4700 und 11).
Ausgabe:
./addition
Zahlen addieren:
================
Ich bin in 'main()'!
Gib die erste Zahl ein: 4700
Gib die zweite Zahl ein: 11
Funktion 'Add()', erhalten: 4700, 11
Zurueck in 'main()'!
4700 + 11 = 4711
Programm beendet.
Programmanalyse:
Die Zeilen 10 bis 14 definieren die Funktion ‚int Add(int x, int y)‘. Beachte: Dies geschieht vor der Definition von ‚main()‘!
Zeile 12 sorgt dafür, dass beim Aufruf von Add() eine Meldung ausgegeben wird, die auch die beiden übergebenen Parameter ausgibt.
Die Hauptfunktion main() erstreckt sich über die Zeilen 17 bis 40.
In Zeile 19 deklarieren wir drei Variablen vom Typ Ganzzahl (int). Sie dienen später der Aufnahme der beiden Summanden und der berechneten Summe.
Zeile 24 fordert zur Eingabe einer Zahl auf.
Zeile 25 liest diese Zahl mit dem Eingabestream cin ein. Dieses Schlüsselwort steht für „Console Input“ und wird vom Eingabeoperator „>>“ (dem Gegenstück zum Ausgabeoperator „<<„) gefolgt. Er verweist auf die Variable summand1, die die eingegebenen Daten (die erste Ganzzahl) aufnehmen soll.
In den Zeilen 26 und 27 wiederholt sich dieser Vorgang der Dateneingabe für die Variable summand2.
Zeile 32 – Hier wird die eigentliche Arbeit geleistet: Der Variablen summe wird das Ergebnis der Berechnung, also der Rückgabewert der Funktion Add() zugewiesen. Hierzu ruft die Zuweisung die Funktion Add() mit den Parametern (summand1, summand2) auf. Überraschung! Hatten wir nicht die Parameterlist von Add() als (int x, int y) definert? Doch, hatten wir – und das ist auch korrekt. Die Parameter x und y sind lokale, nur innerhalb der Funktion selbst sichtbare Variablen. Der Aufruf von außerhalb kann mit jeder passenden Variablen, gleich welchen Namens, erfolgen. Intern arbeitet die Funktion allerdings mit den Variablen x, y. Das kann man gut an den Zeilen 12 und 13 erkennen.
Zeile 36 gibt beide eingegebenen Zahlen und ihre Summe in der Form aus, wie du die Berechnung auch auf ein Blatt Papier schreiben würdest: 4700 + 11 = 4711
Zeile 39 beendet das Hauptprogramm mit dem Rückgabewert Null.
Funktionsprototypen
Bisher haben wir eigene Funktionen immer vor der Hauptfunktion main() definiert. Das ist dem Umstand geschuldet, dass in C/C++ eine Variable oder Funktion immer vor ihrer ersten Verwendung deklariert sein muss. Es geht aber auch anders, wenn man dem Compiler eine Funktion durch ihren Funktionsprototypen rechtzeitig – also vor main() – bekannt macht. Dabei entspricht der Funktionsprototyp genau dem Funktionskopf, gefolgt von einem Semikolon:
// Listing: addition2.cpp
#include <iostream>
using namespace std;
// Funktionsprototyp:
int Add(int x, int y);
// -- Hauptprogramm --
int main(void)
{
int summand1, summand2;
cout << "Zahlen addieren:\n";
cout << "================\n";
cout << "Ich bin in 'main()'!\n";
cout << "Gib die erste Zahl ein: ";
cin >> summand1;
cout << "Gib die zweite Zahl ein: ";
cin >> summand2;
// Berechnung und Ausgabe:
cout << summand1 << " + " << summand2 << " = " << Add(summand1, summand2) << endl;
return 0;
}
// -- Funktion: int Add(int x, int y) --
int Add(int x, int y)
{
return(x + y); // Addieren und zurückgeben
}
Beachte: Im Listing „addition2.cpp“ erfolgt die Definition der Funktion „Add()“ erst nach der Hauptfunktion main(). Um die Funktion trotzdem bereitstellen zu können, wurde in Zeile 7 der Funktionsprototyp entsprechend dem Funktionskopf von Add() deklariert. Auch erfolgt die Berechnung nicht mehr mit Zwischenspeicherung des Ergebnisses in der Variablen summe – diese wurde eingespart. Stattdessen übergeben wir in Zeile 23 den Rückgabewert von Add() direkt an den Ausgabestream.
Die Verwendung von Prototypen hat übrigens den Vorteil, dass der Compiler die korrekte Verwendung einer Funktion bereits frühzeitig überprüfen und auf etwaige Regelverstöße genauer reagieren kann. Gute C/C++-Programme beinhalten deshalb einen Abschnitt, in dem alle Prototypen für Funktionen vor ihrer ersten Verwendung deklariert werden.
Zusammenfassung:
Funktionen geben entweder einen Wert oder void (das heißt: nichts) zurück. Eine Funktion zur Addition von zwei ganzen Zahlen liefert sicherlich die Summe zurück, und man definiert diesen Rückgabewert vom Typ Integer (int). Eine Funktion, die lediglich eine Meldung ausgibt, hat nichts zurückzugeben und wird daher als void (zu deutsch: leer) deklariert.
Funktionen gliedern sich in Kopf und Rumpf. Der Kopf besteht wiederum aus dem Rückgabetyp, dem Funktionsnamen und den Parametern. Mit Parametern lassen sich Werte an eine Funktion übergeben.
Funktionen müssen (wie Variablen) vor ihrer ersten Verwendung dem Compiler bekannt gemacht werden. Aus diesem Grund definiert man sie entweder vor main(), oder aber man verwendet stattdessen einen entsprechenden Funktionsprototypen an ihrer Stelle.
Texte an der Konsole ausgeben mit dem Ausgabeobjekt cout
Vorläufig werden wir cout einfach verwenden, ohne seine Funktionsweise vollständig zu verstehen. Um einen Wert auf dem Bildschirm auszugeben, musst du cout eingeben, gefolgt von dem Umleitungsoperator (<<), den man durch zweimaliges Betätigen der [<]-Taste erzeugt. Auch wenn es sich hier um zwei Zeichen handelt, werden sie von C++ als ein Symbol interpretiert. Im Anschluss an den Umleitungsoperator gibst du deine auszugebenden Daten ein. Das folgende Listing soll dir die Anwendung demonstrieren. Gib das folgende Beispiel wortgetreu ein, nur das du anstatt des Namens Micha B. deine eigenen Namen eintippst (es sei denn, du bist ich).
// Listing 'coutdemo.cpp' zeigt die Verwendung von cout
#include <iostream>
using namespace std;
int main()
{
cout << "Hallo dort.\n";
cout << "Hier ist 5: " << 5 << "\n";
cout << "Der Manipulator endl beginnt eine neue Zeile.";
cout <<
endl;
cout << "Hier ist eine große Zahl:\t" << 70000 << endl;
cout << "Hier ist die Summe von 8 und 5:\t" << 8+5 << endl;
cout << "Hier ist ein Bruch:\t\t" << (float) 5/8 << endl;
cout << "Und eine riesengroße Zahl:\t";
cout << (double) 7000 * 7000 <<
endl;
cout << "Vergiss nicht, Micha B. durch deinen Namen"
" zu ersetzen...\n";
cout << "Micha B. ist ein C++-Programmierer!\n";
return 0;
}
Ausgabe:
./coutdemo
Hallo dort.
Hier ist 5: 5
Der Manipulator endl beginnt eine neue Zeile.
Hier ist eine große Zahl: 70000
Hier ist die Summe von 8 und 5: 13
Hier ist ein Bruch: 0.625
Und eine riesengroße Zahl: 4.9e+07
Vergiss nicht, Micha B. durch deinen Namen zu ersetzen...
Micha B. ist ein C++-Programmierer!
Erklärungen:
Zeile 1 enthält einen einzeiligen Kommentar, welcher den Zweck des Programms erklärt.
Zeile 2 bindet mit der Anweisung #include<iostream> die Datei IOSTREAM in die Quellcode-Datei ein. Dies ist erforderlich, um cout und die verwandten Funktionen verwenden zu können.
Zeile 4 legt den Namensraumstd fest, in welchem cout definiert ist. Würdest du diese Anweisung weglassen, dann müsstest du im Quelltext die Anweisung um std::cout erweitern, anstatt einfach nur cout zu schreiben.
Zeile 8 gibt einen Text und einen Zeilenvorschub mittels \n (newline) aus.
In Zeile 9 werden cout drei Werte übergeben, wobei die Werte voneinander durch einen Umleitungsoperator getrennt werden. Der erste Wert ist der String „Hier ist 5:“ Beachte das Leerzeichen nach dem Doppelpunkt. Dieser Raum ist Teil des Strings. Anschließend wird dem Umleitungsoperator der Wert 5 und das Zeichen für „Neue Zeile“ ( „\n“ für newline, immer in doppelten oder einfachen Anführungszeichen) übergeben. Damit wird insgesamt folgende Zeile
Hier ist 5: 5
auf dem Bildschirm ausgegeben. Da hinter dem ersten String kein „Neue Zeile“-Zeichen kommt, wird der nächste Wert direkt dahinter ausgegeben. Dies nennt man auch »die zwei Werte verketten« (Konkatenation).
Zeile 10 gibt eine Meldung aus und in Zeile 11 wird dann dann der Manipulator endlverwendet. Sinn und Zweck von endl ist es, eine neue Zeile auf dem Bildschirm auszugeben. Damit bewirkt dieser Manipulator dasselbe, als wenn du \n(newline) verwendet hättest – endl steht für end line (Ende der Zeile).
Zeile 14 führt ein neues Formatierungszeichen, das \t, ein. Damit wird ein Tabulatorschritt eingefügt, mit dem die Ausgaben der Zeilen 14 bis 17 bündig ausgerichtet werden. Zeile 14 zeigt auch, dass nicht nur der DatentypInteger, sondern auch Integer vom Typ long ausgegeben werden können. Zeile 15 zeigt, dass cout auch einfache Additionen verarbeiten kann. Der Wert 8+5 wird an cout weitergeleitet und dann als 13 ausgegeben.
In Zeile 16 wird cout der Wert 5/8 übergeben. Mit dem Begriff (float) teilen Sie cout mit, dass das Ergebnis als Dezimalzahl ausgewertet und ausgegeben werden soll. In Zeile 18 übernimmt cout den Wert 7000 * 7000. Der Begriff (double) teilt cout mit, dass du diese Ausgabe in wissenschaftlicher Notation wünschst. Diese Themen werden wir noch im Detail im Kapitel »Variablen und Konstanten« im Zusammenhang mit den Datentypen besprechen.
In Zeile 23 hast du meinen mit deinem Namen ersetzt und der Computer nennt dich einen C++-Programmierer. 😉
Anmerkung:Über den Sinn und Zweck von Kommentaren haben wir ja bereits gesprochen. Kommentare, die beschreiben, was eh schon jeder sieht, sind nicht besonders sinnvoll. Sie können sogar kontraproduktiv sein, wenn sich der Code ändert und der Programmierer vergißt, den Kommentar mit zu ändern. Aber was für den einen offensichtlich ist, ist für andere undurchsichtig. Deshalb ist sorgfältiges Abwägen gefragt.
Zu guter Letzt möchte ich noch anmerken, dass Kommentare nicht mitteilen sollten, was du machst, sondern warum du es machst.
Programme sind besser lesbar und auch nach langer Zeit noch nachvollziehbar, wenn du den Quellcode sinnvoll kommentierst. Außerdem kann es bei der Entwicklung umfangreicherer Programme manchmal sinnvoll bei der Fehlersuche sein, bestimmte Bereiche vorübergehend auszukommentieren. Das funktioniert, weil der Compiler Kommentare bei der Übersetzung eines Programms schlichtweg überliest und deren Inhalt ignoriert. C++ kennt zwei Arten von Kommentaren, die beide ihren Anwendungsbereich und ihre Vorteile haben.
’Oldstyle’ C Kommentare
Es gibt sie schon seit den Zeiten der Programmiersprache C. Man bezeichnet sie auch als mehrzeilige Kommentare, weil alles, was zwischen der einleitenden Zeichenkombination /* und der abschließenden Zeichenkombination */ steht, vom Compiler ignoriert wird. Trotzdem können sie auch als einzeilige Kommentare verwendet werden, wenn beide Kombinationen auf der gleichen Zeile zur Anwendung kommen. Beispiele:
/*
Ich bin ein mehrzeiliger Kommentar im 'alten' Stil von ANSI C.
Der Compiler ignoriert mein Geschwätz.
*/
/* Ich bin ein mehrzeiliger Kommentar, der wie ein Einzeiliger tut. */
’Newstyle’ C++ Kommentare
Sie sind einzeilig und typisch für C++. Alles, was innerhalb der selben Zeile hinter der Zeichenkombination // steht, wird vom Compiler ignoriert. Typische Anwendungsfälle sind Beschreibungen hinter einer Anweisung oder das Auskommentieren einzelner Zeilen. Beispiele:
// ich bin ein einzeiliger Kommentar! Der Compiler ignoriert mein Geschwätz.
int i = 42 // Deklaration einer Integer-Variablen
// 'z' wird auskommentiert:
// int z = 4711;
Das Verschachteln von Kommentaren ist in C und C++ verboten:
/*
Alle meine Entchen!
/* Verboten: Ein verschachtelter Kommentar innerhalb eines Kommentars */
*/
Anmerkung:Kommentare, die beschreiben, was eh schon jeder sieht, sind nicht besonders sinnvoll. Sie können sogar kontraproduktiv sein, wenn sich der Code ändert und der Programmierer vergisst, den Kommentar mit zu ändern. Aber was für den einen offensichtlich ist, ist für andere undurchsichtig. Deshalb ist sorgfältiges Abwägen gefragt.
Zu guter Letzt möchte ich noch anmerken, dass Kommentare nicht mitteilen sollten, was du machst, sondern warum du es machst.
Quelltext-Regeln und Formatierung
C++ ist eine formatfreie Sprache. Das bedeutet, dass du bei der optischen Gestaltung deiner Quelltexte mit wenigen Einschränkungen völlig frei bist. Nehmen wir einmal an, du hättest unser erstes Programm auf diese Art notiert:
#include
<iostream>
int
main() {
std::cout << "Hallo vom langweiligsten Programm der Welt!"
<< std::endl;
return 0;
}
Unübersichtlich, nicht wahr? Trotzdem ist der Quellcode aus Compilersicht legal und wird auch anstandslos übersetzt. Ob diese Art der Formatierung für das menschliche Auge angenehm und leicht lesbar ist, bedarf vermutlich keiner Diskussion.
Es existieren viele unterschiedliche Stile und Empfehlungen bezüglich der Quellcodeformatierung. Grundsätzlich sollte der Quellcode auf jeden Fall leicht lesbar und auf eine nachvollziehbare Strukturierung aufgebaut sein. Wenn du erst einmal einen persönlichen Stil entwickelt hast, der diesen Anforderungen gerecht wird – behalte ihn möglichst konsequent bei. Es wird dir auch nach Jahren noch das Verständnis für deinen Code enorm erleichtern. Hier einige Tipps:
Rücke zusammengehörige Anweisungsblöcke und Bedingungsabfragen immer ihrer Logik nach ein. Schreibe dazu zusammengehörige Klammern stets untereinander und verwende Tabulatorsprünge, um deren Inhalte sichtbar als zugehörig zu kennzeichnen.
Verwende Leerzeilen zwischen Funktionen und thematisch abgeschlossenen Bereichen.
Achte auf die Zeilenlänge deiner Anweisungen – was auf einem hochauflösenden Monitor noch gut aussieht, kann auf dem Drucker ein Desaster sein. Beschränke dich auf bummelig 90 Zeichen pro Zeile und brich längere Zeilen unter Berücksichtigung syntaktischer Regeln um (siehe ‚Zeilenfortsetzung‘ im Abschnitt über den Syntax von C++).
Verwende, wo immer nötig, Kommentare, um dein Programm nachvollziehbar zu halten. Ein Beispiel:
/*
* Das Einsteigerseminar C++
*
* hallo2.cpp
* Das wohl langweiligste Programm der Welt, Version 2
*/
#include <iostream> // Ein-/Ausgabebibliothek einbinden
using namespace std; // Namensraum 'std' (Standard) einbinden
int main()
{
// Lange Zeichenkette aufteilen mit '\':
cout << "Das langweiligste Programm der Welt " \
"meldet sich zurueck mit einer sehr, " \
"sehr langen Zeichenkette!\n"
<< endl;
// Zwei Zeichenketten nacheinander über zwei Zeilen ausgeben:
cout << "Was ich noch sagen wollte:"
<< endl
<< "C++ macht Laune."
<< endl;
return 10; // Gib den Wert 10 zurück an den Aufrufer
}
Syntax-Regeln für die Sprache C/C++
Die Programmiersprache C/C++ beinhaltet gleich mehrere Sprachen/ Syntaxen:
C-Syntax
Präprozessor-Syntax
Printf/Scanf Formatstring Syntax
Terminal Emulation
Compiler/Linker Anweisungen
In diesem Kapitel sollen zunächst nur allgemeine Eigenschaften der Sprache, der grundlegende Syntax und die Kontrollstrukturen erklärt werden. Bei vielen Erklärungen sind Code-Beispiele vorhanden. Vieles, was du hier zu lesen bekommst, wirst du erst im weiteren Verlauf des Tutorials vollständig verstehen – trotzdem ist es wichtig, von den einzelnen Begrifflichkeiten mal gehört zu haben.
Da man aus Fehlern am meisten lernt, sind zum Teil auch negative Beispiele enthalten. Für ein besseres Verständnis empfiehlt es sich, Code-Beispiele selbst nachzuvollziehen.
Zeichensatz:
Der Syntax von C nutzt die unteren 128 Zeichen des ASCII Zeichensatzes. Da UTF-8 in den ersten 128 Zeichen deckungsgleich zu ASCII ist, kann auch dieser zur Erstellung des Source Codes genutzt werden. Zeichen außerhalb dieses gültigen Zeichensatzes können folglich nur in Strings oder Kommentaren vorkommen. Tatsächlich empfiehlt sich die Verwendung eines auf UTF-8 kodierten Zeichensatzes, da dieser bei der Übernahme eines Quelltexts zwischen verschiedenen Betriebssystemen kaum Probleme aufwirft.
Namenskonventionen
Folgende Regeln gelten bzgl. der Benennung von Variablen und Funktionen:
Variablen und Funktionsnamen können aus Buchstaben, Zahlen und dem Unterstrich bestehen. Sie müssen mit einem Buchstaben oder einem Unterstrich beginnen
C/C++ ist Case sensitiv, d.h. es wird zwischen Groß- und Kleinbuchstaben unterschieden
Schlüsselwörter können nicht für Variablen/Funktionsnamen/Datentypen genutzt werden Namenskonventionen von Library-Funktionen:
In C (und C++) sind Schlüsselwörter und Standardlibrary-Funktionen zumeist in Kleinbuchstaben geschrieben. In der C-Standardlibrary werden oftmals verkürzte Ausdrücke wie z.B. isalnum() (zum Testen ob ein Zeichen ein Buchstaben oder ein Digit ist) und in C++ der Unterstrich als Worttrenner (z.b. out_of_range) genutzt.
Makros werden per Konvention in GROSSBUCHSTABEN und ggf. mit Unterstrich als Worttrenner geschrieben.
Namen beginnend mit doppelten Unterstrich oder beginnend mit einem Unterstrich gefolgt von einem Großbuchstaben (z.B. __LINE__ _Reserved) sind für den Compiler und der Standard-C-Library vorbehalten und sollten im eigenen Programm nicht benutzt werden.
Zeilenfortsetzung
Mit dem Backslash Operator (gefolgt von einem Zeilenende) kann eine Zeile in der nächsten Zeile fortgesetzt werden. Der Compiler löscht das \-Zeichen mit anschließendem Zeilenende und ersetzt dies durch nichts. Dies ist insbesondere bei Anweisungen notwendig, die am Ende der Zeile abgeschlossen sein müssen (z.B. Strings, Makros).
Beispiele:
char str1[]="Strings müssen am Ende per Anführungszeichen abgeschlossen sein
so dass dies ein Fehler ist";
char str2[]="Dies\
ist ein Test"; //Vorsicht, führende Leerzeichen vor 'ist'
//bleiben erhalten!
/*mehrzeiliges Makro*/
#define MAX(a,b) (a>b?\
b: \
a)
//Dies ist ein Zeilenkommentar \
welcher in dieser Zeile fortgesetzt wird
/\
* dies ist ein Blockkommentar*\
/
//hinter der Zeilenfortsetzung darf nur ein CR/LF folgen
#define MAX2(a,b) \ //so dass hier kein Kommentar folgen darf
a>b?a:b
Hinweis:
Innerhalb von Char-Literatoren und Strings wird ‚\‘ als Escape-Operator genutzt, welche das ‚\‘ und ein oder mehrere folgende Zeichen ersetzt. Daher darf hinter Backslash als Zeilenfortsetzungszeichen kein weiteres Zeichen folgen.
Gültigkeit und Sichtbarkeit von Variablen
Vorrangig in der objektorientierten Programmierung werden mit Namensräumen Objekte und deren Methoden/Attribute in einer Art Baumstrukturstrukturiert. Dies ermöglicht eine eindeutige Ansprache von Variablen/Objekte, aber auch eine doppelte Verwendung von Methoden-/Attributnamen in unterschiedlichen Namensräumen. Ergänzend zu den Namensräumen kann mit public/private/proteced eine Zugriffsbeschränkung von Methoden/Attributen definiert werden.
Die Programmiersprache Cunterstützt keinen Namensraum. Zugriffsmodifikatoren werden indirekt über Header-Dateien getätigt. Hinsichtlich der Gültigkeit/Sichtbarkeit unterscheidet C folgende Bereiche:
Funktionen (Function Scope)
Datei (File Scope)
Block (Block Scope)
Funktionsparameter in Prototypen (Function Prototype Scope)
Innerhalb eines Gültigkeitsbereiches dürfen Variablen-/Funktions-/Datentypnamen nicht doppelt genutzt werden. In der Programmiersprache C++ sind weitere Scope wie z.B. Class Scope, Enumationation Scope und ergänzend das Konzept von Namensräumen vorhanden (Beschreibung folgt).
Funktionsweite Sichtbarkeit
Eine Label-Definition (als Sprungmarke für die goto-Anweisungen) erfolgt immer mit funktionsweiter Sichtbarkeit/Gültigkeit.
Dateiweite Sichtbarkeit
Erfolgt eine Funktion-/Variablen-/Datentyp-Definition außerhalb eines Block-Scopes oder von Funktionsparameter, so sind diese innerhalb der gesamten Datei und bei Funktionen/Variablen ergänzend Projektweit (für alle Objektdateien) sichtbar/gültig (= global). Alle globalen Funktionen/Variablen können von allen Dateien aus genutzt/zugegriffen werden (sofern sie zuvor deklariert wurden). Beispiel 1:
// Deklaration von func(),
// welche in Datei2.c definiert wird
extern void func(void);
// Definition der Variablen global
int global=0;
int main(void)
{
func();
global++;
return 0;
}
Beispiel 2:
// Deklaration von global,
// welche in Datei1.c definiert wird
extern int global;
// Definition der Funktion func()
void func(void)
{
global++;
}
Wird das Schlüsselwort ’static‘ der Variablen/Funktionsdefinition vorangestellt, so wird die Sichtbarkeit/Gültigkeit auf Dateiweit eingeschränkt. Variablen/Funktionen können nur innerhalb der (Objekt-) Datei genutzt werden und sind für anderen (Objekt-)Dateien unsichtbar.
Beispiel 1:
// Dateiweite Sichtbarkeit von var1
static int var1;
int main(void)
{
var1++;
return 0;
}
Beispiel 2:
// Dateiweite Sichtbarkeit von var1
static int var1;
void func(void)
{
static int var2; // Vorsicht, static
// hat hier eine andere Bedeutung
var1++;
}
Projektweite Gültigkeit
bedeutet insbesondere, dass keine doppelten Benennung von Variablen/Funktionen/Datentypen innerhalb des gesamten Projektes erlaubt sind, d.h. dass alle Variablen/Funktionennamen über alle Dateien/Librarys eindeutig sein müssen. Beispiel 1:
Datei 1:
// datei_1.cpp
#include <stdio.h>
int a=1;
int main(int argc,char *argv[])
{
printf("Hello World %d",a);
void dummy(void); //Deklaration von Dummy
dummy();
return 0;
}
Datei 2:
// datei_2.cpp
#include <stdio.h>
int a=7;
void dummy(void)
{
printf("Hello Again %d\n",a);
}
Versuche, die beiden Dateien zusammen zu compilieren:
g++ -Wall datei_1.cpp datei_2.cpp -o beipiel1
Der Linker meldet beim Zusammenfügen der Objekt-Dateien, dass die Variable a bereits woanders definiert sei (multiple definition of ‚a‘):
gcc -Wall datei1.cpp -o beispiel1
datei_1.cpp:4:5: Fehler: Redefinition von »int a«
4 | int a=1;
| ^
In Datei, eingebunden von datei_1.cpp:3:
datei_2.cpp:3:5: Anmerkung: »int a« wurde bereits hier definiert
3 | int a=7;
| ^
Hier meldet der Compiler eine Fehlermeldung, da das Symbol printf zum einen als Variable und zum anderen als Funktion (innerhalb der inkludierten Datei stdio.h beschrieben) genutzt wird (‚printf‘ redeclared as different kind of symbol):
g++ -Wall beispiel2.cpp
beispiel2.cpp:2:5: Fehler: »int printf« als andere Symbolart redeklariert
2 | int printf=7;
| ^~~~~~
In Datei, eingebunden von beispiel2.cpp:1:
/usr/include/stdio.h:356:12: Anmerkung: vorherige Deklaration von »int printf(const char*, ...)«
356 | extern int printf (const char *__restrict __format, ...);
| ^~~~~~
beispiel2.cpp: In Funktion »int main()«:
beispiel2.cpp:5:12: Warnung: Format »%d« erwartet Argumenttyp »int«, aber Argument 2 hat Typ »int (*)(const char*, ...)« [-Wformat=]
5 | printf("%d\n", printf);
| ~^ ~~~~~~
| | |
| int int (*)(const char*, ...)
Blockweite Sichtbarkeit
Erfolgt eine Funktion-/Variablen-/Datentyp Definition innerhalb einer Funktion, als Funktionsparameter oder eines Blockes, so sind diese nur innerhalb des Blockes sichtbar/gültig (= Lokale Variable). Blöcke können verschachtelt sein, so dass das bei identischer Namensgebung innere Definitionen Vorrang haben. Ebenso haben Blockdefinitionen Vorrang vor Datei-/Projektdefinitionen (überdecken diese):
blockweit1.cpp
int main(void)
{
int var2; //var2 ist nur innerhalb
//von main() sichtbar
struct xyz //Datentyp ist nur innerhalb
{int x,y,z;};//von main()
//sichtbar/gültig
extern void func(void); //Deklaration
//ist nur innerhalb von
//main() sichtbar/gültig
func();
}
void foo(void) {
struct xyz var_xyz; //Fehler, da
//Datentypedefinition hier nicht mehr
//gültig ist!
func(); //Fehler, da Deklaration
//hier nicht mehr gültig ist
}
blockweit2.cpp
#include <stdio.h>
void func(void)
{
int var2=1; //var2 ist
//nur innerhalb von
//func() sichtbar
{
int var2=2;
//var2 ist nur innerhalb
//dieses Blockes sichtbar
printf("%d\n",var2);
}
printf("%d\n",var2);
}
Auch dieses aus zwei Dateien bestehende Programm wird vom Compiler angemeckert:
g++ -Wall blockweit1.cpp blockweit2.cpp -o blockweit
blockweit1.cpp: In Funktion »int main()«:
blockweit1.cpp:2:7: Warnung: Variable »var2« wird nicht verwendet [-Wunused-variable]
2 | int var2; //var2 ist nur innerhalb
| ^~~~
blockweit1.cpp: In Funktion »void foo()«:
blockweit1.cpp:14:13: Fehler: Aggregat »foo()::xyz var_xyz« hat unvollständigen Typ und kann nicht definiert werden
14 | struct xyz var_xyz; //Fehler, da
| ^~~~~~~
blockweit1.cpp:18:2: Fehler: »func« wurde in diesem Gültigkeitsbereich nicht definiert
18 | func(); //Fehler, da Deklaration
| ^~~~
Soviel zunächst zu Syntaxregeln in C/C++. Gräme dich nicht, wenn du gerade an ‚Bahnhof‘ und ‚Züge‘ denkst – wir werden im weiteren Verlauf des Tutorials an den jeweiligen Stellen wiederholend am praktischen Beispiel auf die Syntaxregeln eingehen.
In diesem Teil des C++ Tutorials beschäftigen wir uns mit Techniken zur Speicherung unserer Quelltexte und dem Schreiben und Übersetzen derselben. Bevor es richtig losgeht, noch einige Vorüberlegungen zur Organisation der anfallenden Daten:
Betrachte ab sofort jedes Programm, das du eingibst, als eigenständiges Projekt. Lege dir zu diesem Zweck zunächst ein Sammelverzeichnis an, in welchem du dann wiederum Ordner für die eigentlichen Projekte erstellst. Beispiel:D:\Einsteigerseminar
Jedes Projekt erhält seinen eigenen Ordner, in dem alle relevanten Daten zum Projekt gespeichert werden. Dies ist das Arbeitsverzeichnis des jeweiligen Projekts! Beispiel 1:D:\Einsteigerseminar\001_Hallo Beispiel 2:D:\Einsteigerseminar\002_Kommentare
Relevante Daten können z. B. sein:
C++-Quelltext für das eigentliche Programm (Suffix: .cpp)
C++-Quelltexte, die vom Hauptprogramm eingebunden werden, um weitere Fähigkeiten zum Programm hinzu zu fügen – sog. „Header-Dateien“ (Suffix: .hpp)
Makefiles (Dateien mit Compileranweisungen zur Übersetzung des Programms, kein Suffix)
Vom Programm zu speichernde oder zu lesende Datendateien (Suffix: frei wählbar)
Grafiken (Suffix: .png | .jpg | .ico, usw …)
Textdatei mit einer Beschreibung des Programmes und/oder einer TODO-Liste (Suffix: .txt)
Für unser erstes Projekt könnte das dann z. B. so aussehen:
Erstelle zunächst in deinem Sammelverzeichnis für Projekte den Unterordner 001_Hallo. Starte dann den Editor (Geany) und speichere das (noch leere) Programm unter dem Namen hallo.cpp in diesem Ordner ab. Gib danach den folgenden Quellcode (ohne Zeilennummern!) ein und achte darauf, alles so abzutippen, wie ich es hier notiert habe:
/*
C++ Einsteiger Tutorial
Source: hallo.cpp
*/
#include <iostream>
int main()
{
std::cout << "Hallo vom langweiligsten Programm der Welt!" << std::endl;
return 0;
}
Drücke nun die Funktionstaste , um das Programm zu compilieren. Wenn du alles richtig gemacht hast, meldet der Compiler:
Falls jedoch Fehlermeldungen aufgetreten sein sollten, dann klicke auf die jeweilige Fehlermeldung im Meldungsfenster und vergleiche deine Eingaben mit dem Programmlisting. Korrigiere die aufgetretenen Fehler und compiliere das Programm erneut. Alte Hasen unter den Programmierern nennen diesen Vorgang eine Strafschleife…
Wenn keine Fehlermeldungen mehr auftauchen, dann ist es an der Zeit, unser erstes Programm zu testen. Drücke hierfür die Funktionstaste <F5> – es öffnet sich eine Kommandozeile:
Erwartungsgemäß gibt das Programm den Text „Hallo vom langweiligsten Programm der Welt!“ aus – und eine weitere, wichtige Information:
(program exited with code: 0)
Diese Meldung repräsentiert den Rückgabewert (später mehr dazu!) der Anwendung. Ein Kommandozeilen-Programm kann zu Diagnosezwecken beim Beenden unterschiedliche Codes an die Kommandozeile zurückgeben! Schließe nun die Kommandozeile durch Drücken einer beliebigen Taste.
hallo.cpp – Programmanalyse
Der Quellcode hallo.cpp mutet dir sicher noch sehr kryptisch an. Nehmen wir unser erstes Programm einmal genauer unter die Lupe und betrachten seine Bestandteile:
Zeile 1: Die Sequenz „/*“ leitet einen mehrzeiligen Kommentar ein, der in Zeile 4 mit der Sequenz „*/“ abgeschlossen wird. Alle, was zwischen diesen beiden Sequenzen steht, gilt als Kommentar und wird vom Compiler komplett ignoriert.
die Zeilen 2 und 3 enthalten einen kurzen, erklärenden Kommentar zum Programm.
Zeile 5 ist eine Leerzeile. Sie dient der Strukturierung des Programmcodes und damit der besseren Lesbarkeit.
Zeile 6: Die Angabe von #include veranlasst den Präprozessor (Bestandteil des Compilers) zum Einbinden einer Datei in den jeweiligen Quellcode. Die Präprozessor-Anweisung #include <iostream> bindet eine sogenannte System-Header-Datei – in diesem Fall enthält sie Definitionen und Funktionen der Ein-/Ausgabebibliothek für C++ und stellt u. a. den Ausgabestream cout für die Ausgabe von Texten in der Kommandozeile zur Verfügung. Im Gegensatz zu C besitzen System-Header-Dateien keinen Suffix. Sie werden innerhalb eines Paares spitzer Klammern aufgerufen, der Präprozessor sucht sie automatisch im Include-Verzeichnis des Compilers. Möchte man hingegen eigene Header-Dateien (Suffix per Konvention: .hpp oder .h) einbinden, so setzt man diese innerhalb eines Paares von Hochkommata (aka Anführungszeichen). Wird kein expliziter Pfad angegeben, so sucht der Präprozessor im aktuellen Verzeichnis:
Beispiel 1: #include "hallo_functions.hpp"
Beispiel 2: #include "D:\Projekte\my_functions.hpp"
Zeile 7: Funktionskopf der „Hauptfunktion“ main(), der Steuerung des Programmablaufs eines jeden C++-Programms. Funktionen bestehen grundsätzlich aus einem Funktionskopf mit Angabe der Datentypen für Rückgabewert und Funktionsparametern, dem Funktionsnamen, den in runde Klammern gefassten Funktionsparametern und einem zwischen geschweiften Klammern eingebetteten Funktionsrumpf, innerhalb dessen sich die Funktionslogik befindet – die genaue Verwendung des Begriffs „Datentyp“ klären wir noch. Die Funktion main() ist einzigartig im gesamten Quellcode und darf sich, im Gegensatz zu anderen Funktionen, nicht selbst aufrufen.
Zeile 8: Öffnende geschweifte Klammer des Funktionsrumpfes
Zeile 9: Anweisungen zur Ausgabe eines Textes und dessen Formatierung. Die Bestandteile im Einzelnen: std::cout << "Hallo vom langweiligsten Programm der Welt!" << std::endl; Der Ausgabestream cout ist Bestandteil des Namensraums (aka namespace) std. Da wir es eingangs versäumt haben, dem Compiler diesen Namensraum mittels der Anweisung using namespace std; bekannt zu machen, müssen wir den Ausgabestream explizit unter Angabe seines Namensraums mit dem Präfix std:: aufrufen: std::cout. Das Präfix std::cout teilt dem Compiler mit, dass das Ausgabestream-Objekt cout im Namensraum der Standardbibliothek liegt.
Exkurs: Namensräume bieten eine Methode zur Vermeidung von Namenskonflikten in großen Projekten. Symbole, die innerhalb eines Namespace-Blocks deklariert sind, werden in einen benannten Bereich platziert, der verhindert, dass sie mit gleichnamigen Symbolen in anderen Bereichen verwechselt werden. Es sind mehrere Namespace-Blöcke mit demselben Namen zulässig. Alle Deklarationen innerhalb dieser Blöcke werden in dem benannten Bereich deklariert. Wir gehen später ausführlicher auf dieses Thema ein.
Der Verkettungsoperator << überträgt die auf ihn folgende Zeichenkette an das Ausgabestream-Objekt cout.
Die Zeichenkette (aka String) „Hallo vom langweiligsten Programm der Welt!“ folgt dem Verkettungsoperator und soll ausgegeben werden. Zeichenketten werden zwischen Hochkommata eingeschlossen und können Steuerzeichen enthalten – das ist in diesem Beispiel allerdings nicht der Fall.
Der auf die Zeichenkette folgende Verkettungsoperator bewirkt einen Zeilenvorschub durch Verknüpfung mit dem Manipulator endl.
Der Manipulator endl bewirkt einen Zeilenvorschub und leert den Ausgabestream. Hinweis: Würde man lediglich einen Zeilenvorschub erreichen wollen, so müsste man stattdessen das Steuerzeichen \n innerhalb der vorangegangenen Zeichenkette platzieren: ... << "Hallo vom langweiligsten Programm der Welt \n"
Das Semikolon schließt die zusammengesetzte Anweisung (aka „den Befehl“) ab und sorgt für die Ausführung. Unter C++ schließt man Anweisungen grundsätzlich mit einem Semikolon ab!
Zeile 10: Die Anweisung return 0; liefert den Wert Null an den Aufrufer (die Kommandozeile) zurück. Das Programm ist somit beendet.
Zeile 11: Schließende geschweifte Klammer des Funktionsrumpfes.
Das waren jetzt sehr viele Erklärungen und neue Begriffe auf einen Schlag! Lasse die Informationen ein Weilchen sacken und rekapituliere das Gelernte, bevor du mit dem nächsten Thema weitermachst.
Was geschieht beim Compilerlauf?
Die Kenntnis der genauen Abläufe während des Übersetzungsprozesses kann dir dabei helfen, Fehlermeldungen besser zu interpretieren und so einem Fehler schneller auf die Schliche zu kommen. Der Übersetzungsprozess erstreckt sich über vier Phasen – und in jeder Phase können spezifische Fehler auftreten.
Die vier Phasen des Übersetzungsprozesses:
Unser erstes lauffähiges Programm haben wir nun erstellt und seine Bestandteile besprochen. Gehen wir einen Schritt zurück und schauen uns den Prozess, den der Compiler durchläuft, wenn er dein Programm erstellt, einmal genauer an.
Das Verständnis dieser Abläufe ist deshalb wichtig, weil es verschiedene Arten von Fehlern gibt, die beim Schreiben von Code auftreten könnten. Es wird dir dabei helfen, eventuelle Fehler einzugrenzen und leichter aufzuspüren. Wenn ein Programm aus dem Quellcode erstellt wird, arbeiten vier Tools daran, bevor es sich um eine ausführbare Datei handelt: Der Präprozessor, der Compiler, der Assembler und der Linker.
Auch wenn es sich im Sprachjargon so eingebürgert hat, handelt es sich bei g++ genau genommen nicht wirklich um den Compiler, sondern viel mehr um eine Benutzer- schnittstelle, welche die Arbeit der einzelnen Compiler-Bestandteile koordiniert. Diese Schnittstelle leitet, in Abhängigkeit von übergebenen Parametern und im Quellcode hinterlegten Präprozessor-Anweisungen, die entsprechenden Phasen der Compilierung ein:
Phase 1: Der Präprozessor
Der Präprozessor nimmt den unverarbeiteten Quellcode als Eingabe entgegen und führt einige kleinere Bearbeitungen durch, bevor der von ihm erzeugte Code an den Compiler weitergeschickt wird. Er entfernt u.a. Kommentare und fügt den Inhalt von mit #include-Anweisungen eingebundenen Dateien in den von ihm erstellten Zwischencode ein. Es gibt weitere Präprozessor-Anweisungen, die wir zu einem späteren Zeitpunkt ausführlicher behandeln werden.
Phase 2: Der Compiler
Der Compiler übersetzt den C++-Code in Maschinensprache (Assembler). Assembler-Code ist viel, viel näher an den Anweisungen, die der Computer versteht dran und bleibt dabei für den Menschen lesbar. Der erzeugte Code ist spezifisch für den Prozessor, für den er geschrieben wird.
Phase 3: Der Assembler
Der Assembler erstellt aus dem vom Compiler erzeugten Assembler-Code Objektcode und legt diesen in Objektdateien, die eine .o-Erweiterung haben, ab. Objektcode besteht aus den tatsächlichen maschinenausführbaren Anweisungen, die vom Computer verwendet werden, um dein Programm auszuführen. Er ist jedoch noch nicht ganz ausführungsreif. Im gegenwärtigen Zustand gleichen die Objektdateien, aus denen Ihr Programm besteht, einer Reihe von Puzzlestücken, die erst noch zusammengesetzt werden müssen.
Phase 4: Der Linker
Der Linker fügt deine Objektdateien mit allen Bibliotheken, die du verwendest, zu einem ausführbaren Programm zusammen.
Der Entwicklungszyklus eines C++ Programms
Der vollständige Entwicklungszyklus eines C++ Programms umfasst noch mehr Aspekte, als bloß das Schreiben des Quelltextes und den Compilerlauf.
Am Anfang steht natürlich die Idee zum Programm und ihre Umsetzung in C++ – das Schreiben des Quelltextes.
Im zweiten Schritt wird der Quelltext durch einen Compilerlauf zu einem Object-Code übersetzt. Das kann auf Anhieb funktionieren – oder auch nicht.
Falls in diesem Stadium Compilerfehler auftreten, so muss deren Ursache im Quelltext gesucht und behoben werden. Diesen Vorgang nennt man Source Level Debugging. Anschließend erfolgt ein weiterer Compilerlauf.
Geht nun alles glatt, dann wird versucht, den erzeugte Object-Code durch den Linker um erforderliche Bibliotheken zu erweitert und zu einem ausführbaren Programm zu „binden“.
Falls der Linker Fehler meldet, so müssen die Ursachen erneut im Quelltext gesucht und ein neuer Compilerlauf angestoßen werden. Ist dieser dann erfolgreich, so wird das fertige Programm erzeugt und kann jetzt auf Laufzeitfehler getestet werden. Dazu startet man das Programm und vergleicht sein Verhalten und seine Ausgaben mit den bei Programmlegung festgelegten Erwartungen.
Laufzeitfehler führen zum Absturz des ausgeführten Programms, zu falschen Ergebnissen oder zu nicht vorhersehbarem Verhalten des Programms. Die Ursachen dafür können sehr unterschiedlich sein – beispielsweise wenn Variablen mit falschen/inkonsistenten Daten überschrieben oder gefüllt werden, die in nachfolgenden Befehlen zu Fehlern führen. Laufzeitfehler sind deutlich schwerer aufzuspüren, als einfache Syntaxfehler. In der Regel kommt dann ein Debugger zum Einsatz, mit dem man das Verhalten von Speicheradressen und Variableninhalten zur Laufzeit mitverfolgen kann. GCC stellt hierfür den gdb Debugger zur Verfügung.
Ein Programm ist erst dann als vollständig entwickelt anzusehen, wenn es fehlerfrei läuft.
Um mit C++ programmieren zu können, brauchst du eigentlich nur einen auf deinem Betriebssystem lauffähigen Compiler für C++ und einen entsprechenden Texteditor. Dein Texteditor sollte unbedingt visuelle Zeilennummerierung und die Syntax-Hervorhebung (aka Highlighting) für C++ unterstützen, das macht das Schreiben von Programmen und die Fehlersuche wesentlich einfacher.
Unter Linux bist du von vorn herein fein raus – das OS bringt die GNU Compiler Collection (GCC) und diverse Texteditoren mit oder ohne GUI bereits mit. Du brauchst die entsprechenden Pakete nur mit dem Paketmanager deiner Linux-Distribution zu installieren. Kostet keinen Cent.
Auch unter Windows stehen diverse Compiler-Systeme und Editoren zur Verfügung – neben wahren Monstern wie dem Microsoft Visual Studio u. a. auch die GNU Compiler Collection GCC. Empfehlenswerte Text-Editoren sind u.a. Geany, Notepad++ und die Entwicklungsumgebung Code::Blocks.
Auf einem Macunter OS X musst du zunächst erst XCode installieren. Die Entwicklungsumgebung bringt alle nötigen Bestandteile für die Programmierung mit C++ mit.
Betrachtungen zur Entwicklungsumgebung
Um Programme in C++ schreiben zu können, benötigst du also eine Entwicklungs- umgebung und die besteht in ihrer einfachsten Form aus zwei grundlegenden Komponenten: Einem Compilersystem für C++ und einem dedizierten Texteditor zur Erfassung der Quelltexte.
Beides kostenlos zu beschaffen ist heute kein Problem. Unter Linux steht alles, was du zum Programmieren benötigst, ohnehin im Paketmanager der jeweiligen Distribution zur Verfügung. Für andere Betriebssysteme existieren vorkonfigurierte Lösungen, wie z. B. Microsoft Visual Studio Community Edition unter Windows oder XCode auf dem Mac. Solche Komplettlösungen bieten neben vielen Vorteilen allerdings auch einige gravierende Nachteile für Einsteiger. Sie sind zunächst unübersichtlich, aufwendig zu konfigurieren, erfordern zusätzliche Einarbeitungszeit und lenken dich so erst mal von der eigentlichen Aufgabe ab: Programmieren lernen!
Für dieses Tutorial werden wir uns eine maßgeschneiderte eigene, zukunftssichere Entwicklungsumgebung für C++ zusammenstellen, die nicht nur den Bedürfnissen eines Einsteigers gerecht wird, sondern später auch das Programmieren von Anwendungen mit grafischer Benutzeroberfläche erlaubt.
1 – Der Compiler
Um einen Quelltext in ein lauffähiges Programm zu übersetzen, bedarf es eines C++-Compilers. Ich habe mich hier für die C/C++-Komponenten aus der GNU Compiler Collection (GCC – GNU gcc/g++) entschieden – diese sind stets aktuell, mächtig, erfüllen annähernd die neuesten Sprachstandards und stehen für alle gängigen Betriebssysteme zur Verfügung. Insgesamt unterstützt die GCC mehr als 60 Plattformen. Die Programme der GCC sind ein gut etablierter Standard für Programmierer und werden auch im professionellen Umfeld entsprechend gern und häufig eingesetzt.
GNU Compiler Collection herunterladen
GNU gcc/g++findet man in verschiedenen Varianten im Internet. Auch wenn es ein wenig Overhang bedeutet, werden wir in diesem Tutorial mit Hinblick auf die spätere Entwicklung von Programmen mit grafischer Benutzeroberfläche (nicht Bestandteil dieses Tutorials!) die Installation zusammen mit dem Qt Frameworkund dem Qt-Creator vornehmen.
Unter Linux installiere einfach alle Komponenten für Qt5, Qt6 und den Qt-Creator über den Paketmanager deiner Distribution. Für andere Betriebssysteme gilt:
Melde dich im Installer mit deinen Zugangsdaten an und klicke auf <weiter>
Setze die Haken für die Checkboxen wie im folgenden Bild: Klicke auf <weiter>
Durchlaufe mit die Installationsroutine bis zum Punkt „Benutzerdefinierte Installation“. Bestimme den Speicherort für die Installation von Qt und klicke auf <weiter>
Setze die Haken bei „Qt Creator“, „Additional Libraries“ und in der aktuellsten Qt6 Release Version den Haken bei „MinGW 11.2.0 64-bit“.
Gib im Suchfeld „Qt 5“ ein. Unter dem Eintrag „Qt 5.15.2“ selektiere mindestens den Eintrag „MinGW 8.1.0 64-bit“ und die Einträge für die zusätzlichen Bibliotheken:
Unter „Developer and Designer Tools“ selektiere die Einträge „Qt Creator“, „MinGW 13.0.1 64-bit“, „MinGW 11.1.0 64-bit“, „MinGW 8.1.0 64-bit“, „CMake“ und „Ninja“.
Zum Start der Installation klicke auf und fasse dich in Geduld…
Einrichtung des Compilers
Um den Compiler von der Kommandozeile aus erreichen zu können ist es notwendig, die PATH Variable deines Betriebssystems entsprechend um den Pfad zum Compiler zu erweitern. Unter Linux geschieht das automatisch bei der Installation von gcc, unter anderen Betriebssystemen musst du die Anpassung selbst vornehmen.
Angenommen, du befindest dich unter Windows, bist der Installationsanleitung gefolgt und hast Qt nach C:\Qt installiert. Die aktuell höchste installierte Version von MinGW (gcc) findest Du dann unter C:\Qt\Tools\mingw1310_64.
Öffne den Dialog zur Einstellung von Umgebungsvariablen, selektiere „PATH“ und erweitere die Variable um die aus dem folgenden Bild ersichtlichen Einträge:
Bestätige den Dialog und alle übergeordneten Instanzen, öffne eine neue Kommandozeile und teste deine Einstellungen durch Eingabe von gcc -v:
Dein Compiler-System sollte nun einsatzbereit sein. Für das Setzen von Systemvariablen unter anderen Betriebssystemen konsultiere bitte das entsprechende Handbuch.
Verwenden des Compilers
Am Einfachsten ist es, den Compiler über entsprechende Befehle aus dem Editor heraus zu steuern – und das ist auch die bevorzugt Vorgehensweise in diesem Tutorial. Trotzdem kann es nicht schaden, wenn du weißt, wie man den Compiler auch auf der Kommandozeile bedienen kann.
Für die Übersetzung von C++-Quelltexten verwenden wir GNU g++, das C++-Compiler Backend der GNU Compiler Collection. Es wird über diverse Schalter gesteuert, die sein Verhalten regeln. Hier eine kurze Aufstellung der wichtigsten Schalter:
-Wall (Warn all) sorgt dafür, dass der Compiler neben Fehlern auch auf sämtliche Warnungen bei der Übersetzung eines Programmes reagiert und diese ausgibt
-v (verbose) bewirkt eine sehr detaillierte Ausgabe von Compilermeldungen
-std= <Bezeichner> bewirkt die Verwendung eines bestimmten, definierten Compilerstandards
-s (strip) bewirkt die Optimierung der Größe des erzeugten Programms
-g, -ggdb (gnudebug) fügt dem erzeugten Programm sogenannten Debug-Code hinzu – das Programm lässt sich so mit dem Debugger (Werkzeug zur Fehlersuche) gdb aus der GNU Compiler Collection auf Fehler überprüfen
-o (output) speichert das übersetzte Programm unter einem frei wählbaren Namen
-l (Library) bindet eine zusätzliche Linker-Bibliothek ein. Häufig verwendet wird z. B. -lm zur Einbindung der mathematischen Bibliothek für arrithmetische CoProzessoren (FPU)
-O (optimize) bewirkt die Optimierung des zu übersetzenden Programms, wobei der Parameter für die Optimierungsstufe steht. Üblich ist z.B. -O3. Optimierungen sollten nur bei einem vorher ausgiebig getesteten, für die Veröffentlichung bestimmten Programm zugeschaltet werden!
Zum Compilieren eines Programms auf der Kommandozeile musst du dich in dem Verzeichnis befinden, in dem der zugehörige Quelltext gespeichert ist. Der übliche Aufruf von g++ für die Programme in diesem Tutorial lautet dann: g++ -Wall -std=c++17 -o meinProgramm meinCode.cpp
Dabei gibt g++ sämtliche auftretenden Fehlermeldungen und Warnungen unter Verwendung des ISO/ANSI-Standards c++17 auf der Kommandozeile aus und erstellt aus dem Quelltext meinCode.cpp das lauffähige Programm meinProgramm. Unter DOS/Windows wird automatisch noch der Suffix .exe an den Programmnamen angehängt. Das fertig übersetzte Programm befindet sich danach im gleichen Verzeichnis wie der zugehörige Quellcode und kann durch
meinProgramm + <Eingabetaste>
zur Ausführung gebracht werden.
Der Compiler besitzt noch viele weitere Schalter und Optionen. Außerdem enthält die GNU Compiler Collection weitere nützliche Tools und Hilfsprogramme. Eine vollständige Beschreibung findest Du in den Handbüchern unter https://gcc.gnu.org/onlinedocs/gcc-13.3.0/gcc/
2 – Der Editor
Zum Schreiben von Programmen brauchst Du, wie bereits gesagt einen Editor, der deine Quelltexte als einfache ASCII-Dateien speichern kann. Grundsätzlich könnte man hierfür nun den Qt-Creator oder andere Speichermonster wie Visual Studio Code, Word oder LibreOffice Writer mit speziellen Speicheroptionen einsetzen – jedoch ist es deutlich angenehmer, auf einen Editor zurück zu greifen, der dich nicht ablenkt, Nützlichkeiten wie Syntax Highlighting, Code Folding und Auto-Vervollständigung beherrscht und am Besten auch gleich noch den Compiler anzusprechen vermag. Die Puristen unter den Programmierern nehmen hierfür GNU Emacs, vi oder nano unter Linux, wir allerdings wollen es in diesem Tutorial etwas bequemer haben.
Es existiert eine mehr oder minder reiche Auswahl an solchen Editoren für die verschiedenen Betriebssysteme. In diesem Buch gehe ich davon aus, dass du den quelloffenen Editor „Geany“ verwendest – er erfüllt alle oben genannten Kriterien und ist für Linux, Windows und MacOS kostenlos erhältlich.
Geany herunterladen und installieren
Unter Linux ist der Editor bereits Bestandteil vieler Distributionen – installiere das Programm einfach über den Paketmanager der von dir verwendeten Linux-Distribution. Für andere gebräuchliche Betriebssysteme gilt: Geany und seine Zusatzkomponenten kannst du hier für dein Betriebssystem herunterladen: https://www.geany.org/download/releases/ Um Geany zu installieren genügt es, den jeweiligen Installer für das von dir verwendete Betriebssystem zu starten.
Geany konfigurieren
Geany kommt bereits vorkonfiguriert daher, unterstützt viele gängige Programmier- sprachen und erkennt selbstständig, mit Welcher du gerade arbeitest. Dabei lässt sich der jeweilige Befehlssatz zur Ansteuerung des Compilers aber noch nachjustieren. Klicke dazu im Menü „Erstellen“ auf den Eintrag „Kommandos zum Erstellen konfigurieren“ – es erscheint der folgende Dialog:
Die Compiler gcc und g++ übersetzen Programme standardmäßig mit gnu99, einer älteren Vorgabe für C/C++ mit einigen zusätzlichen Besonderheiten der GNU Compiler Collection. In diesem Buch wollen wir aber mit einem fortgeschrittenen, relativ aktuellen Sprachstandard arbeiten, wie er in den Normen c++14 und c++17 für ISO/ANSI C++ definiert ist.
Der Schalter -std veranlasst gcc/g++ dazu, einen anderen als den vorgegebenen Sprachstandard zu verwenden. Trage deshalb im Konfigurationsdialog hinter allen Vorkommen von g++ -Wall den Schalter -std=c++17 ein.
Der Schalter -Wall (Warn all) sorgt dafür, dass der Compiler Warnungen und Fehlermeldungen ausgibt. Das ist besonders nützlich für die Fehlersuche im Quelltext. Besonders geschwätzig wird der Compiler, wenn du zusätzlich noch den Schalter -v (verbose) einfügst – allerdings wird die Ausgabe der Meldungen dann schnell unübersichtlich.
Geany verwenden
Geany zeichnet sich durch Schlichtheit und Funktionalität aus – alles ist intuitiv und leicht erreichbar. Das folgende Bild illustriert den Aufbau des Editors:
Texteingabefenster: Hier gibst du deine Programme ein.
Meldungsfenster: Hier erfolgt die Ausgabe von Compilermeldungen. Falls bei der Übersetzung ein Fehler oder eine Warnung aufgetreten ist, so kannst du diese anklicken – die Schreibmarke springt dann im Texteingabefenster auf die vermutete Stelle, an der ein Problem aufgetreten ist.
Seitenleiste: Hier listet der Editor Klassen, Funktionen, Strukturen und andere Elemente von C++ auf. Durch einen Klick auf einen Eintrag springt die Schreibmarke zur entsprechenden Stelle im Texteingabefenster.
Die Arbeit mit dem Editor gestaltet sich denkbar einfach:
Programme schreiben: Gib deinen Quelltext im Texteingabefenster ein und speichere ihn unter einem aussagekräftigen Namen mit der Dateiendung .cpp ab.
Übersetzung anstoßen: Drücke die Funktionstaste <F9> , um den Quelltext compilieren zu lassen. Veränderungen am Quelltext werden dabei automatisch gespeichert. Falls du den Quelltext noch nicht abgespeichert hast, so musst du das vor dem ersten Compilerlauf nachholen.
Programm ausführen: Drücke die Funktionstaste <F5>, um das fertige Programm zu starten.
Die Programmiersprache C++ ist weltweit eine der beliebtesten Entwicklersprachen für professionelle Software. Mit ihr kannst du sehr systemnah komplexe und schnelle Programme schreiben, die mit wenig Aufwand auf nahezu jedes beliebige Betriebssystem portierbar sind, für das es einen C++-Compiler gibt.
C++ wurde ab 1979 von Bjarne Stroustrup bei AT&T als Erweiterung der Programmiersprache C entwickelt. Die Sprache ermöglicht sowohl die effiziente und maschinennahe Programmierung als auch eine Programmierung auf hohem Abstraktionsniveau. Was Stroustrup hinzufügte, sind die Merkmale der objektorientierten Programmierung (OOP). Somit wird – zumindest theoretisch – das bis dahin gängige Paradigma der prozeduralen Programmierung zugunsten eines dem menschlichen Denken ähnlicheren Ansatzes abgelöst.
C++ wie auch C sind Compilersprachen – was besagt, dass der Quellcode vor der Ausführung in Maschinensprache übersetzt werden muss. Diese Aufgabe kommt dem C++-Compiler zu. C++ ist eine Obermenge von C – das besagt, dass jedes gültige C-Programm mit einem C++-Compiler übersetzt werden kann. Was es keinesfalls besagt ist, dass du über C-Kenntnisse verfügen musst, um C++ zu erlernen! Die beiden Sprachen sind nicht annähernd so nah miteinander verwandt, wie man auf den ersten Blick glauben möchte und oft ist es besser, ohne festgefahrene Strukturen an eine neue Sprache heran zu gehen.
Die Sprache C++ verwendet nur etwa 60 Schlüsselwörter („Sprachkern“), manche werden in verschiedenen Kontexten (static, default) mehrfach verwendet. Ihre eigentliche Funktionalität erhält sie, ähnlich wie auch die Sprache C, durch die C++-Standardbibliothek, die der Sprache fehlende wichtige Funktionalitäten beibringt (Arrays, Vektoren, Listen, . . . ) wie auch die Verbindung zum Betriebssystem herstellt (iostream, fopen, exit, . . . ). Je nach Einsatzgebiet kommen weitere Bibliotheken und Frameworks dazu. C++ legt einen Schwerpunkt auf die Sprachmittel zur Entwicklung von wiederverwendbaren Bibliotheken. Dadurch favorisiert es verallgemeinerte Mechanismen für typische Problemstellungen und besitzt nur wenige, kaum in die Sprache integrierte Einzellösungen.