C++-Programmierung/ Einführung in C++
Zielgruppe:
Programmieranfänger, für Umsteiger reicht die Zusammenfassung.
Lernziel:
Grundelemente in C++
Hallo, du schöne Welt! [Bearbeiten]
Es ist eine alte Tradition, eine neue Programmiersprache mit einem „Hello-World“-Programm einzuweihen. Auch dieses Buch soll mit der Tradition nicht brechen, hier ist das „Hello-World“-Programm in C++:
Zugegebenermaßen ist es nicht die Originalversion, sondern eine Originellversion von „Hello-World“. Wenn Sie das Programm ausführen, bekommen Sie den Text „Hallo, du schöne Welt!“ am Bildschirm ausgegeben. Sie wissen nicht, wie Sie das Programm ausführen können? Dann lesen Sie doch einmal das Kapitel über Compiler.
#include <iostream>
stellt die nötigen Befehle zur Ein- und Ausgabe bereit. Als nächstes beginnt die „Hauptfunktion“ main()
. Diese Hauptfunktion wird beim Ausführen des Programms aufgerufen. Sie ist also der zentrale Kern des Programms. Wenn Funktionen im Text erwähnt werden, stehen dahinter übrigens immer Klammern, um sie besser von anderen Sprachbestandteilen wie beispielsweise Variablen unterscheiden zu können.
std::cout
beschreibt den Standardausgabe-Strom. Dabei wird der Text meist in einem Terminal angezeigt (wenn die Ausgabe nicht in eine Datei oder an ein anderes Programm umgeleitet wird). Die beiden Pfeile (<<
) signalisieren, dass der dahinterstehende Text auf die Standardausgabe „geschoben“ wird. Das std::endl
gibt einen Zeilenumbruch aus und sorgt dafür, dass der Text jetzt am Bildschirm ausgegeben wird.
return 0
beendet das Programm und zeigt dem Betriebssystem an, dass es erfolgreich ausgeführt wurde. Auf die Einzelheiten wird in den folgenden Kapiteln (oder Abschnitten) noch ausführlich eingegangen. Diese Zeile ist optional; wird sie nicht angegeben, gibt der Compiler implizit 0 zurück.
Im Moment sollten Sie sich merken, dass jeder C++-Befehl mit einem Semikolon (;
) abgeschlossen wird und dass geschweifte Klammern ({...}
) Zusammengehörigkeit symbolisieren – so auch oben in der Hauptfunktion. Alles was zwischen den geschweiften Klammern steht, gehört zu ihr. Leerzeichen, Tabulatorzeichen und Zeilenumbrüche spielen für den C++-Compiler keine Rolle. Sie können das folgende Programm genauso übersetzen wie seine gut lesbare Version von weiter oben:
Vorsichtig sollten Sie bei Zeilen sein, die mit #
beginnen. Leerzeichen und Tabulatoren sind zwar auch hier bedeutungslos, aber Zeilenumbrüche dürfen nicht stattfinden.
Es ist übrigens (für Ihren Rechner) auch irrelevant, ob Sie etwas wie // Ein- und Ausgabebibliothek
mit in Ihr Programm schreiben oder nicht. Es handelt sich dabei um sogenannte Kommentare, die Sie in Kürze auch genauer kennenlernen werden. Beachten sollten Sie übrigens, dass bei C++ die Groß- und Kleinschreibung relevant ist. Schlüsselwörter und Namen der Standardbibliothek werden stets kleingeschrieben. Für die Groß-/Kleinschreibung von selbstdefinierten Namen gibt es gewisse Konventionen, auf welche in einem späteren Kapitel eingegangen wird.
Bei allen in diesem Buch beschriebenen Programmen handelt es sich um so genannte Kommandozeilenprogramme. Falls Sie eine IDE zur Entwicklung benutzen und das Programm direkt aus dieser heraus aufrufen, kann es Ihnen passieren, dass Sie nur einen kurzen Blitz von Ihrem Programm sehen, weil sich das Kommandozeilenfenster nach der Programmbeendigung sofort schließt.
In diesem Fall haben Sie zwei Optionen:
- Rufen Sie eine Kommandozeile auf und führen Sie das Programm von dort manuell aus. (Das wird empfohlen!)
- Schauen Sie nach, ob es in Ihrer IDE eine Funktion gibt, die das Fenster nach Programmende noch einen Tastendruck lang offen hält. (Eine solche Option muss nicht vorhanden sein!)
Einfache Ein- und Ausgabe [Bearbeiten]
Ein- und Ausgaberoutinen geben Ihnen die Möglichkeit, mit einem Programm zu interagieren. Dieses Kapitel beschäftigt sich mit der Eingabe über die Tastatur und der Ausgabe auf der Konsole in C++-typischer Form. Die C-Variante werden Sie später noch kennenlernen.
Um die C++-Ein- und Ausgabe nutzen zu können, müssen Sie die Bibliothek iostream
einbinden. Das geschieht mit:
Danach müssen die Befehle daraus bekanntgegeben werden, da sie sich in einem Namensraum (engl. namespace
) befinden. Was Namensräume sind und wofür man sie einsetzt, werden Sie später noch erfahren. Um nun die Ein- und Ausgabebefehle nutzen zu können, müssen Sie dem Compiler sagen: Benutze den Namensraum std
. Dafür gibt es zwei Möglichkeiten: Sie können die folgende Zeile verwenden, um alle Namen aus dem Namensraum std
verfügbar zu machen:
Oder Sie können den Namensraum „std
“ immer direkt angeben. Er enthält alle Komponenten der Standardbibliothek von C++. In den Programmen dieses Buches wird der Namensraum immer direkt angegeben. Das heißt, wenn beispielsweise ein Objekt benutzt werden soll, das im Namensraum „std
“ liegt, wird ihm „std::
“ vorangestellt. Die beiden Doppelpunkte heißen Bereichsoperator.
Die Variante mit using namespace
sollte üblicherweise nur innerhalb von Funktionen genutzt werden, da bei Verwendung im globalen Namensraum das ganze Konzept der Namensräume obsolet wird. In gedruckten Büchern wird es gern global verwendet, da dies etwas Platz spart und der in solchen Büchern knapp ist. Lassen Sie sich davon nicht beirren, es ist schlechter Stil.
Einfache Ausgabe
[Bearbeiten]Nun wollen wir aber endlich auch mal was Praktisches tun. Zugegebenermaßen nichts Weltbewegendes und im Grunde nicht einmal etwas wirklich Neues, denn Text haben wir ja schon im „Hello-World“-Programm ausgegeben.
Wie der Text bereits selbst sagte, erscheint er in der Kommandozeile und der zweite schließt sich ohne Unterbrechung an. Der letzte von den drei abschließenden Punkten steht in einer zusätzlichen Zeile, um Ihnen zu demonstrieren, dass man einzelne Zeichen nicht in Anführungszeichen sondern in Apostrophe setzt. Das gilt aber nur für einzelne Zeichen, ab zwei Zeichen ist es schon eine Zeichenkette und dann verwendet man normale Anführungszeichen.
Wenn Sie innerhalb einer Zeichenkette einen Zeilenumbruch einfügen möchten, gibt es zwei Möglichkeiten. Sie können die Escape-Sequenz \n
in die Zeichenkette einfügen oder den Manipulator endl
(für end-line) benutzen. Was genau Escape-Sequenzen oder Manipulatoren sind, ist Thema eines späteren Kapitels, aber folgendes Beispiel demonstriert schon mal die Verwendung für den Zeilenumbruch:
#include <iostream>
int main(){
std::cout << "Text in der Kommandozeile!\n"; // Escape-Sequenz \n
std::cout << "Dieser Text schließt sich an...\n"; // Das steht in einer eigenen Zeile
std::cout << std::endl; // Leerzeile mittels endl
std::cout << "Text in der Kommandozeile!" << std::endl; // Zeilenumbruch mit endl
std::cout << "Dieser Text schließt sich an..." << std::endl; // Das steht in einer eigenen Zeile
}
Text in der Kommandozeile!
Dieser Text schließt sich an...
Text in der Kommandozeile!
Dieser Text schließt sich an...
Beide Methoden haben scheinbar den gleichen Effekt. Verwenden Sie die Escape-Sequence Methode mit "\n"
. std::endl
ist langsamer, da es nicht nur einen Zeilenumbruch ausgibt, sondern auch den Ausgabepuffer leert. Dies wird später noch genauer erläutert.
Einfache Eingabe
[Bearbeiten]Für die Eingabe muss ein wenig vorgegriffen werden, denn um etwas einzulesen, ist etwas nötig, worin das Eingelesene gespeichert werden kann. Dieser „Behälter“ nennt sich Variable. Eine Variable muss zunächst einmal angelegt werden. Am besten lässt sich die Eingabe an einem Beispiel erklären:
Es wird, wie bereits erwähnt, erst eine Variable angelegt (int integer;
), welcher dann ein Wert zugewiesen wird (cin >> integer;
). Diese zweite Zeile bedeutet so viel wie: Lies eine ganze Zahl von der Tastatur und speichere sie in der Variablen integer
.
cin
(gesprochen c-in) ist sozusagen die Tastatur, integer
ist die Variable und >>
bedeutet so viel wie „nach“. Zusammen ergibt sich „Tastatur nach Variable“, es wird also der „Inhalt“ der Tastatur in die Variable integer
verschoben. Dass eine Ganzzahl von der Tastatur gelesen wird, ist übrigens vom Datentyp der Variablen abhängig, aber dazu später mehr.
Um den Inhalt der Variablen wieder auszugeben, müssen Sie nichts weiter tun, als sie mit einem weiteren Schiebeoperator (<<
) hinter cout
(gesprochen c-out) anzuhängen. Es ist ohne Weiteres möglich, mehrere solcher Schiebeoperatoren hintereinander zu schalten, solange Sie nur die letzte Ein- oder Ausgabe mit einem Semikolon (;
) abschließen. Bei der Eingabe muss natürlich der >>
-Operator statt dem <<
-Operator benutzt werden. Die Reihenfolge der Ein- oder Ausgabe bei solchen Konstruktionen entspricht der eingegebenen Folge im Quelltext. Was zuerst hinter cout
oder cin
steht, wird also auch zuerst ausgeführt.
Im Anhang zu diesem Kapitel finden Sie:
- Fragen und die dazugehörigen Antworten.
Kommentare [Bearbeiten]
In allen Programmiersprachen gibt es die Möglichkeit, im Quelltext Notizen zu machen. Für andere oder auch für sich selbst, denn nach ein paar Wochen werden Sie möglicherweise Ihren eigenen Quelltext nicht mehr ohne Weiteres verstehen. Kommentare helfen Ihnen und anderen besser und vor allem schneller zu verstehen, was der Quelltext bewirkt. In C++ gibt es zwei Varianten, um Kommentare zu schreiben:
// Ein Kommentar, der mit zwei Schrägstrichen eingeleitet wird, geht bis zum Zeilenende
/* Ein Kommentar dieser Art kann
sich über mehrere Zeilen
erstrecken oder ... */
a = b /* ... vor dem Zeilenende enden. */ + c;
Die erste, bis zum Zeilenende geltende Sorte, ist die moderne Art des Kommentars. Sie ist in der Regel vorzuziehen, da sie einige Vorteile gegenüber der alten, noch aus C stammenden Variante hat. Die zweite Sorte (manchmal auch als C-Kommentar bezeichnet) in seiner mehrzeiligen Form sollte nur an Stellen verwendet werden, an denen längere Textpassagen stehen und möglichst nicht zwischen Code. Anwendung finden solche Kommentare oft am Dateianfang, um den Inhalt kurz zusammenzufassen oder Lizenzrechtliches zu regeln.
Der hauptsächliche Nachteil bei den mehrzeiligen Kommentaren besteht darin, dass man sie nicht „verschachteln“ kann. Oft werden beispielsweise Teile des Quellcodes zu Testzwecken kurzweilig auskommentiert. Folgendes Beispiel soll dies demonstrieren:
#include <iostream> /* Ein- und Ausgabebibliothek */
int main(){ /* Hauptfunktion */
/*
std::cout << "Hallo, du schöne Welt!" << std::endl; /* Ausgabe */
*/
}
Das würde nicht funktionieren. Wenn hingegen die anderen Kommentare benutzt werden, gibt es solche Probleme nicht:
#include <iostream> // Ein- und Ausgabebibliothek
int main(){ // Hauptfunktion
/*
std::cout << "Hallo, du schöne Welt!" << std::endl; // Ausgabe
*/
}
Im ersten Beispiel wird die Einleitung von /* Ausgabe */
einfach ignoriert. Der abschließende Teil beendet den Kommentar, der die Code-Zeile auskommentieren soll und der eigentliche Abschluss führt zu einem Kompilierfehler. Zugegeben, das ist nicht weiter schlimm, denn solch ein Fehler ist schnell gefunden, aber ihn von vorn herein zu vermeiden, ist eben noch zeitsparender. Im übrigen muss bei einzeiligen Kommentaren oft weniger geschrieben werden. Eine Ausnahme bilden Kommentare, die einfach zu lang sind, um sie auf eine Zeile zu schreiben. Dennoch sollten auch sie durch //
-Kommentare realisiert werden.
viel Code…
// Ich bin ein Beispiel für einen langen Kommentar, der durch doppelte
// Schrägstriche über mehrere Zeilen geht. Würde ich am Dateianfang stehen,
// hätte man mich wahrscheinlich mit anderen Kommentarzeichen ausgestattet,
// aber da ich hier eindeutig von einer Riesenmenge Quelltext umgeben bin,
// hat man sich trotz der erhöhten Schreibarbeit für // entschieden
noch viel mehr Code…
Viele Texteditoren enthalten eine Tastenkombination, über die sich Text ein- und auskommentieren lässt. Besonders für längere Codepassagen ist dies nützlich.
Rechnen (lassen) [Bearbeiten]
In diesem Kapitel soll unser Rechner einmal das tun, was er ohnehin am besten kann: Rechnen. Wir werden uns derweil zurücklehnen und zusehen oder besser gesagt, werden wir das tun, nachdem wir ihm mitgeteilt haben, was er rechnen soll.
Einfaches Rechnen
[Bearbeiten]Zugegebenermaßen hätten Sie diese Rechnung wahrscheinlich auch im Kopf lösen können aber warum sollten Sie sich so unnötig anstrengen. Ihr Rechner liefert doch auch das richtige Ergebnis, und wenn Sie dies mit der Eingabe von Zahlen kombinieren, können Sie sogar bei jedem Programmdurchlauf zwei unterschiedliche Zahlen addieren:
#include <iostream>
int main(){
int summand1, summand2; // Anlegen von zwei Variablen
std::cin >> summand1 >> summand2; // Zwei Zahlen eingeben
std::cout << summand1 << " + " << summand2 // beide durch " + " getrennt wieder ausgeben
<< " = " // " = " ausgeben
<< summand1 + summand2 // Ergebnis berechnen und ausgeben
<< std::endl; // Zeilenumbruch
}
Benutzereingabe: 774
Benutzereingabe: 123
774 + 123 = 897
Das Ergebnis lässt sich natürlich auch in einer Variablen zwischenspeichern. Folgendes Beispiel demonstriert diese Möglichkeit:
#include <iostream>
int main(){
int summand1, summand2, ergebnis; // Anlegen von drei Variablen
std::cin >> summand1 >> summand2; // Zwei Zahlen eingeben
ergebnis = summand1 + summand2; // Ergebnis berechnen
std::cout << summand1 << " + " << summand2 // beide durch " + " getrennt wieder ausgeben
<< " = " // " = " ausgeben
<< ergebnis // Ergebnis ausgeben
<< std::endl; // Zeilenumbruch
}
Benutzereingabe: 400
Benutzereingabe: 300
400 + 300 = 700
Die großen Vier
[Bearbeiten]C++ beherrscht die vier Grundrechenarten: Addition (+
), Subtraktion (-
), Multiplikation (*
) und Division (/
). Genau wie in der Mathematik gilt auch in C++ die Regel: Punktrechnung geht vor Strichrechnung und Klammern gehen über alles. Das folgende Beispiel soll eine komplexere Rechnung demonstrieren:
#include <iostream>
int main(){
int ergebnis; // Anlegen einer Variable
ergebnis = ((3 + 3*4)/5 - 1)*512 - 768; // Ergebnis berechnen
std::cout << "((3 + 3*4)/5 - 1)*512 - 768 = " // Aufgabe ausgeben
<< ergebnis // Ergebnis ausgeben
<< std::endl; // Zeilenumbruch
}
((3 + 3*4)/5 - 1)*512 - 768 = 256
Gerechnet wird in dieser Reihenfolge:
3 * 4 = 12 3 + 12 = 15 15 / 5 = 3 3 - 1 = 2 2 * 512 = 1024 1024 - 768 = 256
Sie sollten darauf achten, immer die gleiche Anzahl öffnende und schließende Klammern zu haben, denn dies ist ein beliebter Fehler, der von Anfängern meist nicht so schnell gefunden wird. Compiler bringen in solchen Fällen nicht selten Meldungen, die einige Zeilen unter dem eigentlichen Fehler liegen.
Zusammengesetzte Operatoren
[Bearbeiten]C++ ist eine Sprache für schreibfaule Menschen. Daher gibt es die Möglichkeit, die Rechenoperatoren mit dem Zuweisungsoperator zu kombinieren. Dies sieht dann folgendermaßen aus:
zahl = 22;
zahl += 5; // zahl = zahl + 5;
zahl -= 7; // zahl = zahl - 7;
zahl *= 2; // zahl = zahl * 2;
zahl /= 4; // zahl = zahl / 4;
Als Kommentar sehen Sie die Langfassung geschrieben. Diese Kurzschreibweise bedeutet nicht mehr, als dass die vor (!) dem Zuweisungsoperator stehende Rechenoperation mit der Variablen auf der linken Seite und dem Wert auf der rechten Seite ausgeführt und das Ergebnis der Variablen auf der linken Seite zugewiesen wird. Sie sollten diese Kurzschreibweise der ausführlichen vorziehen, da sie nicht nur die Finger schont, sondern auch noch ein wenig schneller ist.
Am besten werden Sie dies wahrscheinlich verstehen, wenn Sie es einfach ausprobieren. Stehen auf der rechten Seite noch weitere Rechenoperationen, so werden diese zuerst ausgeführt. Das Ganze stellt sich dann also folgendermaßen dar:
Inkrement und Dekrement
[Bearbeiten]Inkrementieren bedeutet, den Wert einer Variablen um 1 zu erhöhen, entsprechend bedeutet Dekrementieren 1 herunterzuzählen. Dem Inkrementoperator schuldet C++ übrigens seinen Namen. Die beiden Operatoren gibt es jeweils in der Präfix- und der Postfix-Variante. Insgesamt ergeben sich also vier Operatoren:
zahl = 5;
zahl++; // Inkrement Postfix (zahl == 6)
++zahl; // Inkrement Präfix (zahl == 7)
zahl--; // Dekrement Postfix (zahl == 6)
--zahl; // Dekrement Präfix (zahl == 5)
Der Unterschied zwischen Inkrement (++
) und Dekrement (--
) ist ohne größeres Nachdenken erkennbar. Der Sinn von Präfix und Postfix ergibt sich hingegen nicht sofort von selbst. C++ schuldet seinen Namen der Postfix-Variante.
Der Unterschied zwischen Präfix und Postfix besteht im Rückgabewert. Die Präfix-Variante erhöht den Wert einer Zahl um 1 und gibt diesen neuen Wert zurück. Die Postfix-Variante erhöht den Wert der Variablen ebenfalls um 1, gibt jedoch den Wert zurück, den die Variable vor der Erhöhung hatte.
Das folgende kleine Programm zeigt den Unterschied:
#include <iostream>
int main(){
int zahl; // Anlegen einer Variable
std::cout << "zahl direkt ausgeben:\n";
zahl = 5; // zahl den Wert 5 zuweisen
std::cout << zahl << ' '; // zahl ausgeben
zahl++; // Inkrement Postfix (zahl == 6)
std::cout << zahl << ' '; // zahl ausgeben
++zahl; // Inkrement Präfix (zahl == 7)
std::cout << zahl << ' '; // zahl ausgeben
zahl--; // Dekrement Postfix (zahl == 6)
std::cout << zahl << ' '; // zahl ausgeben
--zahl; // Dekrement Präfix (zahl == 5)
std::cout << zahl << ' '; // zahl ausgeben
std::cout << "\nRückgabewert des Operators ausgeben:\n";
zahl = 5; // zahl den Wert 5 zuweisen
std::cout << zahl << ' '; // zahl ausgeben
std::cout << zahl++ << ' '; // Inkrement Postfix (zahl == 6)
std::cout << ++zahl << ' '; // Inkrement Präfix (zahl == 7)
std::cout << zahl-- << ' '; // Dekrement Postfix (zahl == 6)
std::cout << --zahl << ' '; // Dekrement Präfix (zahl == 5)
std::cout << "\nEndwert von zahl: " << zahl << std::endl;
}
zahl direkt ausgeben:
5 6 7 6 5
Rückgabewert des Operators ausgeben:
5 5 7 7 5
Endwert von zahl: 5
In einem späteren Kapitel werden Sie noch ein paar zusätzliche Informationen erhalten, was beim Rechnen schief gehen kann und wie Sie es vermeiden. Wenn Sie beim Herumexperimentieren mit Rechenoperationen plötzlich scheinbar unerklärliche Ergebnisse erhalten, dann ist es an der Zeit einen Blick auf dieses Kapitel zu werfen.
Im Anhang zu diesem Kapitel finden Sie:
- Aufgaben und zugehörige Musterlösungen.
Variablen, Konstanten und ihre Datentypen [Bearbeiten]
Variablen sind Behälter für Werte, sie stellen gewissermaßen das Gedächtnis eines Programms bereit. Konstanten sind spezielle Variablen, sie ändern ihren Wert nie. Der Datentyp einer Variablen oder Konstanten beschreibt, wie der Inhalt zu verstehen ist.
Ein Rechner kennt nur zwei Grundzustände: 0 und 1. Durch eine Aneinanderreihung solcher Zustände lassen sich mehr verschiedene Werte darstellen. Mit acht aufgereihten Zuständen (= 8 Bit = 1 Byte) lassen sich bereits 256 verschiedene Werte darstellen. Diese Werte kann man beispielsweise als Ganzzahl, (Schrift-)Zeichen, Wahrheitswert oder Gleitkommazahl interpretieren. Der Datentyp gibt daher Auskunft darüber, um was es sich handelt.
Der C++-Standard schreibt nicht vor, dass ein Byte aus genau 8 Bit bestehen muss – diese Anzahl ist jedoch weitverbreitet. Es ist also möglich, dass auf einer speziellen Prozessorarchitektur z. B. eine Anzahl von 10 Bit als „ein Byte“ festgelegt ist. Dies ist jedoch äußerst selten der Fall, daher werden wir im Folgenden annehmen, dass ein Byte aus 8 Bit besteht.
Datentypen
[Bearbeiten]Zunächst sollen die Datentypen von C++ beschrieben werden, denn sie sind grundlegend für eine Variable oder Konstante. Die vier wichtigsten (Gruppen von) Datentypen sind: Wahrheitswerte, Zeichen, Ganzzahlen und Gleitkommazahlen.
Wahrheitswerte
[Bearbeiten]Der Datentyp für Wahrheitswerte heißt in C++ bool
, was eine Abkürzung für boolean ist. Er kann nur zwei Zustände annehmen: true
(wahr) oder false
(falsch). Obwohl eigentlich 1 Bit ausreichen würde, hat bool
mindestens eine Größe von einem Byte (also 8 Bit), denn 1 Byte ist die kleinste adressierbare Einheit und somit die Minimalgröße für jeden Datentyp. Es ist auch durchaus möglich, dass ein bool
beispielsweise 4 Byte belegt, da dies auf einigen Prozessorarchitekturen die Zugriffsgeschwindigkeit erhöht.
Zeichen
[Bearbeiten]Zeichen sind eigentlich Ganzzahlen. Sie unterscheiden sich von diesen nur bezüglich der Ein- und Ausgabe. Jeder Zahl ist ein Zeichen zugeordnet. Mit den Zahlen lässt sich ganz normal rechnen aber bei der Ausgabe erscheint das zugeordnete Zeichen auf dem Bildschirm. Welches Zeichen welcher Zahl entspricht, wird durch den verwendeten Zeichensatz festgelegt.
Die meisten Zeichensätze beinhalten den sogenannten ASCII-Code (American Standard Code for Information Interchange), welcher die Zeichen 0 – 127 belegt. Er enthält 32 Steuerzeichen (0 – 31) und 96 druckbare Zeichen (32 – 127).
char
ist der Standard-Datentyp für Zeichen. Er ist in der Regel 1 Byte groß und kann somit 256 verschiedene Zeichen darstellen. Diese genügen für einen erweiterten ASCII-Code, welcher zum Beispiel auch deutsche Umlaute definiert. Für Unicode-Zeichen gibt es die Datentypen char16_t
mit einer Größe von 2 Byte und char32_t
mit einer Größe von 4 Byte. Früher nutzte man für Unicode auch den Datentyp wchar_t
, welcher je nach System 2 oder 4 Byte groß war. Dieser Datentyp sollte jedoch nicht mehr eingesetzt werden. Die folgende Liste enthält einige nützliche Links zu Artikeln der Wikipedia:
- ASCII
- Unicode (UTF-8, UTF-16, UTF-32)
- ISO 8859-1 (Latin-1)
- Codepage 437 (DOS / DOS-Box)
Es gibt in C++ 4 eingebaute Datentypen für Zeichen:
char
char16_t
char32_t
wchar_t
(veraltet)
Ganzzahlen
[Bearbeiten]C++ definiert folgende eingebaute Datentypen für Ganzzahlen:
Schreibweise | Typ | Anzahl Bits nach data model | ||||
---|---|---|---|---|---|---|
C++ standard | LP32 | ILP32 | LLP64 | LP64 | ||
signed char
|
signed char
|
mindestens 8 |
8 | 8 | 8 | 8 |
unsigned char
|
unsigned char
| |||||
short
|
short
|
mindestens 16 |
16 | 16 | 16 | 16 |
short int
| ||||||
signed short
| ||||||
signed short int
| ||||||
unsigned short
|
unsigned short
| |||||
unsigned short int
| ||||||
int
|
int
|
mindestens 16 |
16 | 32 | 32 | 32 |
signed
| ||||||
signed int
| ||||||
unsigned
|
unsigned
| |||||
unsigned int
| ||||||
long
|
long
|
mindestens 32 |
32 | 32 | 32 | 64 |
long int
| ||||||
signed long
| ||||||
signed long int
| ||||||
unsigned long
|
unsigned long
| |||||
unsigned long int
| ||||||
long long
|
long long
|
mindestens 64 |
64 | 64 | 64 | 64 |
long long int
| ||||||
signed long long
| ||||||
signed long long int
| ||||||
unsigned long long
|
unsigned long long
| |||||
unsigned long long int
|
Alle Schreibweisen sind identisch zu dem jeweils zugeordneten Typ. Die Schreibweise, die in der Typ-Spalte verwendet wird, ist die in diesem Buch genutzte Schreibweise. In der Praxis finden sie aber, je nachdem wer den Code geschrieben hat, auch die äquivalenten Schreibweisen der ersten Spalte.
Die genaue Anzahl der Bits hängt von der Implementierung ab und wird gemeinhin als data model bezeichnet. Vier data models sind weit verbreitet:
- 32 Bit Systeme
- LP32 oder 2/4/4 (
int
hat 16 Bits,long
und Zeiger haben 32 Bits)- Win16 API
- ILP32 oder 4/4/4 (
int
,long
und Zeiger haben 32 Bits)- Win32 API
- Unix und Unixoide Systeme (Linux, Mac OS X)
- LP32 oder 2/4/4 (
- 64 Bit Systeme
- LLP64 oder 4/4/8 (
int
undlong
haben 32 Bits, Zeiger haben 64 Bits)- Win64 API
- LP64 oder 4/8/8 (
int
hat 32 Bits,long
und Zeiger haben 64 Bits)- Unix und Unixoide Systeme (Linux, Mac OS X)
- LLP64 oder 4/4/8 (
Der Wertebereich von vorzeichenbehafteten Typen (»signed
«) berechnet sich durch:
Für vorzeichenlose Typen (»unsigned
«) berechnet er sich durch:
Auffällig sind in der Tabelle die beiden char
-Datentypen. Im Gegensatz zu den anderen Datentypen gibt es hier keine Schreibweise, in der am Ende ein int
steht. Und was noch wichtiger ist, signed char
darf nicht mit char
abgekürzt werden! Das liegt daran, dass char
ein Datentyp für Zeichen ist, während signed char
und unsigned char
üblicherweise Zahlen repräsentieren. Historisch bedingt ist die Trennung zwischen Zeichen und Zahlen in C++ leider sehr unsauber, was sich vor allem bei den char
-Datentypen zeigt.
Sowohl signed char
, als auch unsigned char
werden bei der Ein- und Ausgabe als Zeichen behandelt, weshalb hier immer zuvor nach int
gecasted (umgewandelt, Näheres im Kapitel Casts) werden muss. Der Datentyp char
kann vom Wertebereich her je nach Compiler entweder zu signed char
oder zu unsigned char
identisch sein. Zu beachten ist jedoch, dass char
dennoch ein eigenständiger Typ ist! Für den Augenblick ist diese Unterscheidung nicht so wichtig, wir werden jedoch noch einmal auf dieses (im Kontext von Templates wichtige) Detail zu sprechen kommen.
Für den Augenblick sollten Sie sich merken: Wenn Sie eine Zahl mit einer 1-Byte-Variablen repräsentieren wollen, dann nutzen Sie signed char
oder unsigned char
, wenn Sie ein Zeichen repräsentieren wollen, verwenden Sie char
.
Wählen Sie ein int
, wenn dieser Typ alle Zahlen des nötigen Wertebereichs aufnehmen kann, bei vorzeichenlosen Zahlen verwenden Sie unsigned
. Reicht dieser Wertebereich nicht aus und ist long
größer, dann nehmen Sie long
(bzw. unsigned long
). short
und unsigned short
sollte nur Verwendung finden, wenn Speicherplatz knapp ist, etwa bei Verwendung großer Arrays, oder wenn Low-Level-Datenstrukturen festgelegter Größe benutzt werden müssen. Achten Sie darauf, dass der theoretisch größte Wert, welcher für Ihre Variable auftreten könnte, den größten möglichen Wert nicht überschreitet. Selbiges gilt natürlich auch für die Unterschreitung des kleinstmöglichen Wertes.
Ein Unter- oder Überlauf ist übrigens durchaus möglich. Die meisten Compiler bieten zwar eine Option an, um in einem solchen Fall einen Fehler zu erzeugen, aber diese Option ist standardmäßig nicht aktiv. Im folgenden kleinen Beispiel werden die Datentypen short
(min: -32768, max: 32767) und unsigned short
benutzt und bei beiden wird je ein Unter- und ein Überlauf ausgeführt:
Davon ausgehend, dass short
eine Größe von 2 Byte hat, finden je zwei Über- bzw. Unterläufe statt.
#include <iostream>
int main(){
short variable1 = 15000;
unsigned short variable2 = 15000;
std::cout << "short Variable: " << variable1 << std::endl
<< "unsigned short Variable: " << variable2 << std::endl
<< "+30000\n\n";
variable1 += 30000;
variable2 += 30000;
std::cout << "short Variable: " << variable1 << " (Überlauf)" << std::endl
<< "unsigned short Variable: " << variable2 << std::endl
<< "+30000\n\n";
variable1 += 30000;
variable2 += 30000;
std::cout << "short Variable: " << variable1 << std::endl
<< "unsigned short Variable: " << variable2 << " (Überlauf)" << std::endl
<< "-30000\n\n";
variable1 -= 30000;
variable2 -= 30000;
std::cout << "short Variable: " << variable1 << std::endl
<< "unsigned short Variable: " << variable2 << " (Unterlauf)" << std::endl
<< "-30000\n\n";
variable1 -= 30000;
variable2 -= 30000;
std::cout << "short Variable: " << variable1 << " (Unterlauf)" << std::endl
<< "unsigned short Variable: " << variable2 << std::endl;
}
short Variable: 15000
unsigned short Variable: 15000
+30000
short Variable: -20536 (Überlauf)
unsigned short Variable: 45000
+30000
short Variable: 9464
unsigned short Variable: 9464 (Überlauf)
-30000
short Variable: -20536
unsigned short Variable: 45000 (Unterlauf)
-30000
short Variable: 15000 (Unterlauf)
unsigned short Variable: 15000
Verständlicher wird dieses Phänomen, wenn man die Zahlen binär (Duales Zahlensystem) darstellt. Der Einfachheit halber beginnen wir mit der Darstellung der unsigned short
Variablen, die ersten beiden Additionen von jeweils 30000:
Addition im Dualsystem mit unsigned short
als Datentyp
Rechnung 1
|0011101010011000| 15000
+ |0111010100110000| 30000
--------------------------------
Merker |111 11 |
--------------------------------
= |1010111111001000| 45000
Rechnung 2
|1010111111001000| 45000
+ |0111010100110000| 30000
--------------------------------
Merker 1|1111111 |
--------------------------------
= 1|0010010011111000| 9464
Die beiden Rechnungen weisen keinerlei Besonderheiten auf. Da nur die letzten 16 Ziffern beachtet werden (2 Byte = 16 Bit), entfällt in der zweiten Rechnung die 1 vor dem Vertikalstrich, wodurch das Ergebnis (in dezimaler Schreibweise) 9464 und nicht 75000 lautet.
Anschließend werden von der unsigned short
-Ganzzahl zwei Mal jeweils 30000 subtrahiert:
Subtraktion im Dualsystem mit unsigned short
als Datentyp
Rechnung 3
|0010010011111000| 9464
- |0111010100110000| 30000
----------------------------------
Merker...1|1111111 |
----------------------------------
=...1|1010111111001000| 45000
Rechnung 4
|1010111111001000| 45000
- |0111010100110000| 30000
----------------------------------
Merker |111 11 |
----------------------------------
= |0011101010011000| 15000
In diesem Fall ist die zweite Rechnung unauffällig. In der ersten Rechnung wird hingegen eine große Zahl von einer kleineren abgezogen, was zu einem negativen Ergebnis führt – oder besser – führen würde, denn die Untergrenze ist in diesem Fall 0. Dort wo die 3 Punkte stehen, folgt eine unendliche Anzahl von Einsen. Dies ist keineswegs nur bei Dualzahlen der Fall. Wenn Sie im dezimalen System eine große Zahl von einer kleineren nach den üblichen Regeln der schriftlichen Subtraktion abziehen, so erhalten Sie ein ähnliches Ergebnis:
24 - 31 ------------ Merker...1 ------------ =...993
Im Programm werden mit der Vorzeichen-behafteten Ganzzahl, der short
-Variablen, ebenfalls vier Rechnungen durchgeführt. Deren duale Darstellung wird Ihnen sehr bekannt vorkommen:
Addition im Dualsystem mitshort
als Datentyp Rechnung 1 |0|011101010011000| 15000 + |0|111010100110000| 30000 -------------------------------- Merker |1|11 11 | -------------------------------- = |1|010111111001000| -20536 Rechnung 2 |1|010111111001000| -20536 + |0|111010100110000| 30000 -------------------------------- Merker 1|1|111111 | -------------------------------- = 1|0|010010011111000| 9464 Subtraktion im Dualsystem mitshort
als Datentyp Rechnung 3 |0|010010011111000| 9464 - |0|111010100110000| 30000 ---------------------------------- Merker...1|1|111111 | ---------------------------------- =...1|1|010111111001000| -20536 Rechnung 4 |1|010111111001000| -20536 - |0|111010100110000| 30000 ---------------------------------- Merker |1|11 11 | ---------------------------------- = |0|011101010011000| 15000
Die dualen Ziffern sind die gesamte Zeit über exakt die gleichen, der einzige Unterschied besteht darin, dass negative Zahlen in Zweierkomplement-Darstellung repräsentiert werden. Dies führt zu einer veränderten Darstellung im Dezimalsystem.
Meist ist es besser, wenn man die genaue Größe der Datentypen festlegt. Hierfür muss der Header cstdint
eingebunden werden. Darin sind Alias-Namen für die eingebauten Datentypen definiert:
std::int8_t
optionalstd::int16_t
optionalstd::int32_t
optionalstd::int64_t
optionalstd::int_fast8_t
std::int_fast16_t
std::int_fast32_t
std::int_fast64_t
std::int_least8_t
std::int_least16_t
std::int_least32_t
std::int_least64_t
std::intmax_t
std::intptr_t
optionalstd::uint8_t
optionalstd::uint16_t
optionalstd::uint32_t
optionalstd::uint64_t
optionalstd::uint_fast8_t
std::uint_fast16_t
std::uint_fast32_t
std::uint_fast64_t
std::uint_least8_t
std::uint_least16_t
std::uint_least32_t
std::uint_least64_t
std::uintmax_t
std::uintptr_t
optional
Alle Datentypen, die mit int
beginnen, stehen für signed
Datentypen, alle, die mit uint
beginnen, für unsigned
Datentypen. Die mit »optional« gekennzeichneten Datentypen sind nur definiert, wenn sie durch die Plattform auch nativ unterstützt werden. Dies ist jedoch auf den schon oben referenzierten Systemen immer der Fall. Lediglich auf Mikrocontrollern oder ähnlichem kann es hier zu Problemen kommen.
Die least
-Datentypen entsprechen dem kleinsten Typ, der mindestens so viele Bits hat, die fast
-Datentypen dem schnellsten Datentyp, der mindestens so viele Bits hat. Die max
-Datentypen entsprechen dem jeweils größten verfügbaren Datentyp. Die ptr
-Datentypen haben exakt so viele Bit wie ein Zeiger (siehe Kapitel Zeiger) und sind daher ebenfalls nur verfügbar, wenn die Hardware einen entsprechenden Integer-Datentyp nativ unterstützt.
In der Regel ist es am sinnvollsten die exakten Datentypen zu verwenden, also etwa std::uint16_t
, wenn man einen Datentyp möchte, der 16 Bit hat und vorzeichenlos ist. Die fast
-Datentypen können nötigenfalls zur Geschwindigkeitsoptimierung innerhalb von Funktionen eingesetzt werden.
Gleitkommazahlen
[Bearbeiten]Eine Gleitkommavariable kann sich eine bestimmte Anzahl Ziffern merken und dazu die Position des Kommas. Das Wissen über den internen Aufbau einer solchen Zahl werden Sie wahrscheinlich eher selten bis nie brauchen, daher sei an dieser Stelle auf den Wikipediaartikel über Gleitkommazahlen verwiesen. In C++ werden Sie Gleitkommazahlen/-variablen für das Rechnen mit Kommazahlen verwenden. Es gibt drei Datentypen für Gleitkommazahlen, die in der folgenden Tabelle mit ihren üblichen Werten aufgelistet sind:
Typ | Speicherplatz | Wertebereich | kleinste positive Zahl | Genauigkeit |
---|---|---|---|---|
float |
4 Byte | 6 Stellen | ||
double |
8 Byte | 12 Stellen | ||
long double |
10 Byte | 18 Stellen |
Die Auswahl eines Gleitkommadatentyps ist weniger einfach als die einer Ganzzahl. Wenn Sie nicht genau wissen, was Sie nehmen sollen, ist double
in der Regel eine gute Wahl. Sobald Sie erst einmal ausreichend Erfahrung haben, wird es Ihnen leichter fallen abzuschätzen, ob float
oder long double
für Ihr Problem vielleicht eine bessere Wahl ist.
Variablen
[Bearbeiten]Bevor eine Variable verwendet werden kann, muss sie dem Compiler bekannt gegeben werden. Dies bezeichnet man als Deklaration der Variablen. Das eigentliche Anlegen einer Variablen, so dass der Compiler Speicherplatz für sie reserviert, wird Definition genannt. Eine Definition ist immer auch eine Deklaration und bei Variablen ist der Unterschied zwischen Deklaration und Definition etwas zu kompliziert, um ihn an dieser Stelle bereits zu erklären. Sie werden die Begriffe zunächst in Zusammenhang mit Funktionen kennenlernen und später auch für Variablen. Sie sollten sich jedoch jetzt bereits merken, dass es sich beim Anlegen der Variablen, die wir verwenden, immer um Definitionen handelt. Dies ist insofern wichtig, als dass eine Definition immer nur einmal geschrieben werden darf, während eine Deklaration beliebig oft vorgenommen werden kann. Um einen Vergleich zur realen Welt zu ziehen, wollen wir das Entstehen neuen Lebens betrachten. Sie können beliebig oft erzählen, dass ein bestimmtes Kind geboren wird. Tatsächlich geschehen kann dies aber nur einmal. Im vorherigen Kapitel haben wir bereits mit Variablen vom Typ int
gerechnet. Nun sollen Sie lernen, wie Variablen in C++ angelegt werden. Die allgemeine Syntax lautet:
Außerdem ist es möglich, mehrere Variablen des gleichen Typs hintereinander anzulegen:
Auch kann man einer Variablen einen Anfangswert geben, dies bezeichnet man als Initialisierung. Es gibt zwei syntaktische Möglichkeiten (Schreibweisen) für Initialisierungen, welche anhand einer int
Variable gezeigt werden soll:
Die erste Variante ist weit verbreitet aber nicht zwingend besser. Bei den fundamentalen Datentypen von C++ spielt es keine Rolle, welche Variante Sie verwenden, aber bei komplexeren Datentypen (Klassen) kann es zu Verwechslungen mit dem Zuweisungsoperator kommen, wenn Sie Möglichkeit 1 benutzen. Den genauen Unterschied zwischen einer Initialisierung und einer Zuweisung werden Sie kennenlernen, sobald es um Klassen geht. Für den Moment sollten Sie sich für eine der beiden Varianten entscheiden. Für Möglichkeit 1 spricht die große Verbreitung und die damit verbundene intuitive Nutzung. In diesem Buch werden wir diese erste Methode verwenden und in Zusammenhang mit Klassen auch eine Empfehlung geben, wann diese Methode zur besseren Übersicht im Quellcode beitragen kann. Für Möglichkeit 2 spricht hingegen, dass diese Syntax für Initialisierungen immer gültig ist. Nebenbei wird bei dieser Methode deutlich, dass es sich um eine Initialisierung handelt.
Variablen mit Anfangswerten können natürlich auch hintereinander angelegt werden, sofern sie den gleichen Datentyp besitzen, allerdings ist davon aus Gründen der Übersichtlichkeit abzuraten.
Wenn Sie einer Variablen keinen Anfangswert geben, müssen Sie ihr später im Programm noch einen Wert zuweisen, bevor Sie mit ihr arbeiten (also damit rechnen oder den Inhalt ausgeben lassen). Weisen Sie einer solchen Variablen keinen Wert zu und benutzen sie, so kann der Inhalt zufällig sein. Genaugenommen handelt es sich dann um die Bitfolge, die an der Stelle im Speicher stand, an der Ihre Variable angelegt wurde. Es gibt in C++ Regeln, in welchen Fällen der Compiler eine Variable ohne explizite Initialisierung implizit mit 0 initialisiert und wann stattdessen einfach der aktuelle Speicherinhalt stehen bleibt. Allerdings sind diese Regeln so kompliziert, dass es sich nicht lohnt, sie sich zu merken. Denn sollte je ein anderer Programmierer Ihren Code lesen, so muss auch dieser die Regeln kennen, um den Code sofort verstehen zu können. Das nachfolgende kleine Programm zeigt einen Fall, in dem C++ besagt, dass keine implizite Initialisierung mit 0 stattfindet.
#include <iostream> // Ein-/Ausgabe
int main(){
int zahl; // Ganzzahlige Variable
double kommazahl1, kommazahl2; // Gleitkommavariablen
char zeichen; // Zeichenvariable
std::cout << "zahl: " << zahl << std::endl // Ausgabe der Werte
<< "kommazahl1: " << kommazahl1 << std::endl // welche jedoch
<< "kommazahl2: " << kommazahl2 << std::endl // nicht festgelegt
<< "zeichen: " << zeichen << std::endl; // wurden
}
zahl: -1211024315
kommazahl1: 4.85875e-270
kommazahl2: -3.32394e-39
zeichen: f
Die Ausgabe kann bei jedem Ausführen des Programms anders lauten. Sollte dies bei Ihnen nicht der Fall sein, so stehen nur zufällig die gleichen Werte an der Stelle im Speicher, welchen die jeweilige Variable belegt. Spätestens nach einem Neustart Ihres Rechners haben Sie höchstwahrscheinlich eine andere Ausgabe. Variablen keinen Anfangswert zu geben, ist beispielsweise sinnvoll, wenn Sie vorhaben, über std::cin
einen Wert in die Variable einzulesen. Dennoch würde es auch in diesem Fall keinen Schaden anrichten, wenn Sie die Variablen explizit mit 0 initialisieren.
#include <iostream> // Ein-/Ausgabe
int main(){
int zahl; // Ganzzahlige Variable
double kommazahl1, kommazahl2; // Gleitkommavariablen
char zeichen; // Zeichenvariable
std::cout << "Geben Sie bitte durch Leerzeichen getrennt eine Ganzzahl, zwei Kommazahlen "
"und ein Zeichen ein:\n";
std::cin >> zahl // Eingabe von Werten
>> kommazahl1 // mit denen die vier
>> kommazahl2 // Variablen gefüllt
>> zeichen; // werden
std::cout << "Zahl: " << zahl << std::endl // Ausgabe der Werte
<< "Kommazahl1: " << kommazahl1 << std::endl // welche zuvor
<< "Kommazahl2: " << kommazahl2 << std::endl // eingegeben
<< "Zeichen: " << zeichen << std::endl; // wurden
}
Geben Sie bitte durch Leerzeichen getrennt eine Ganzzahl, zwei Kommazahlen und ein Zeichen ein:
Benutzereingabe: 6 8.4 6.0 g
Zahl: 6
Kommazahl1: 8.4
Kommazahl2: 6
Zeichen: g
Konstanten
[Bearbeiten]Konstanten sind, wie schon oben beschrieben, Variablen, welche ihren Wert nicht verändern. Daraus folgt, dass einer Konstanten nur genau ein Mal ein Wert zugewiesen werden kann; in C++ muss dies das Initialisieren mit einem Anfangswert sein, andernfalls hätten Sie eine Konstante mit einem zufälligen Wert und das ergibt kaum einen Sinn. Das Schlüsselwort, um eine Variable zu einer Konstanten zu machen, ist const
. Es gehört immer zu dem, was links davon steht, es sei denn, links von ihm steht nichts mehr, dann gehört 'const' zu dem Begriff auf dessen rechter Seite. Dies klingt zwar kompliziert, ist es aber eigentlich gar nicht. Für uns bedeutet es im Moment nur, dass Sie zwei Möglichkeiten haben, eine Variable zu einer Konstanten zu machen:
const int zahl(400); // Alternativ: const int zahl=400;
// oder
int const zahl(400); // Alternativ: int const zahl=400;
Beides hat die gleiche Wirkung, wieder ist die erste Variante weit verbreitet und wieder ist die zweite Variante der besseren Lesbarkeit bei komplexeren Datentypen (Arrays von Zeigern Konstante auf Memberfunktionen…) vorzuziehen. Entscheiden Sie sich für die Variante, die Ihnen besser gefällt und verwenden Sie diese. Wichtig ist, dass Sie der Variante, für die Sie sich entscheiden, treu bleiben, wenigstens für die Dauer eines Projekts. Denn Code, in dem sich der Schreibstil ständig ändert, ist schwieriger zu lesen.
Literale und ihre Datentypen
[Bearbeiten]Ein Literal ist eine Angabe im Quellcode, die einen konkreten Wert angibt und einen der oben beschriebenen Datentypen besitzt.
Bei der Ausgabe ist zu beachten, dass Boolean-Werte als 0 (false) bzw. 1 (true) ausgegeben werden.
Automatische Bestimmung des Datentyps
[Bearbeiten]Da die Bestimmung des Typs für einen ganzzahligen Wert etwas schwieriger ist als bei den übrigen, werden wir diese zuletzt behandeln. Bei Gleitkommaliteralen ist festgelegt, dass es sich um double
-Werte handelt. Um einen Gleitkommaliteral mit einem anderen Typ zu erhalten, ist ein so genanntes Suffix nötig.
Eine Zahl mit Komma (.
) ist also ein double
-Wert. Folgt der Zahl ein f
oder ein F
wird sie zu einem float
-Wert und folgt ihr ein l
oder ein L
wird sie zu einem long double
-Wert. Gleitpunktzahlen können auch in der wissenschaftlichen Schreibweise dargestellt werden.
Wie die letzten beiden Beispiele zeigen, können auch hierbei die Suffixe für den Datentyp genutzt werden.
Um ein Zeichen beziehungsweise eine Zeichenkette als char16_t
bzw. char32_t
zu kennzeichnen, stellt man ein kleines u
bzw. großes U
voran. Für wchar_t
nutzt man ein großes L
.
'a' // char
u'b' // char16_t
U'b' // char32_t
L'b' // wchar_t
"Ich bin ein Text" // char const*
u"Ich bin ein Text" // char16_t const*
U"Ich bin ein Text" // char32_t const*
L"Ich bin ein Text" // wchar_t const*
Was das Sternchen (*
) hinter dem Datentyp im Kommentar bedeutet, werden Sie in einem späteren Kapitel erfahren. bool
kann nur zwei Zustände annehmen, entsprechend gibt es auch nur zwei bool
-Literale: true
und false
.
Nun zu den Ganzzahlen. Neben der dezimalen Darstellung von Zahlen gibt es in C++ auch die Möglichkeit der Binären, Oktalen und Hexadezimalen Darstellung. Um eine Zahl als Binär zu kennzeichnen, wird ein 0b
oder 0B
vorangestellt, für Oktal wird nur eine 0
(Null) vorangestellt und für eine Hexadezimalzahl wird 0x
oder 0X
vorangestellt. Die Groß-/Kleinschreibung der hexadezimalen Ziffern a
bis f
spielt keine Rolle.
756 // Dezimal, Dezimal: 756
0b10 // Binär, Dezimal: 2
0B111 // Binär, Dezimal: 7
046 // Oktal, Dezimal: 38
0757 // Oktal, Dezimal: 495
0xffff // Hexadezimal, Dezimal: 65535
0X1234ABcd // Hexadezimal, Dezimal: 305441741
Der Datentyp wird durch die Größe des Wertes bestimmt, wobei die folgende Reihenfolge gilt: int
, unsigned int
, long
, unsigned long
, long long
, unsigned long long
. Weiterhin kann jeder Ganzzahl auch explizit das Suffix u
oder U
für unsigned
und ein l
oder L
für long
bzw. ein ll
oder LL
für long long
angehängt werden. Die Reihenfolge ändert sich entsprechend den durch die Suffixe festgelegten Kriterien.
übersichtlichere int-Schreibweise
[Bearbeiten]Es ist außerdem möglich, Integer-Literale an beliebigen Stellen durch das Zeichen '
zu trennen, um die Übersicht zu verbessern. Dies kann insbesondere bei binären Literalen nützlich sein, da diese oft sehr lang sind.
9'756'432'108 // ' als Tausender-Trennzeichen
978'3'446'43981'8 // ' als Trennzeichen für eine ISBN-Nummer
0B1111'0000'1010'0101 // ' als Trennzeichen für einen 16 Bit Wert mit 4 Gruppen
Das Wissen über die Datentypen von Literalen werden Sie wahrscheinlich eher selten benötigen, daher reicht es „mal etwas davon gehört zu haben“ und es, wenn nötig, nachzuschlagen.
Rechnen mit unterschiedlichen Datentypen [Bearbeiten]
Sie kennen nun die Datentypen in C++ und haben auch schon mit int
-Variablen gerechnet. In diesem Kapitel erfahren Sie, wie man mit Variablen unterschiedlichen Typs rechnet. Es geht also weniger um das Ergebnis selbst, als viel mehr darum, wie der Ergebnisdatentyp lautet.
Ganzzahlen
[Bearbeiten]Das Rechnen mit Ganzzahlen ist leicht zu begreifen. Die „kleinen“ Datentypen werden als int
behandelt. Bei den größeren entscheidet der größte Datentyp über den Ergebnistyp. Die folgende Liste zeigt die Zusammenhänge:
char + char => int | wchar_t + char => int char + wchar_t => int | wchar_t + wchar_t => int char + signed char => int | wchar_t + signed char => int char + unsigned char => int | wchar_t + unsigned char => int char + short => int | wchar_t + short => int char + unsigned short => int | wchar_t + unsigned short => int char + int => int | wchar_t + int => int char + unsigned int => unsigned int | wchar_t + unsigned int => unsigned int char + long => long | wchar_t + long => long char + unsigned long => unsigned long | wchar_t + unsigned long => unsigned long signed char + char => int | unsigned char + char => int signed char + wchar_t => int | unsigned char + wchar_t => int signed char + signed char => int | unsigned char + signed char => int signed char + unsigned char => int | unsigned char + unsigned char => int signed char + short => int | unsigned char + short => int signed char + unsigned short => int | unsigned char + unsigned short => int signed char + int => int | unsigned char + int => int signed char + unsigned int => unsigned int | unsigned char + unsigned int => unsigned int signed char + long => long | unsigned char + long => long signed char + unsigned long => unsigned long | unsigned char + unsigned long => unsigned long short + char => int | unsigned short + char => int short + wchar_t => int | unsigned short + wchar_t => int short + signed char => int | unsigned short + signed char => int short + unsigned char => int | unsigned short + unsigned char => int short + short => int | unsigned short + short => int short + unsigned short => int | unsigned short + unsigned short => int short + int => int | unsigned short + int => int short + unsigned int => unsigned int | unsigned short + unsigned int => unsigned int short + long => long | unsigned short + long => long short + unsigned long => unsigned long | unsigned short + unsigned long => unsigned long int + char => int | unsigned int + char => unsigned int int + wchar_t => int | unsigned int + wchar_t => unsigned int int + signed char => int | unsigned int + signed char => unsigned int int + unsigned char => int | unsigned int + unsigned char => unsigned int int + short => int | unsigned int + short => unsigned int int + unsigned short => int | unsigned int + unsigned short => unsigned int int + int => int | unsigned int + int => unsigned int int + unsigned int => unsigned int | unsigned int + unsigned int => unsigned int int + long => long | unsigned int + long => long oder unsigned long int + unsigned long => unsigned long | unsigned int + unsigned long => unsigned long long + char => long | unsigned long + char => unsigned long long + wchar_t => long | unsigned long + wchar_t => unsigned long long + signed char => long | unsigned long + signed char => unsigned long long + unsigned char => long | unsigned long + unsigned char => unsigned long long + short => long | unsigned long + short => unsigned long long + unsigned short => long | unsigned long + unsigned short => unsigned long long + int => long | unsigned long + int => unsigned long long + unsigned int => unsigned long | unsigned long + unsigned int => unsigned long long + long => long | unsigned long + long => unsigned long long + unsigned long => unsigned long | unsigned long + unsigned long => unsigned long
Zugegebenermaßen wirkt dies erst einmal erschlagend aber es ist eigentlich nicht schwierig zu begreifen. Bei jeder Rechenoperation hat jeder der 2 Operanden, sowie das Ergebnis der Rechnung, einen Datentyp:
#include <iostream>
int main(){
char zahl1=22;
short zahl2=40;
std::cout << zahl1 * zahl2 << std::endl; // 22 * 40 = 880
// char + short => int
}
Gleitkommarechnen
[Bearbeiten]Beim Rechnen mit Gleitkommazahlen gelten im Grunde die gleichen Regeln wie bei Ganzzahlen. Der Ergebnistyp entspricht auch hier dem des Operanden mit dem „größeren“ Typ. Die aufsteigende Reihenfolge lautet: float
, double
, long double
. Es gilt also:
float + float => float float + double => double float + long double => long double double + float => double double + double => double double + long double => long double long double + float => long double long double + double => long double long double + long double => long double
Casting
[Bearbeiten]Casting bedeutet in diesem Zusammenhang die Umwandlung eines Datentyps in einen anderen. Diese Typumwandlung kann sowohl automatisch (implizit) stattfinden, als auch vom Programmierer angegeben (explizit) werden.
Implizite Typumwandlung
[Bearbeiten]Mit impliziter Typumwandlung hatten Sie bereits reichlich zu tun, denn es kann ausschließlich mit Zahlen gerechnet werden, die den gleichen Typ besitzen.
Beispiele:
char + int => int | int + int => int short + unsigned int => unsigned int | unsigned int + unsigned int => unsigned int float + double => double | double + double => double
Umformungsregeln
[Bearbeiten]Viele binäre Operatoren, die arithmetische oder Aufzählungsoperanden erwarten, verursachen Umwandlungen und ergeben Ergebnistypen auf ähnliche Weise. Der Zweck ist, einen gemeinsamen Ergebnistyp zu finden. Dieses Muster wird "die üblichen arithmetischen Umwandlungen" genannt, die folgendermaßen definiert sind:
„Gleitkomma geht vor“:
- Wenn ein Operand vom Typ
long double
ist, dann wird der andere zulong double
konvertiert. - Andernfalls, wenn ein Operand vom Typ
double
ist, dann wird der andere zudouble
konvertiert. - Andernfalls, wenn ein Operand vom Typ
float
ist, dann wird der andere zufloat
konvertiert.
Ist kein Gleitkommatyp beteiligt, dann werden folgende Ganzzahl-Umwandlungen auf beide Operanden angewendet:
- Wenn ein Operand vom Typ
unsigned long
ist, dann wird der andere zuunsigned long
konvertiert. - Andernfalls, wenn ein Operand vom Typ
long
und der andere vom Typunsigned int
, dann wird, falls einlong
alle Werte einesunsigned int
darstellen kann, derunsigned int
-Operand zulong
konvertiert; andernfalls werden beide Operanden zuunsigned long
konvertiert. - Andernfalls, wenn ein Operand vom Typ
long
ist, dann wird der andere zulong
konvertiert. - Andernfalls, wenn ein Operand vom Typ
unsigned int
ist, dann wird der andere zuunsigned int
konvertiert.
Hinweis: Der einzig verbleibende Fall ist, dass beide Operanden vom Typ int sind.
Diese Regeln wurden so aufgestellt, dass dabei stets ein Datentyp in einen anderen Datentyp mit "größerem" Wertebereich umgewandelt wird. Das stellt sicher, dass bei der Typumwandlung keine Wertverluste durch Überläufe entstehen. Es können allerdings bei der Umwandlung von Ganzzahlen in float
-Werte Rundungsfehler auftreten:
Für die Berechnung werden zunächst beide Operanden in den Datentyp float
konvertiert und anschließend addiert. Das Ergebnis ist wiederum ein float
und somit aber nicht in der Lage, Zahlen in der Größenordnung von 17 Millionen mit der nötigen Genauigkeit zu speichern, um zwischen 17000000 und 17000001 zu unterscheiden. Das Ergebnis der Addition ist daher wieder 17000000.
Explizite Typumwandlung
[Bearbeiten]In C++ gibt es dafür zwei Möglichkeiten. Zum einen den aus C übernommenen Cast (Typ)Wert
und zum anderen die vier (neuen) C++ Casts.
static_cast< Zieltyp >(Variable)
const_cast< Zieltyp >(Variable)
dynamic_cast< Zieltyp >(Variable)
reinterpret_cast< Zieltyp >(Variable)
Die Leerzeichen zwischen dem Zieltyp und den spitzen Klammern sind nicht zwingend erforderlich, Sie sollten sich diese Notation jedoch angewöhnen. Speziell wenn Sie später mit Templates oder Namensräumen arbeiten, ist es nützlich, Datentypen ein wenig von ihrer Umgebung zu isolieren. Sie werden an den entsprechenden Stellen noch auf die ansonsten möglichen Doppeldeutigkeiten hingewiesen.
Im Moment benötigen Sie nur den static_cast
. Was genau die Unterschiede zwischen diesen Casts sind und wann man welchen einsetzt, erfahren Sie im Kapitel Casts. Auf C-Casts wird in diesem Kapitel ebenfalls eingegangen, merken Sie sich jedoch schon jetzt, dass Sie diese nicht einsetzen sollten. Natürlich müssen Sie sie als C++-Programmierer dennoch kennen, falls Sie einmal auf einen solchen stoßen sollten.
Ganzzahlen und Gleitkommazahlen
[Bearbeiten]Wird mit einer Ganzzahl und einer Gleitkommazahl gerechnet, so ist das Ergebnis vom gleichen Typ wie die Gleitkommazahl.
Rechnen mit Zeichen
[Bearbeiten]Mit Zeichen zu rechnen, ist besonders praktisch. Um beispielsweise das gesamte Alphabet auszugeben, zählen Sie einfach vom Buchstaben 'A'
bis einschließlich 'Z'
:
Für eine Erklärung des obigen Quellcodes lesen Sie bitte das Kapitel Schleifen.
Wenn Sie binäre Operatoren auf Zeichen anwenden, ist das Ergebnis (mindestens) vom Typ int
. Im folgenden Beispiel wird statt eines Buchstabens der dazugehörige ASCII-Wert ausgegeben. Um also wieder ein Zeichen auszugeben, müssen Sie das Ergebnis wieder in den Zeichentyp casten. (Beachten Sie im folgenden Beispiel, dass die Variable i
– im Gegensatz zum vorherigen Beispiel – nicht vom Typ char
ist):
#include <iostream>
int main(){
char zeichen = 'A';
for(int i = 0; i < 26; ++i){
std::cout << zeichen + i << ' '; // Ergebnis int
}
std::cout << std::endl;
for(int i = 0; i < 26; ++i){
std::cout << static_cast< char >(zeichen + i); // Ergebnis char
}
}
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Verzweigungen [Bearbeiten]
Eine Verzweigung (bedingte Anweisung, conditional statement) dient dazu, ein Programm in mehrere Pfade aufzuteilen. Beispielsweise kann so auf Eingaben des Benutzers reagiert werden. Je nachdem, was der Benutzer eingibt, ändert sich der Programmablauf.
Falls
[Bearbeiten]Verzweigungen werden mit dem Schlüsselwort if
begonnen. In der einfachsten Form sieht das so aus:
Wenn die Bedingung
erfüllt ist, wird die Anweisung
ausgeführt, ansonsten wird sie übersprungen. Sollen nicht nur eine, sondern mehrere Anweisungen ausgeführt werden, fassen Sie diese mit {...}
zu einer Blockanweisung zusammen:
Als Bedingung
darf jeder Ausdruck verwendet werden, der einen bool
zurückgibt oder dessen Ergebnis sich in einen bool
umwandeln lässt. Ganzzahlige und Gleitkommadatentypen lassen sich nach bool
umwandeln, die Regel lautet: Ist eine Zahl (exakt) gleich 0, so wird sie als false
ausgewertet, andernfalls als true
.
Andernfalls
[Bearbeiten]Das Schlüsselwort else
erweitert die Einsatzmöglichkeiten der Verzweigung. Während ein normales (also einzelnes) if
einen bestimmten Teil des Codes ausführt, falls eine Bedingung erfüllt ist, stellt else
eine Erweiterung dar, anderen Code auszuführen, falls die Bedingung nicht erfüllt ist.
int i;
cin >> i;
if (i)
cout << "Sie haben einen Wert ungleich 0 eingegeben!\n";
else
cout << "Sie haben 0 eingegeben!\n";
Natürlich könnten auch hier, sowohl für die if
-Anweisung, als auch für die else
-Anweisung, ein Anweisungsblock stehen. Wenn Sie Pascal oder eine ähnliche Programmiersprache kennen, wird Ihnen auffallen, dass auch die Anweisung vor dem else
mit einem Semikolon abgeschlossen wird. Da auf eine if
- oder else
-Anweisung immer nur eine Anweisung oder ein Anweisungsblock stehen kann, muss zwangsläufig direkt danach ein else
stehen, um dem if
zugeordnet zu werden.
Sie können in einer Verzweigungsanweisung auch mehr als zwei Alternativen angeben:
int i;
cin >> i;
if (i == 10)
cout << "Sie haben zehn eingegeben\n";
else
if (i == 11)
cout << "Sie haben elf eingegeben\n";
else
cout << "Sie haben weder zehn noch elf eingegeben\n";
Es können beliebig viele Zweige mit else if
vorkommen. Allerdings ist es üblich, eine andere Einrückung zu wählen, wenn solche „if
-else
-Bäume“ ausgebaut werden:
int i;
cin >> i;
if (i == 10)
cout << "Sie haben zehn eingegeben\n";
else if (i == 11)
cout << "Sie haben elf eingegeben\n";
else
cout << "Sie haben weder zehn noch elf eingegeben\n";
Außerdem ist es zu empfehlen, auch bei einer Anweisung einen Anweisungsblock zu benutzen. Letztlich ist die Funktionalität immer die gleiche, aber solche Blöcke erhöhen die Übersichtlichkeit und wenn Sie später mehrere Anweisungen, statt nur einer angeben möchten, brauchen Sie sich um etwaige Klammern keine Gedanken zu machen, weil sie sowieso schon vorhanden sind.
int i;
cin >> i;
if (i == 10) {
cout << "Sie haben zehn eingegeben\n";
} else if(i == 11) {
cout << "Sie haben elf eingegeben\n";
} else {
cout << "Sie haben weder zehn noch elf eingegeben\n";
}
Sie werden für die Positionierung der Klammern übrigens auch oft auf eine andere Variante treffen:
int i;
cin >> i;
if (i == 10)
{
cout << "Sie haben zehn eingegeben\n";
}
else
{
if (i == 11)
{
cout << "Sie haben elf eingegeben\n";
}
else
{
cout << "Sie haben weder zehn noch elf eingegeben\n";
}
}
Einige Programmierer finden dies übersichtlicher, für dieses Buch wurde jedoch die Variante mit den öffnenden Klammern ohne Extrazeile verwendet. Das hat den Vorteil, dass weniger Platz benötigt wird und da die Einrückung ohnehin die Zugehörigkeit andeutet, ist eine zusätzliche Kennzeichnung nicht unbedingt nötig.
Die Einrückung von Quelltextzeilen hat für den Compiler übrigens keine Bedeutung. Sie ist lediglich eine grafische Darstellungshilfe für den Programmierer. Auch die in diesem Buch gewählte Einrückungstiefe von vier Leerzeichen ist optional, viele Programmierer verwenden etwa nur zwei Leerzeichen. Andere hingegen sind davon überzeugt, dass acht die ideale Wahl ist. Aber egal, wofür Sie sich entscheiden, wichtig ist, dass Sie Ihren Stil einhalten und nicht ständig ihren Stil wechseln. Das verwirrt nicht nur, sondern sieht auch nicht schön aus.
Wenn es Sie nicht stört, in Ihrem Texteditor Tabulatorzeichen und Leerzeichen anzeigen zu lassen, dann sollten Sie für die Einrückung Tabulatorzeichen verwenden und für alles hinter der normalen Einrückung (etwa den Abstand bis zum Kommentar) Leerzeichen. Das hat den Vorteil, dass Sie die Einrückungstiefe jederzeit ändern können, indem Sie angeben wie viele Leerzeichen einem Tabulatorzeichen entsprechen.
Nachteil: Wenn Sie den Quelltext in verschiedenen Editoren bearbeiten oder weitergeben, muss in jedem Editor eingestellt werden, was die Tabulator-Breite sein soll. Beim Verwenden von Leerzeichen bleibt die Einrückung immer gleich, auch wenn die Tabulatorbreite verschieden eingestellt sein sollte.
Vergleichsoperatoren
[Bearbeiten]Im obigen Beispiel kam schon der Vergleichsoperator ==
zum Einsatz. In C++ gibt es insgesamt sechs Vergleichsoperatoren. Sie liefern jeweils den Wert true
, wenn die beiden Operanden (die links und rechts des Operators stehen) dem Vergleichskriterium genügen, ansonsten den Wert false
.
==
|
identisch |
<=
|
ist kleiner (oder) gleich |
>=
|
ist größer (oder) gleich |
<
|
ist kleiner |
>
|
ist größer |
!=
|
ist ungleich |
Der Vergleichsoperator ==
wird von Anfängern oft mit dem Zuweisungsoperator =
verwechselt. Da es absolut legal ist, eine Zuweisung innerhalb einer if
-Bedingung zu machen, führt das oft zu schwer zu findenden Fehlern. Eine Zuweisung wird ausgewertet zum zugewiesenen Wert.
Problem-Beispiel:
Die „Bedingung“ weist den Wert von b
an die Variable a
zu (a = 8
). Für das if
wird der Gesamtausdruck ausgewertet (also 8
), was true
bedeutet.
Prüfen Sie bei seltsamen Verhalten also immer, ob vielleicht der Zuweisungsoperator =
statt des Gleichheitsoperators ==
verwendet wurde.
Eine weitere Falle ist der Ungleichheitsoperator !=
, wenn er falsch herum geschrieben wird (=!
). Letzteres sind in Wahrheit zwei Operatoren, nämlich die Zuweisung =
und die logische Negierung !
, die Sie gleich kennen lernen werden. Um das zu unterscheiden, machen Sie sich einfach klar, was das in Worten heißt:
!=
– nicht gleich=!
– gleich nicht
Logische Operatoren
[Bearbeiten]Mit logischen Operatoren können Sie mehrere Bedingungen zu einem Ausdruck verknüpfen. C++ bietet folgende Möglichkeiten:
!
|
Logisches Nicht | Resultat wahr, wenn der Operand falsch ist |
&&
|
Logisches Und | Resultat wahr, wenn beide Operanden wahr sind |
||
|
Logisches Oder | Resultat wahr, wenn mindestens ein Operand wahr ist (inclusive-or) |
Die Operatoren lassen sich übersichtlich mit Wahrheitstafeln beschreiben (bei der hier gewählten Darstellung ist jede Spalte für sich zu lesen):
Logisches Und (&& )
| ||||
---|---|---|---|---|
a |
true |
true |
false |
false
|
b |
true |
false |
true |
false
|
a && b |
true |
false |
false |
false
|
Beispiel für die dritte Spalte: Mit a = false
und b = true
gilt a && b -> false
.
Logisches Oder (|| )
| ||||
---|---|---|---|---|
a |
true |
true |
false |
false
|
b |
true |
false |
true |
false
|
a || b |
true |
true |
true |
false
|
Logisches Nicht (! )
| ||
---|---|---|
a |
true |
false
|
!a |
false |
true
|
Beispiel:
Aus Gründen der Lesbarkeit sollten Vergleichsausdrücke grundsätzlich von Klammern umgeben sein. Der obige Code würde folglich so aussehen:
Sowohl beim &&
-Operatoren (Logik-und) als auch beim ||
-Operator (Logik-oder) werden die Teilausdrücke von links nach rechts bewertet, und zwar nur so lange, bis das Resultat feststeht. Wenn z. B. bei einer &&
-Verknüpfung A && B && C
schon die erste Bedingung 'A' falsch ist, werden 'B' und 'C' gar nicht mehr untersucht, da bei &&
ja alle Bedingungen true
sein müssen.
Der Rückgabewert der beiden Operatoren ist vom Typ bool
.
Gelegentlich ist daher anzutreffen:
if ( (variable_ist_gueltig) && (variable_erfuellt_zusaetzliche_detailbedingung) ) { /* mache etwas */ }
Ob die Variable die Detailbedingung erfüllt, kann nur geprüft werden, wenn sie einen gültigen Wert enthält. Die erste Bedingung auf Gültigkeit schützt somit die nachfolgende davor, mit ungültigen Werten arbeiten zu müssen.
Beachten Sie bitte, dass der UND-Operator (&&
) eine höhere Priorität als der ODER-Operator (||
) hat. Das heißt, Sie müssen bei Ausdrücken wie dem Folgenden vorsichtig sein.
int i = 10, j = 20;
// Erwartete Reihenfolge ((((i == 10) || (j == 20)) && (j == 20)) && (i == 5))
// Tatsächliche Reihenfolge ((i == 10) || (((j == 20) && (j == 20)) && (i == 5)))
if (i == 10 || j == 20 && j == 20 && i == 5) {
cout << "i ist Zehn und fünf oder (j ist Zwanzig und i fünf)!\n";
} else {
cout << "i ist nicht Zehn oder (j ist Zwanzig oder i nicht fünf)!\n";
}
i ist Zehn und fünf oder (j ist Zwanzig und i fünf)!
Die Ausgabe ist von der Logik her falsch, weil der Ausdruck in der Reihenfolge ((i == 10) || (((j == 20) && (j == 20)) && (i == 5)))
ausgewertet wird. Solche Fehler sind sehr schwer zu finden, also sollten Sie sie auch nicht machen. Daher der Tipp: Verwenden Sie bei solch komplexen Bedingungen immer Klammern, um klar zu machen, in welcher Reihenfolge Sie die Ausdrücke auswerten wollen. Das ist unmissverständlich, und der menschliche Leser liest den Ausdruck genauso wie der Compiler. Um zu erkennen, an welcher Stelle eine Klammer wieder geschlossen wird, beherrschen die meisten Editoren das sogenannte Bracket Matching. Dabei hebt der Editor (automatisch oder über einen bestimmten Hotkey) die schließende Klammer hervor.
Wenn Sie mit mehreren &&
und ||
arbeiten, dann schreiben Sie den Ausdruck, der am wahrscheinlichsten zutrifft, auch am weitesten links, also vor den anderen Ausdrücken. Das ist eine (zugegebenermaßen) sehr geringfügige Optimierung, aber es gibt Situationen in denen sie trotzdem sinnvoll ist. Beispielsweise wenn die Bedingung innerhalb einer Schleife sehr oft ausgeführt wird.
Hinweis für fortgeschrittene Leser: Beachten Sie bitte, dass für überladende Operatoren andere Regeln gelten. Alle diejenigen, die noch nicht wissen, was überladende Operatoren sind, brauchen sich um diesen Hinweis (noch) nicht zu kümmern.
Bedingter Ausdruck
[Bearbeiten]Häufig werden Verzweigungen eingesetzt, um abhängig vom Wert eines Ausdrucks eine Zuweisung vorzunehmen. Das können Sie mit dem Auswahloperator ? ... : ...
auch einfacher formulieren:
Grafisch sieht das so aus:
Der Variablen min
wird der kleinere, der beiden Werte a
und b
zugewiesen. Analog zum Verhalten der logischen Operatoren wird nur derjenige „Zweig“ bewertet, der nach Auswertung der Bedingung (a < b
) tatsächlich ausgeführt wird.
Der Bedingungsoperator kann auch wie folgt verwendet werden:
Im Gegensatz zur if...else
-Anweisung ist es hier aber nicht möglich, in Abhängigkeit zu einer Bedingung,
nur eine Anweisung auszugeben, indem man die else
-Anweisung einfach weg lässt:
Das liegt daran, dass der Bedingungsoperator ?:
keine Kontrollstruktur im eigentlichen Sinne ist.
Vielmehr wird mit (a) ? make(b) : make(c);
ein Ausdruck „berechnet“, der einen Wert aufweisen muss,
während sich die if
-Anweisung logisch in einen "Tu-nichts-Pfad" auflösen kann.
Schleifen [Bearbeiten]
Mit dem, was Sie bis jetzt gelernt haben, sollte es für Sie eine leichte Übung sein, die Zahlen von eins bis zehn ausgeben zu lassen. So könnte ein Programm aussehen, das dies tut:
Dieses Programm ist einfach – aber was wäre, wenn die Zahlen eins bis einer Million ausgegeben werden sollen? Oder schlimmer noch – ja, es geht noch schlimmer: die Ausgabe hängt von der Benutzereingabe ab. Dann müssten Sie von der größtmöglichen Zahl ausgehen (bei unsigned int
üblicherweise 4.294.967.295) und zusätzlich noch nach jeder Ausgabe überprüfen, ob die vom Benutzer eingegebene Zahl erreicht ist.
#include <iostream>
int main() {
unsigned i = 1; // 2 Variablen anlegen
unsigned benutzer = 0;
std::cin >> benutzer; // Benutzer gibt Zahl ein
if (i <= benutzer) { // Benutzereingabe erreicht?
std::cout << "1\n"; // Ausgabe der Zahl 1
++i; // Ausgegebene Zahlen mitzählen
} else {
return 0; // Anwendung beenden
}
if (i <= benutzer) { // Benutzereingabe erreicht?
std::cout << "2\n"; // Ausgabe der Zahl 2
++i; // Ausgegebene Zahlen mitzählen
} else {
return 0; // Anwendung beenden
}
// ...
if (i <= benutzer) { // Benutzereingabe erreicht?
std::cout << "4294967295\n"; // Ausgabe der Zahl 4294967295
++i; // Ausgegebene Zahlen mitzählen
} else {
return 0; // Anwendung beenden
}
// Wenn Ihr Compiler diese Stelle erreichen soll, brauchen Sie einen leistungsstarken
// Rechner und ein robustes Betriebssystem.
// Um dieses Problem zu lösen, setzen Sie sich einfach in die nächste Zeitmaschine
// und bestellen Sie sich einen Rechner aus dem Jahr 2030!
}
Insgesamt würden sich über 17,1 Milliarden Zeilen Code ergeben.
An diesem Punkt kommen Schleifen ins Spiel. Bis jetzt wurden alle Programme einfach der Reihe nach abgearbeitet und zwischendurch wurde eventuell mal verschiedenen Zweigen gefolgt. Mit einer Schleife können Sie erreichen, dass ein Programmteil mehrfach abgearbeitet wird. C++ stellt drei Schleifenkonstrukte zur Verfügung. Die kopfgesteuerte while
-Schleife, die fußgesteuerte do-while
-Schleife und die (ebenfalls kopfgesteuerte) for
-Schleife. Was kopf- und fußgesteuerte Schleife bedeutet erfahren Sie in Kürze. Vielleicht wissen Sie es aber auch schon aus dem Kapitel „Grundlegende Elemente“, aus dem Abschnitt „Für Programmieranfänger“.
Die while
-Schleife
[Bearbeiten]Eine while
-Schleife hat die folgende allgemeine Form:
Natürlich können Sie, wie bei den Verzweigungen auch, hier wieder mehrere Anweisungen zu einem Anweisungsblock zusammenfassen, da diese als eine Anweisung gilt:
Solange die Bedingung erfüllt ist, wird die Anweisung oder der Anweisungsblock ausgeführt. Da es sich hier um eine kopfgesteuerte Schleife handelt, wird erst die Bedingung ausgewertet. Ist diese erfüllt, so wird dann die Anweisung ausgeführt. Ist die nächste Überprüfung der Bedingung positiv, so wird der Schleifenrumpf erneut ausgeführt und so fort. Ist die Bedingung nicht erfüllt, wird der Schleifeninhalt übersprungen und mit dem Quelltext nach der Schleife fortgesetzt. Es kann daher vorkommen, dass der Schleifenrumpf gar nicht ausgeführt wird - wenn die Bedingung schon zu Beginn nicht erfüllt wird.
Was eine fußgesteuerte Schleife macht, erfahren Sie unter der Überschrift do-while
. Eine Gegenüberstellung der beiden Schleifen gibt es in der Zusammenfassung dieses Kapitels.
Wie Bedingungen ausgewertet werden, können Sie im Kapitel „Verzweigungen“ nachlesen - das vorherige Kapitel.
Beachten Sie, dass Schleifen so lange ausgeführt werden, bis die Bedingung nicht mehr erfüllt ist. Wenn Sie also nicht innerhalb der Schleife dafür sorgen, dass die Bedingung irgendwann nicht mehr erfüllt ist, dann haben Sie eine sogenannte Endlosschleife. Das heißt, der Schleifenrumpf (so nennt man die Anweisung oder den Anweisungsblock einer Schleife) wird immer wieder ausgeführt. Das Programm kann nur durch Abbrechen beendet werden. In Kombination mit einem Schlüsselwort, das Sie in Kürze kennen lernen werden, kann eine solche Endlosschleife durchaus gewollt und sinnvoll sein (der Befehl kann die Schleife abbrechen), aber in der Regel entsteht so etwas versehentlich. Wenn Ihr Programm also mal „abgestürzt“ ist, im Sinne von „Es reagiert nicht mehr! Wie schrecklich!“, dann haben Sie vermutlich irgendwo eine Endlosschleife eingebaut.
Nun aber zurück zu unserer Schleifenaufgabe mit 17,1 Milliarden Zeilen Code. Da Ihr Compiler immer noch nicht damit fertig ist, die oben vorgestellte Lösung zu übersetzen, versuchen wir jetzt mal das ganze mit einer while
-Schleife zu lösen.
#include <iostream>
int main() {
unsigned i = 1; // 2 Variablen anlegen
unsigned benutzer = 0;
std::cin >> benutzer; // Benutzer gibt Zahl ein
while (i <= benutzer) { // Benutzereingabe erreicht?
std::cout << i << std::endl; // Ausgabe von i und einem Zeilenumbruch
i++; // i um eins erhöhen (-> ausgegebene Zahlen mitzählen)
}
}
Nun ja, das sieht dem Programm von oben doch irgendwie ähnlich, nur die knapp 4,3 Milliarden if
-Anweisungen sind weggefallen und haben einer vom Aufbau fast identischen while
-Schleife Platz gemacht. Nun haben wir natürlich ein Problem: Ihr Superrechner aus dem Jahr 2020 ist immer noch mit der Übersetzung der ersten Programmversion beschäftigt und wird es wohl auch noch bis zu seiner Erfindung bleiben. Aber zum Glück haben Sie ja noch einen alten Rechner von 1980. Also versuchen Sie das Programm auf diesem zu übersetzen und auszuführen. Tatsächlich. Es funktioniert. Erstaunlich, dass so eine alte Kiste einen Rechner überholt, den Sie sich extra aus der Zukunft haben liefern lassen, um die maximale Leistungsstärke zu bekommen.
Wenn Sie Schwierigkeiten haben das Beispiel nachzuvollziehen, dann sehen Sie sich noch mal die Schleifen-Version für die Zahlen von 1 bis 10 an.
So sieht das Ganze schon viel kompakter und vielleicht auch übersichtlicher aus als die Version von ganz oben. Die Ausgabe dagegen ist völlig identisch. Hier wird i
am Anfang auf eins gesetzt. Somit ist die Bedingung (eins ist kleiner oder gleich zehn) erfüllt. Damit wird der Schleifenrumpf ausgeführt. „1“ wird ausgegeben. Es folgt ein Zeilenumbruch. Danach wird der Wert von i
um eins erhöht. Danach wird wieder geprüft, ob die Bedingung erfüllt ist. Da i
jetzt zwei ist, lautet die Bedingung „zwei ist kleiner oder gleich zehn“, da dies eine wahre Aussage ist, wird wieder der Schleifenrumpf ausgeführt. Das wiederholt sich bis i
Schließlich den Wert elf hat. Die Bedingung lautet dann „elf ist kleiner oder gleich zehn“, diese Aussage ist zweifellos falsch. Daher wird der Schleifenrumpf nun übersprungen und mit dem Code dahinter weitergemacht. Da in unserem Beispiel dort aber kein Code mehr folgt, wird das Programm beendet.
Zusammengefasst:
- Quelltext vor der Schleife
- Schleifen Bedingung
- Erfüllt:
- Schleifenrumpf
- weiter mit 2
- Nicht erfüllt:
- weiter mit 3
- Erfüllt:
- Quelltext nach der Schleife
Die do-while
-Schleife
[Bearbeiten]Wie versprochen lüften wir nun das Geheimnis um die fußgesteuerten Schleifen. do-while
ist eine fußgesteuerte Schleife, das heißt, als erstes wird der Schleifenrumpf ausgeführt, danach die Bedingung überprüft und dann abhängig von der Bedingung, wieder der Rumpf ausgeführt (Bedingung erfüllt) oder mit dem Quelltext nach der Schleife fortgesetzt (Bedingung nicht erfüllt). Eine fußgesteuerte Schleife zeichnet sich also dadurch aus, dass der Schleifenrumpf mindestens ein mal ausgeführt wird.
Bei dieser Schleife finden wir die obige Syntax nur selten, da der Schleifenrumpf hier zwischen den beiden Schlüsselwörtern do
und while
steht, wird fast immer ein Anweisungsblock benutzt, auch wenn nur eine Anweisung vorhanden ist. Für Ihren Compiler spielt das natürlich keine Rolle, aber für einen Menschen der den Quelltext liest, ist es übersichtlicher.
Unser anfängliches Riesenprogramm sieht mit einer do-while
Schleife so aus:
#include <iostream>
int main() {
unsigned i = 1; // 2 Variablen anlegen
unsigned benutzer = 0;
std::cin >> benutzer; // Benutzer gibt Zahl ein
do { // Schleifenanfang
std::cout << i << std::endl; // Ausgabe von i
++i; // Ausgegebene Zahlen mitzählen
} while (i <= benutzer); // Benutzereingabe erreicht?
}
Sie werden feststellen, dass die Ausgabe dieses Programms mit der Ausgabe in der while
-Schleifen Version übereinstimmt. Den Unterschied bemerken Sie, wenn Sie 0 eingeben: Während die while
-Version keine Ausgabe macht, gibt diese do-while
-Version „1“ und einen Zeilenumbruch aus, denn der Schleifenrumpf wird immer erst einmal ausgeführt, erst danach wird die Bedingung überprüft und entschieden ob er noch einmal ausgeführt werden muss.
Zusammengefasst:
- Quelltext vor der Schleife
- Schleifenrumpf
- Schleifen Bedingung
- Erfüllt: weiter mit 2
- Nicht erfüllt: weiter mit 4
- Quelltext nach der Schleife
Die for
-Schleife
[Bearbeiten]Die for
-Schleife ist etwas komplexer als die vorherigen beiden Schleifen. Sie gliedert sich in Teile:
for(«Initialisierungsteil»; «Bedingungsteil»; «Anweisungsteil») «Schleifenrumpf»
Der Schleifenrumpf kann wie immer eine einzelne Anweisung oder ein {}-Anweisungsblock sein. Der Bedingungsteil verhält sich genau wie bei der while
- und der do-while
-Schleife oder sagen wir fast genau so, denn einen kleinen aber feinen Unterschied gibt es doch. Während while
und do-while
immer eine Bedingung erwarten, muss bei einer for
-Schleife nicht unbedingt eine Bedingung angegeben werden. Wenn keine Bedingung angegeben ist, wird einfach angenommen, dass die Bedingung immer erfüllt ist, Sie erhalten eine Endlosschleife. Wie das sinnvoll eingesetzt wird erfahren Sie in Kürze.
Im Anweisungsteil können Sie eine beliebige Anweisung ausführen, dieser Teil wird oft verwendet, um Variablen bei jedem Schleifendurchlauf hoch oder runter zu zählen. Im nächsten Beispiel wird dies auch demonstriert. Es ist auch möglich, mehrere solcher „hoch- oder runter-Zähl“-Anweisungen durch Komma getrennt anzugeben, das wird im nächsten Beispiel aber nicht gemacht. Der Anweisungsteil wird übrigens direkt nach dem Schleifenrumpf und vor dem nächsten Bedingungstest ausgeführt. Der Initialisierungsteil ist dem Anweisungsteil dahingehend ähnlich, als dass auch hier eine beliebige Anweisung ausgeführt werden kann. Zusätzlich ist es hier aber noch möglich Variablen eines Datentyps anzulegen. Sie können also problemlos 2 int
-Variablen anlegen, aber nicht eine int
- und eine char
-Variable. Der Initialisierungsteil wird nur einmal am Beginn der Schleife ausgeführt.
Im Kapitel Lebensdauer und Sichtbarkeit von Objekten werden Sie noch etwas genaueres darüber erfahren wo Sie die im Initialisierungsteil der Schleife angelegten Variablen verwenden können. Dort werden Sie auch erfahren wie eine for
-Schleife mit Hilfe einer while
-Schleife „nachgebaut“ werden kann.
Jetzt sollten Sie sich jedoch merken, dass Sie eine solche Variable nur innerhalb der Schleife verwenden können, also nicht mehr nach ihrem Verlassen.
#include <iostream>
int main() {
unsigned benutzer = 0; // Variablen für Benutzereingabe
std::cin >> benutzer; // Benutzer gibt Zahl ein
for(unsigned i = 1; i <= benutzer; ++i) // for-Schleife
std::cout << i << std::endl; // Ausgabe von i
}
Zusammengefasst:
- Quelltext vor der Schleife
- Initialisierungsteil der Schleife
- Schleifen Bedingung
- Erfüllt:
a) Schleifenrumpf
b) Anweisungsteil
c) weiter mit 3 - Nicht erfüllt: weiter mit 4
- Erfüllt:
- Quelltext nach der Schleife
Seit C++11 gibt noch eine weitere Form der for
-Schleife. In vielen anderen Programmiersprachen nennt man diese Form die »foreach«-Schleife:
An dieser Stelle des Buches lässt sich die Funktionsweise noch schwer erklären, da die Datenstrukturen (welche hier mit «Container» angedeutet sind) noch nicht behandelt wurden. Daher wird erst später genauer auf diese Form der for
-Schleife eingegangen.
Zu erkennen ist sie daran, das es keine zwei Semikolons zwischen den Klammern gibt, sondern einen Doppelpunkt.
Die break
-Anweisung
[Bearbeiten]Jetzt ist es an der Zeit, das Geheimnis um die sinnvolle Verwendung von Endlosschleifen zu enthüllen. Die break
-Anweisung wird innerhalb von Schleifen verwendet, um die Schleife sofort zu beenden. Der Quelltext wird dann ganz normal nach der Schleife fortgesetzt. break
können Sie in jeder der drei Schleifen verwenden. Das folgende Beispiel demonstriert den Einsatz von break
, anhand unseres Lieblingsprogramms in diesem Kapitel.
#include <iostream>
int main() {
unsigned i = 1; // 2 Variablen anlegen
unsigned benutzer = 0;
std::cin >> benutzer; // Benutzer gibt Zahl ein
for(;;){ // Endlos-for-Schleife
std::cout << i << "\n"; // Ausgabe von i
++i; // Variable erhöhen
if(i > benutzer) break; // Abbrechen, wenn Bedingung erfüllt
}
}
Sie können aus jeder Schleife eine Endlosschleife machen, hier wurde die for
-Schleife gewählt um zu zeigen wie Sie ohne Bedingung aussieht. Bei einer while
- oder do-while
-Schleife könnten Sie beispielsweise true
als Bedingung angeben. Die for-Schleife legt jetzt übrigens das gleiche Verhalten an den Tag, wie eine do-while
-Schleife. Sie können auch problemlos nur einen oder zwei Teile des Schleifenkopfes bei for
-Schleife übergeben, wichtig ist nur, dass Sie die beiden Semikolons immer angeben, da Sie dem Compiler mitteilen, was welcher Teil des Schleifenkopfes ist.
Nun wissen Sie wieder etwas mehr über for
, dabei sollte es unter dieser Überschrift doch eigentlich um break
gehen. Wie Sie aber sehen, hängt letztlich alles mit allem zusammen und so ist es oft schwer eine richtige Abgrenzung zu schaffen.
Die continue
-Anweisung
[Bearbeiten]Das zweite wichtige Schlüsselwort für Schleifen ist continue
. Es wird genau so verwendet wie break
, bricht die Schleife allerdings nicht völlig ab, sondern setzt die Codeausführung am Ende des Schleifenrumpfes fort. Für while
und do-while
bedeutet das beim Bedingungstest, für die for
-Schleife beim Anweisungsteil. Mit continue
können Sie also den Rest des aktuellen Schleifendurchlaufs überspringen. Wir sehen uns das wieder anhand des Beispiels an.
#include <iostream>
int main() {
unsigned benutzer = 0; // Variablen für Benutzereingabe
std::cin >> benutzer; // Benutzer gibt Zahl ein
for(unsigned i = 1; i <= benutzer; ++i) { // for-Schleife
if (i % 2 == 0) continue; // alle geraden Zahlen überspringen
std::cout << i << std::endl; // Ausgabe von i
}
}
Hier werden nur ungrade Zahlen ausgegeben, da der %
-Operator (liefert den Rest einer Ganzzahligen Division) bei allen geraden Zahlen, (teilt man durch 2 ist als Rest ja nur 0 oder 1 möglich,) 0 zurückliefert und somit continue
ausgeführt wird.
Natürlich könnten Sie das auch noch auf eine andere Weise realisieren. Aber es gibt ja beim Programmieren viele Wege, die nach Rom führen, wie in diesem Kapitel anhand der verschieden Schleifen schon bewiesen wurde. Leider gibt es aber noch mehr Wege, die auf direktem Wege an Rom vorbeiführen... Aber hier ist für das Beispiel von eben noch ein Pfad, der sicher nach Rom führt:
#include <iostream>
int main() {
unsigned int benutzer = 0; // Variablen für Benutzereingabe
std::cin >> benutzer; // Benutzer gibt Zahl ein
for(unsigned i = 1; i <= benutzer; i += 2) // for-Schleife mit 2er Schritten
std::cout << i << std::endl; // Ausgabe von i
}
Strukturierte Programmierung
[Bearbeiten]Die Anweisungen 'break' und 'continue' gelten als „unsauber“, sie erschweren fast immer das Verständnis des Programms. Die Verwendung von 'break' ist oft ein Zeichen dafür, dass die Schleifenbedingung unvollständig ist - und man daher nicht einzig aus der Schleifenbedingung ersehen kann, wie lange die Schleife läuft. Die 'continue'-Anweisung kann meist durch eine 'if'-Anweisung ersetzt werden - oder ebenfalls durch eine exaktere Schleifenbedingung. In als „gut“ erachteten Programmen kommen 'break' und 'continue' möglichst gar nicht vor.
Kapitelanhang
[Bearbeiten]Im Anhang zu diesem Kapitel finden Sie:
- Aufgaben und zugehörige Musterlösungen.
- Am Anfang gibt der Spieler einen Zahlenbereich ein. (Zum Beispiel: 1-100)
- Der Spieler muss sich innerhalb dieses Bereiches eine Zahl merken (eingegebene Grenzzahlen sind nicht zulässig).
- Das Programm soll dann die Zahl erraten. Der Benutzer teilt dem Programm mit, ob die Zahl an die er denkt kleiner, größer oder gleich der vom Programm geratenen Zahl ist. Die kann zum Beispiel über die Eingabe von
<
,>
und=
erfolgen.
Es gibt natürlich viele Wege dieses Problem zu lösen und Sie sehen ja ob Ihr Programm funktioniert oder nicht. Hier wird nur eine Musterlösung vorgestellt, falls Sie überhaupt nicht zurechtkommen oder sich einfach dafür interessieren wie der Autor an das Problem herangegangen ist.
#include <iostream>
int main() {
int min, max; // Variablen für den möglichen Zahlenbereich
int zahl; // Zahl die der Rechner vermutet
std::cout << "Wo fängt die Zahlenreihe an?: "; // Zahlenbereich abfragen
std::cin >> min; // Benutzereingabe einlesen
std::cout << "Wo hört die Zahlenreihe auf?: "; // Zahlenbereich abfragen
std::cin >> max; // Benutzereingabe einlesen
for (char eingabe = '0'; eingabe != '=';) { // Abbrechen wenn eingabe '=' ist
zahl = min + (max - min) / 2; // Mittlere Zahl berechnen
std::cout << "Denken Sie an " << zahl << "? "; // Vermutung ausgeben
std::cin >> eingabe; // Antwort einlesen
if (eingabe == '<') // Ist die Zahl kleiner?
max = zahl; // Setzte max auf den zu großen Wert zahl
else if (eingabe == '>') // Ist die Zahl größer?
min = zahl; // Setzte min auf den zu kleinen Wert zahl
else if (eingabe != '=') // Ist Eingabe auch kein Gleichheitszeichen
std::cout << "Sie haben ein unzulässiges Zeichen eingegeben!\n"; // Fehlerhafte Eingabe melden
if (min+1 >= max) { // Keine Zahl mehr im gültigen Bereich
std::cout << "Sie sind ein Lügner!\n"; // Das Programm ist äußert entsetzt
break; // Schleife wird abgebrochen
}
}
std::cout << "Die von Ihnen gemerkte Zahl ist " << zahl << "!" << std::endl; // Ausgabe der erratenen Zahl
}
Wo fängt die Zahlenreihe an?: 0
Wo hört die Zahlenreihe auf?: 100
Denken Sie an 50? <
Denken Sie an 25? >
Denken Sie an 37? <
Denken Sie an 31? >
Denken Sie an 34? <
Denken Sie an 32? >
Denken Sie an 33? =
Die von Ihnen gemerkte Zahl ist 33!
Dies ist eine Beispielausgabe, alles nach einem Fragezeichen ist eine Benutzereingabe.
Auswahl [Bearbeiten]
Dieses Kapitel ist leider noch nicht vorhanden… | |
Wenn Sie Lust haben können Sie das Kapitel [[C++-Programmierung/ {{{Name}}}/ {{{Kapitel}}}|{{{Kapitel}}}]] selbst schreiben oder einen Beitrag dazu leisten. |
Ein Taschenrechner wird geboren [Bearbeiten]
Dieses Kapitel ist leider noch nicht vorhanden… | |
Wenn Sie Lust haben können Sie das Kapitel [[C++-Programmierung/ {{{Name}}}/ {{{Kapitel}}}|{{{Kapitel}}}]] selbst schreiben oder einen Beitrag dazu leisten. |
Zusammenfassung [Bearbeiten]
Zu diesem Abschnitt existiert leider noch keine Zusammenfassung… | |
Wenn Sie Lust haben können Sie die [[C++-Programmierung/ {{{Name}}}/ Zusammenfassung|Zusammenfassung zum Abschnitt {{{Name}}}]] selbst schreiben oder einen Beitrag dazu leisten. |