Im Folgenden wird ein Überblick über die Programmiersprache C++ gegeben.
Einer der ersten Verarbeitungsschritte, die der Compiler durchführt ist die Zerlegung der Eingabe in sogenannte Token. Während die Eingabe aus einzelnen Buchstaben besteht, sind Token das Äquivalent zu Wörtern. In C++ gibt es die folgenden Token:
Bezeichner (Identifier)
Schlüsselwort (Keyword)
Literal
Operator
Trennzeichen (Separator)
Leerzeichen, Tabulatoren, Zeilenenden und Kommentare werden ignoriert, sie trennen aber Token voneinander.
Ein Kommentar beginnt mit den Zeichen /* und endet mit
*/ und dient zum Erläutern und Beschreiben des Codes.
Kommentare werden vom Compiler ignoriert.
/*
Diese Schreibweise für einen Kommentar ermöglicht es den Text
über mehrere Zeilen zu verteilen.
*/Solche Kommentare können nicht verschachtelt werden. Darüber hinaus
lässt sich der Rest einer Zeile durch // als Kommentar
kennzeichnen.
// auskommentierte Zeile
int i; // ZählerEin Bezeichner ist eine beliebig lange Folge von Buchstaben und
Ziffern, wobei das erste Zeichen allerdings ein Buchstabe sein muss. Zu
den Buchstaben wird auch das Underscore-Zeichen "_"
gerechnet. Bei Bezeichnern wird zwischen Groß- und Kleinschreibung
unterschieden.
In C++ sind u.a. folgende Schlüsselwörter reserviert und nicht anderweitig benutzbar:
alignof const_cast goto public typedef
alignas constexpr friend protected try
and continue if register typeid
and_eq decltype inline reinterpret_cast typename
asm default int return union
auto delete long short unsigned
bitand do mutable signed using
bitor double namespace sizeof virtual
bool dynamic_cast new static void
break else noexcept static_assert volatile
case enum not static_cast wchar_t
catch explicit not_eq struct while
char export nullptr switch xor
char16_t extern operator template xor_eq
char32_t false or this
class final or_eq thread_local
compl float override throw
const for private trueBei Schlüsselwörtern wird wie bei allen Bezeichnern Groß- und Kleinschreibung unterschieden. Bei American Standard Code for Information Interchange (ASCII)-Texten werden folgende Zeichen zur Interpunktion verwendet:
! % ^ & * ( ) - + = { } | ~ [ ] \ ; ' : " < > ? , . /Außerdem folgende Zeichenkombinationen als Operatoren:
-> ++ -- .* ->* << >> <= >= == != &&
|| *= /= %= += -= <<= >>= %= ^= &= |= ::Bei Integer (ganzzahligen)-Konstanten gibt es folgende Erscheinungsformen:
Eine Zahl, die mit einer Ziffer ungleich 0 beginnt, ist eine
Dezimalkonstante, z.B. 1234 oder
3990.
Eine Zahl, die mit 0 beginnt, ist eine
Oktalkonstante, z.B. 015 oder 0377.
Eine Zahl, die mit 0x beginnt, ist eine
Hexadezimalkonstante, z.B. 0xffff oder
0x5da7.
Der Typ einer Konstanten (siehe auch weiter hinten) richtet sich nach
ihrem Wert und den entsprechenden Wertebereichen der Datentypen in der
Implementation. Durch Angabe von L oder U
hinter der Konstanten kann das aber auch explizit angegeben werden, z.B.
1999U (unsigned) oder 0xabcdL (long).
Ein Zeichen (character), eingeschlossen in einfache
Hochkommata ('x') ist eine Zeichenkonstante, nämlich der numerische Wert
des Zeichens in der internen Codierung des Rechners (meistens
ASCII). Beispiel: '0' hat den numerischen Wert 48 bei
ASCII-Codierung. Folgende Sonderzeichen werden in Character-Konstanten
verstanden:
\0 0-Byte (Stringende)
\n Zeilenvorschub (Newline)
\t Horizontaler Tabulator (Tab)
\r Zeilenende (Carriage Return)
\’ Hochkomma (Single Quote)
\" Anführungszeichen (Double Quote)
\nnn Oktaler Zeichencode
\xhhh Hexadezimaler Zeichencode
Eine Zahl mit Vorkommastellen, dem Punkt ".", Nachkommastellen und
optional der Exponentenangabe mit e und Exponent ist eine
Fließkommakonstante. Beispiel: 12.35 oder
1.602e-19 (1.602 * 10−19).
Der Typ einer solchen Konstanten ist double, durch Angabe von
F (float) oder L (long double) kann der
entsprechende Typ explizit angegeben werden, z.B.
0.25F.
Bei Gleitkommazahlen bzw. entsprechenden Ausdrücken muss entweder die
Exponentenschreibweise verwendet werden oder es muss ein Punkt enthalten
sein, sonst werden Sie als Integer-Konstante interpretiert und es kann
bei Ausdrücken zu unerwünschtem Verhalten kommen. So ergibt z.B. der
Ausdruck 1/2 als Ergebnis 0, da dies eine
Integer-Division ist. Soll eine Fließkommadivision durchgeführt werden,
so muss mindestens einer der Operanden den entsprechenden Typ haben, im
Beispiel führt 1/2.0 oder 1.0/2.0 zum
gewünschten Ergebnis 0.5.
Eine Sequenz von Zeichen, eingeschlossen in Anführungszeichen, ist
eine C-String-Konstante, z.B. "abc". Der Typ einer
C-String-Konstanten ist ein Array von char. Eine
C-String-Konstante kann daher nicht verändert werden.
Bei C-String-Konstanten können die gleichen Sonderzeichen mit
\ eingegeben werden wie bei Character-Konstanten, z.B.
"`Test.\n"'. Ein C-String wird vom Compiler durch ein
abschließendes 0-Byte gekennzeichnet, "abc" wird im
Speicher also als 'a' 'b' 'c' '\0' abgebildet.
Datentypen haben einen hohen Stellenwert in C++. Sie werden dazu benutzt, Problemstellungen genau abzubilden. Zunächst werden die fundamentalen Datentypen beschrieben, von denen alle anderen Datentypen abgeleitet sind.
char // Zeichen (als Zahl 1Byte mit oder ohne Vorzeichen, je nach Compiler)
signed char // Zeichen (als Zahl 1Byte mit Vorzeichen: -128 ... +127)
unsigned char // Zeichen (als Zahl 1Byte ohne Vorzeichen: 0 ... 255)Ob der char-Typ vorzeichenbehaftet ist oder nicht, ist
nicht standardisiert. In sehr vielen Implementationen ist er aber
signed.
short, short int // mit Vorzeichen (2Byte: -32768 ... +32767)
int // mit Vorzeichen (meist 4Byte: -21474883648 ... +21474883647)
long, long int // mit Vorzeichen (4Byte: -21474883648 ... +21474883647)Alle diese Typen gibt es auch in einer unsigned
Variante, z.B. unsigned long.
float // Einfach genaue Fließkommazahl
double // Doppelt genaue Fließkommazahl
long double // Extra genaue FließkommazahlIn den allermeisten 32-Bit-Implementierungen, gelten die in
angegebenen Werte. Bei 64-Bit-Implementierungen sind die
long-Datentypen oft 8 Bit breit mit entsprechend größerem
Wertebereich. Um unabhängig von einer speziellen Implementierung zu
sein, sollte man sich bei beim Programmieren nicht auf die angegebenen
Werte verlassen. Wenn es für die Implementierung wichtig ist, die
Grenzen zu beachten, sollten die Konstanten aus dem Header
<numeric_limits> verwendet werden, die in jeder
Situation die korrekten Grenzen zurückgeben.
| Typ | Größe (in Byte) | Wertebereich |
|---|---|---|
| Char/Integer mit Vorzeichen | ||
char |
1 | -128...127 |
short |
2 | -32768...32767 |
int |
4 | -2147483648...2147483647 |
long |
4 | -2147483648...2147483647 |
| Char/Integer ohne Vorzeichen | ||
unsigned char |
1 | 0...255 |
unsigned short |
2 | 0...65535 |
unsigned int |
4 | 0...4294967295 |
unsigned long |
4 | 0...4294967295 |
| Fließkomma | ||
float |
4 | ≈ 1.4*10-45...3.4*10+38 |
double |
8 | ≈ 4.94*10 -324...1.8*10+308 |
| Boolean | ||
bool |
1 | true, false |
Umwandlungen zwischen den Standard-Datentypen werden normalerweise implizit durchgeführt. Darüber hinaus können Umwandlungen erzwungen werden (siehe auch Typumwandlung):
int x = 5;
double y = double(x); // double y = x auch möglichDer Datentyp void ist ein fundamentaler Datentyp, von
dem keine Objekte, Arrays aus Objekten oder Referenzen erstellt werden
können. void wird benutzt, um anzugeben, dass eine Funktion
keinen Rückgabewert hat.
void f()
{
// kein return
// Funktion liefert keinen Wert.
}Logische Werte - sogenannte Wahrheitswerte - werden mit dem Typ
bool dargestellt, der die Werte true und
false annehmen kann.
bool bIsPos; // Variable mit Vorbesetzung false
bool bIsVar = true; // Variable mit Vorbesetzung true
bool bIsEmpty(); // Funktion mit boolschem Rückgabewert
bIsPos = bIsEmpty(); // Zuweisung des Funktionswertes
bIsPos = 5 < 7; // Zuweisung eines logischen Ausdrucks (hier true)Bei Deklaration von Variablen mit gleichzeitiger Initialisierung kann
man den Datentyp auch anhand des Typs der Initialisierungsvariablen
festlegen. Anstelle des Datentyps schreibt man dann
auto.
auto bIsVar = true; // Variable vom Typ bool
auto i = 5; // Variable vom Typ int
auto d = 2.0; // Variable vom Typ doubleDieses Verhalten hat einige Vorteile:
Ändert sich der Typ der rechten Seite, ändert sich auch der Datentyp der linken Seite
Die Initialisierung wird vereinfacht
Diese Festlegung des Datentyps mit auto sollte man
dennoch mit Sorgfalt verwenden. Sinnvolle Anwendungsmöglichkeiten sind
lokale Variablen, Schleifenvariablen oder Zwischenwerte für
Containerelemente.
C++ bietet folgende zusammengesetzte Datentypen, die aus den elementaren Typen gebildet werden können:
Referenzen auf Objekte oder Funktionen
Pointer auf Objekte oder Funktionen
Arrays (Felder) von Objekten
Strukturen
Konstanten (siehe const)
Klassen
Eine Referenz ist ein weiterer Name für ein bestehendes Objekt. Das heißt, bei der Erstellung einer Referenz wird das Objekt nicht kopiert und Änderungen an der Referenz ändern auch das ursprüngliche Objekt.
Für einen Datentyp Type ist Type& eine
Referenz auf ein Objekt dieses Typs. Zu beachten ist, dass Referenzen
immer initialisiert werden müssen:
int a;
// int& b; // Fehler: keine Initialisierung
int& c = a; // Gültig.
c = 7; // Auch a hat jetzt den Wert 7Auf welches Objekt eine Referenz zeigt, kann nicht geändert werden. Referenzen spielen besonders bei Funktionsargumenten und Rückgabewerten eine wesentliche Rolle.
Ein Pointer ist ein Datentyp, der die Adresse eines Objekts beinhaltet. Deklariert wird ein Pointer folgendermaßen:
Type* name;Type* ist der Datentyp "Pointer auf Type".
name enthält die Adresse eines Objektes vom Typ
Type.
Es gibt zwei prinzipielle Operationen mit Pointern:
Dereferenzieren (*)
Zugriff auf das Objekt, auf das der Pointer zeigt.
Address of (&)
Erstellung eines Pointers auf ein Objekt über seine Adresse.
Beispiel:
char c1 = 'A';
char* p = &c1; // p enthält jetzt die Adresse von c1
char c2 = *p; // c2 enthält jetzt ebenfalls 'A'
p = &c2; // p zeigt jetzt auf c2Um darzustellen, dass ein Pointer (momentan) auf kein Objekt zeigt,
kann ihm nullptr zugewiesen werden. Das ist für jeden
Pointer möglich.
char* p;
if (error)
p = nullptr; // Null-Pointer
else
p = some_function();Die Konstante NULL, wie aus C bekannt, sollte für diesen
Zweck nicht benutzt werden. Das Dereferenzieren von einem
nullptr ist ein Programmierfehler mit undefinierten
Auswirkungen.
Es gibt zwei wichtige Unterschiede zwischen Pointern und Referenzen:
Pointern können mehrmals neue Objekte zugewiesen werden (Beispiel oben).
Pointer können den speziellen Wert nullptr
haben.
In aktuellen Versionen von C++ gibt es Smartpointer, die einige Vorteile gegenüber einfachen Pointer haben (siehe Smartpointer).
Felder sind ein- oder mehrdimensionale geordnete Ansammlungen eines Datentyps, z.B. ein Feld aus Integern.
Mit der folgenden Deklaration wird eine Feld aus fünf Fließkommazahlen erstellt:
double arr[5];Über den Index-Operator wird auf einzelne Elemente zugegriffen:
double a[10]; // Ein Array mit 10 doubles, indiziert von 0..9
int k[5]; // Ein Array mit 5 ints, indiziert von 0..4
a[0] = 0.0; // Zuweisung an ein Element
a[9] = 0.9;
k[3] = 125;
a[3] = k[3];Mehrdimensionale Felder sind Felder, die wieder Felder als Elemente enthalten.
// Feld mit 10 Elementen vom Typ "Array mit 20 Element vom Typ char"
// äquivalent: Matrix mit 10 x 20 char-Elementen
char matrix[10][20];
matrix[0][0] = 'a'; // äußeres Feld zuerst indiziert
matrix[0][10] = 'b';
matrix[9][0] = 'c';Die Indizes eines mit n dimensionierten Feldes laufen immer
von 0 bis n - 1. Die Indizierung wird nicht
überprüft. Zugriff auf ein Feld außerhalb seiner Grenzen ist ein
Programmierfehler mit undefinierten Auswirkungen.
C++ stellt in der Standardbibliothek Container zur Verfügung, bei denen eine Prüfung der Grenzen erfolgt.
Der Zugriff auf Felder kann auch über Pointer erfolgen. Dabei ist
a[i] äquivalent zu *(a+i).
int* p; // Pointer auf int
int a[10]; // Array aus 10 ints
int i;
p = &(a[3]); // p zeigt jetzt auf das Element a[3]
i = *p; // *p ist der Inhalt von a[3]
p++; // p zeigt jetzt auf a[4]
*p = i; // a[4] erhält jetzt über p den Wert von iDies wird insbesondere bei der Übergabe von Feldern in Funktionen benutzt.
Im Gegensatz zu anderen Sprachen liegt die Speicherverwaltung bei C++ in der Verantwortung des Programmierers. Es gibt einige Anforderungen an die Speicherverwaltung:
Alle erstellten Objekte müssen genau einmal gelöscht werden.
Beim Zerstören müssen die Ressourcen (Speicher, offene Dateien etc.) freigegeben werden, die ein Objekt besitzt.
Gewöhnlich haben Objekte eine Lebensdauer, die durch ihren Gültigkeitsbereich bestimmt wird. Manchmal ist es jedoch sinnvoll, Objekte zu erzeugen, deren Lebensdauer vom aktuellen Gültigkeitsbereich unabhängig ist bzw. deren Art und Anzahl erst zur Ausführung des Programms bekannt ist. Dafür gibt es dynamische Speicherverwaltung.
new/deleteKlassisch gibt es dafür die Operatoren new und
delete. Objekte, die durch new angelegt
wurden, befinden sich im Freispeicher. Der Programmierer ist selbst für
die Speicherverwaltung verantwortlich, d.h. Objekte, die mit
new explizit angelegt werden, müssen auch wieder explizit
mit delete zerstört werden.
Beispiel für das Erzeugen/Zerstören eines Objekts:
int* p = nullptr;
delete p; // Zerstören mit nullptr erlaubt, aber ohne Wirkung;
p = new int(5); // Neuen Integerwert erstellen
delete p; // Zerstören des Integerwertes (Wert des Zeigers unverändert)
p = nullptr; // Zeiger wieder auf nullptr setzenBeispiel für das Erzeugen/Zerstören eines Feldes von Objekten:
char* p = new char [10]; // Feld mit 10 char-Werten
p[3] = 'c'; // Zugriff auf das 4.Zeichen
delete [] p; // Löschen des Speichers für die Integerwerte
p = nullptr; // Zeiger wieder auf nullptr setzenDer Unterschied zwischen dem Anlegen/Zerstören von Objekten und
Feldern ist unbedingt zu beachten. Wird ein delete auf ein
Feld angewendet oder ein delete[] auf ein Objekt, bedeutet
dies undefiniertes Verhalten, was meist zu einer Speicherverletzung oder
einer anderen unerwünschten Programmfunktion führt.
Der Aufruf von delete auf einen nullptr ist
unproblematisch. Daher ist es sinnvoll, Zeiger immer mit
nullptr zu initialisieren. Dynamische Objekte sollten
möglichst immer "parallel" erzeugt und zerstört werden: Entweder
Neuerstellen/Zerstören innerhalb einer Funktion oder bei Klassen
Zerstören von Objekten, die im Konstruktor erstellt wurden, im
Destruktor.
Die Speicherverwaltung wird komplexer, sobald Referenzen und Pointer
verwendet werden. Im folgenden Beispiel gibt die Funktion
badUse eine ungültige Referenz zurück. Das Objekt auf dem
Stack von badUse wurde zerstört, aber die Referenz
existiert noch. Zugriffe auf die Referenz führen dann zu
Speicherverletzungen (so genannte dangling pointer).
Daher ist darauf zu achten, dass Objekte länger als ihre
Referenzen leben.
string& badUse() {
string s = "schlecht";
return s;
}
void f() {
// Speicherverletzung: referenziertes Objekt wurde schon zerstört
//string& s = badUse();
//s.size();
}Um den String sicher zurückzugeben, kann er als Wert zurückgegeben werden. Dabei wird aber eine Kopie erstellt und nicht mit dem ursprünglichen Objekt weitergearbeitet.
string BadUse() {
string s = "schlecht";
return s;
}
void f() {
string s_stack = badUse();
string& s_ref = s_stack; // OK. Objekt lebt lang genug.
s_ref.size();
}Strukturen (struct) sind zusammengesetzte Datentypen aus
verschiedenartigen Elementen. Der Gebrauch von Strukturen soll an
folgenden Beispielen erläutert werden:
// Beispiel für eine Strukturdeklaration:
struct BirthDate
{
string name;
int year, month, day;
};
// Definition eines Objektes:
BirthDate mm;
mm.name = "Max Mueller";
mm.year = 1999;
mm.month = 2;
mm.day = 29;Strukturen sind in C++ tatsächlich Klassen mit dem einzigen Unterschied, dass der Zugriff auf die Elemente standardmäßig nicht eingeschränkt (siehe auch Beschreibung Klassen) ist. Mit dem Punktoperator kann auf die einzelnen Elemente der Struktur zugegriffen werden. Bei Pointern kann die Dereferenzierung und der Punktoperator zum Pfeiloperator zusammengezogen werden:
BirthDate mm;
BirthDate *pmm = &mm;
mm.name; // Diese Zugriffe sind gleichwertig
(*pmm).name;
pmm->name;Benutzerdefinierte Konstanten sind Werte, die einmal gesetzt und dann
nicht mehr geändert werden. Konstanten führen zu besser wartbarem Code
als direkt im Code eingesetzte Literale. Das Schlüsselwort
const kann der Deklaration eines Objekts zugefügt werden,
wobei dadurch der Typ des Elements nicht verändert wird.
Da eine Konstante nicht verändert werden darf, muss sie bei ihrer Definition initialisiert werden. Folgendes Beispiel verdeutlicht die Verwendung von benutzerdefinierten Konstanten:
Beispiel:
const double pi = 3.14159265;
const int array[] = { 0, 1, 2 ,3 };
// const int i; // Fehler, keine Initialisierung
// pi = 25.9; // Fehler, da ZuweisungBei Zeigern sind zwei Objekte beteiligt, der Zeiger und das Element
auf den der Zeiger zeigt. Wenn der Zeiger
const ist, steht das Schlüsselwort rechts
vom *. Wenn der Element const
ist, steht das Schlüsselwort links vom
*.
Beispiel:
char* c0; // Zeiger auf char
char* const c1; // Konstanter Zeiger auf char
char const* c2; // Zeiger auf konstanten char
const char* c3; // konstanter Zeiger auf (nicht konstanten) char
const char* const c1; // Konstanter Zeiger auf konstanten charMit enum wird ein neuer Datentyp definiert, der nur die
angegebenen Werte annehmen kann:
enum class Color
{
red,
yellow,
blue
};Intern ist der Datentyp als Integer repräsentiert. Bei Bedarf kann ein bestimmter Datentyp für die Repräsentation ausgewählt werden:
enum class Color
: byte
{
red,
yellow,
blue
};Wenn man keine Werte angibt, beginnt die Aufzählung bei 0 und wird
jeweils um 1 erhöht. Optional können die Werte definiert werden, wobei
fehlende Werte den nächsthöheren Wert erhalten (z.B. hier
C02 = 2):
enum class CentCoin
{
C01 = 1,
C02,
C05 = 5,
C10 = 10,
C20 = 20,
C50 = 50
};Variablen werden wie folgt angelegt und zugewiesen:
Color favColor = Color::red;
CentCoin aCoin;
aCoin = CentCoin::C50;Um mit diesen Variablen Berechnungen durchzuführen, müssen sie erst zu einem Integer-Datentyp konvertiert werden (siehe auch Typumwandlung ):
Color favColor = Color::red;
int colorCode = (int)(Color::red);
CentCoin e = CentCoin::C05;
e = (CentCoin)((int)e + 5); // e = CentCoin::C10;Die Vorteile gegenüber Integer-Konstanten sind:
Der Datentyp überprüft, dass nur definierte Werte benutzt werden (leider nicht automatisch).
Falls die Repräsentation als Integer gefordert ist, ist dies nur explizit möglich.
Größere Übersichtlichkeit, da alle Werte an einem Ort definiert sind.
Gleich benannte Werte unterschiedlicher enums kollidieren nicht:
enum class Enum0
{
Value = 0,
};
enum class Enum1
{
Value = 0,
};
Enum0 e0 = Enum0::Value;
Enum0 e1 = Enum1::Value;Schließlich können auch wie in C Konstanten mit dem Präprozessor definiert werden:
#define PI 3.14159265
#define false 0
#define true 1Die Verwendung von const ist jedoch vorzuziehen.
C++ definiert einige Konzepte die hier kurz beschrieben werden.
Eine Deklaration macht einen Namen dem Programm bekannt. Eine Deklaration ist gleichzeitig auch eine Definition, es sei denn,
sie deklariert eine Funktion ohne den Funktionsrumpf
sie enthält die extern-Spezifikation und keine
Initialisierung oder einen Funktionsrumpf
sie ist eine Deklaration eines static Mitglieds
einer Klasse
sie ist eine Deklaration eines Klassennamens
Beispiele für Definitionen:
int a;
int f(int x) { return x + a; }
struct S { int a; int b;};
enum class { up, down };Beispiele für reine Deklarationen:
extern int a;
int f(int);
struct S;Ein Gültigkeitsbereich definiert, in welchen Teilen eines Quelltextes ein Name sichtbar, d.h. verwendbar ist. In C++ gibt es folgende Arten von Gültigkeitsbereichen:
Lokal: Ein Name, der lokal in einem Block oder einer Funktion deklariert wird, kann nur dort und nicht außerhalb benutzt werden. Parameter einer Funktion werden so behandelt, als wären sie im äußeren Block der Funktion deklariert.
Global: Ein Name, deklariert außerhalb aller Blöcke und Klassen, kann überall nach seiner Deklaration benutzt werden. Diese Namen werden global genannt.
Klasse: Der Name eines Klassenmitglieds ist lokal in
dieser Klasse und kann nur von einer Memberfunktion der Klasse, über den
Punkt-Operator, den Pfeil-Operator, den Bereichsauflösungs-Operator
(::) oder von einer abgeleiteten Klasse benutzt werden.
Durch Deklaration eines gleichen Namens in einem eingeschlossenen
Block oder einer Klasse wird der "äußere" Name "versteckt". Über den
Scope-Operator "::" kann man allerdings darauf auch
zugreifen.
int x; // Global
void some_function()
{
int x; // Versteckt globale Variable
x = 1; // Zuweisung an lokale Variable
::x = 1; // Zuw. an globale Variable über Scope-Operator
}Objekte haben eine Lebensdauer, die direkt nach der Erstellung beginnt und am Ende des Blocks der Erstellung bzw. direkt vor dem Destruktor endet. Sie dürfen nur innerhalb ihrer Lebensdauer verwendet werden.
Es gibt drei fundamentale Speicherklassen in C++:
Automatischer Speicher: Hier werden lokale Variablen und Funktionsargumente abgelegt. Sie werden automatisch bei der Definition angelegt und am Ende des Gültigkeitsbereichs wieder gelöscht. Zur Laufzeit werden sie auf einer Datenstruktur abgelegt, die als Stapelspeicher bzw. Stack bezeichnet wird.
struct S {};
void func(S s0) // Lebensdauer von s0 beginnt
{
// Lebensdauer von s1 beginnt
S s1 = s0;
// Lebensdauer von s1 endet
}
// Lebensdauer von s0 endetStatischer Speicher: Globale Variablen leben von Programmbeginn bis Programmende. Sie werden in der Reihenfolge ihrer Definition erstellt.
int s0 = 10; // Lebensdauer von s0 beginnt
int main()
{
int s1 = s0 + 5; // Lebensdauer von s1 beginnt
return 0; // Lebensdauer von s1 endet
}
// Lebensdauer von s0 endetIm Beispiel wird erst die Variable s0 erstellt, dann
s1. Die Variable s1 lebt während der
main-Funktion, s0 auch während des Restes des Programms. s1
wird beim Verlassen der Funktion, s0 erst am Ende des Programms
zerstört, d.h. erst s1 und dann s0. Auch mit
static gekennzeichnete Variablen in Funktionen und Klassen
haben diese Lebensdauer.
Freispeicher/dynamischer Speicher: Diese Speicherklasse wird benutzt, wenn Speicherplatz während der Programmausführung angefordert wird. Diese Art Speicher wird auch dynamischer Speicher genannt. Die Benutzung dieser Speicherklasse wird unter SmartPointer beschrieben.
C++ beinhaltet eine Vielzahl von Operatoren, die in Ausdrücken Werte verändern können. Die folgende Tabelle enthält eine Zusammenfassung aller Operatoren in C++. Für jeden Operator ist ein gebräuchlicher Name und ein Beispiel seiner Benutzung angegeben. Die hier vorgestellten Bedeutungen treffen für fundamentale Typen zu. Zusätzlich kann man Bedeutungen für Operatoren definieren, die auf benutzerdefinierte Typen angewendet werden (siehe Operatoren überladen).
Klassenname ist der Name einer Klasse,Element ist ein Elementname,Objekt ist ein Ausdruck, der ein Objekt einer Klasse
ergibt,Zeiger ist ein Ausdruck, der einen Zeiger ergibt,Ausdruck ist ein Ausdruck undLvalue ist ein Ausdruck, der ein nichtkonstantes Objekt
bezeichnet.| Bereichsauflösung | Klassenname::Element |
| Bereichsauflösung | Namensbereichs−Name::Element |
| global | ::Name |
| global | ::qualifizierter-Name |
| Elementselektion | Objekt.Element |
| Elementselektion | Zeiger->Element |
| Indizierung | Zeiger\[Ausdruck\] |
| Funktionsaufruf | Ausdruck(Ausdrucksliste) |
| Werterzeugung | Typ(Ausdrucksliste) |
| Postinkrement | Lvalue++ |
| Postdekrement | Lvalue-- |
| Typidentifikation | typeid(Typ) |
| Laufzeit-Typinformation | typeid(Ausdruck) |
| zur Laufzeit geprüfte Konvertierung | dynamic_cast<Typ>(Ausdruck) |
| zur Übersetzungszeit geprüfte Konvertierung | static_cast<Typ>(Ausdruck) |
| ungeprüfte Konvertierung | reinterpret_cast<Typ>(Ausdruck) |
| const-Konvertierung | const_cast<Typ>(Ausdruck) |
| Objektgröße | sizeof Objekt |
| Typgröße | sizeof(Typ) |
| Präinkrement | ++Lvalue |
| Prädekrement | --Lvalue |
| Komplement | ~Ausdruck |
| Nicht | !Ausdruck |
| einstelliges Minus | -Ausdruck |
| einstelliges Plus | +Ausdruck |
| Adresse | \&Lvalue |
| Dereferenzierung | *Ausdruck |
| Erzeugung (Belegung) | new Typ |
| Erzeugung (Belegung und Initialisierung) | new Typ(Ausdrucksliste) |
| Erzeugung (Plazierung) | new(Ausdrucksliste) Typ |
| Erzeugung(Plazierung und Initialisierung) | new(Ausdrucksl.) Typ(Ausdrucksl.) |
| Zerstörung (Freigabe) | delete Zeiger |
| Feldzerstörung | delete [] Zeiger |
| Cast (Typkonvertierung) | (Typ) Ausdruck |
| Elementselektion | Objekt.*Zeiger−auf−Element |
| Elementselektion | Objekt->*Zeiger−auf−Element |
| Multiplikation | Ausdruck * Ausdruck |
| Division | Ausdruck / Ausdruck |
| Modulo (Rest) | Ausdruck % Ausdruck |
| Addition | Ausdruck + Ausdruck |
| Subtraktion | Ausdruck - Ausdruck |
| Linksschieben | Ausdruck << Ausdruck |
| Rechtsschieben | Ausdruck >> Ausdruck |
| Kleiner als | Ausdruck < Ausdruck |
| Kleiner gleich | Ausdruck <= Ausdruck |
| Größer als | Ausdruck > Ausdruck |
| Größer gleich | Ausdruck >= Ausdruck |
| Gleich | Ausdruck == Ausdruck |
| Ungleich | Ausdruck != Ausdruck |
| Bitweises Und | Ausdruck & Ausdruck |
| Bitweises Exklusiv-Oder | Ausdruck ^ Ausdruck |
| Bitweises Oder | Ausdruck \ |
| Logisches Und | Ausdruck && Ausdruck |
| Logisches Oder | Ausdruck \ |
| Bedingte Zuweisung | Ausdruck ? Ausdruck : Ausdruck |
| Einfache Zuweisung | Lvalue = Ausdruck |
| Multiplikation und Zuweisung | Lvalue *= Ausdruck |
| Division und Zuweisung | Lvalue /= Ausdruck |
| Modulo und Zuweisung | Lvalue %= Ausdruck |
| Addition und Zuweisung | Lvalue += Ausdruck |
| Subtraktion und Zuweisung | Lvalue -= Ausdruck |
| Linksschieben und Zuweisung | Lvalue <<= Ausdruck |
| Rechtsschieben und Zuweisung | Lvalue >>= Ausdruck |
| Und und Zuweisung | Lvalue &= Ausdruck |
| Oder und Zuweisung | Lvalue \ |
| Exklusiv-Oder und Zuweisung | Lvalue ^= Ausdruck |
| Ausnahme werfen | throw Ausdruck |
| Komma (Sequenzoperator) | Ausdruck, Ausdruck |
Jeder Kasten enthält Operatoren gleicher Priorität. Die
Operatoren in den oberen Kästen haben höhere Priorität als die in den
unteren. Beispielsweise bedeutet a+b*c das gleiche wie
a+(b*c), da * eine höhere Priorität hat als
+.
Einstellige Operatoren und Zuweisungsoperatoren sind
rechts-bindend, alle anderen links-bindend. Beispielsweise bedeutet
a=b=c das gleiche wie a=(b=c) und
*p++ bedeutet *(p++).
Die Ergebnistypen von arithmetischen Operatoren werden nach einer
Menge von Regeln bestimmt, die als die "üblichen arithmetischen
Konvertierungen" bekannt sind. Das generelle Ziel ist es, ein Resultat
des "größten" Operandentyps zu erzeugen. Beispielsweise ergibt
double+int einen double-Wert oder
int*long ergibt einen long-Wert.
Die Reihenfolge der Auswertung von Teilausdrücken innerhalb eines Ausdrucks ist undefiniert. Speziell kann man nicht erwarten, dass ein Ausdruck von links nach rechts ausgewertet wird. Beispielsweise ist es unspezifiziert, ob bei dem Code
int x = f(2) + g(3);zuerst f() oder zuerst g() aufgerufen wird.
Der Grund dafür ist, dass Compiler dadurch besser optimieren
können.
if-else-AnweisungBedingte Anweisung:
if ( expression ) statement_1bzw. mit else-Teil:
if ( expression )
statement_1
else
statement_2bzw. als Statement mit ?-Operator:
( expression ) ? statement_1 : statement_2;Bei dieser Anweisung wird zunächst die Bedingung
(expression) ausgewertet. Ist das Ergebnis true,
so wird statement_1 ausgeführt, ansonsten wird in den beiden
letzten Varianten statement_2 ausgeführt.
Beispiel:
int a = 1;
int b = 2;
int z = 0;
if (a < b) { // Bedingung in runden Klammern
z = b; // Geschweifte Klammern bei nur einer
} // Anweisung eigentlich nicht notwendig
else
z = a; // else-Teil kann auch entfallen
if (a < b)
z = 1; // a < b => z = 1
else if (a == b) // Alternative Bedingung
z = 0; // a = b => z = 0
else
z = -1; // a > b => z = -1
z = (a>b)?a:b; // z = max(a,b)switch-AnweisungBedingte Anweisung mit Ausführung abhängig vom Wert eines Ausdrucks.
switch ( expression )
{
case constant_expression_1:
statements
case constant_expression_2:
statements
case default:
statements
}Für expression sind nur int- oder char-Ausdrücke zulässig.
Normalerweise besteht eine switch-Anweisung aus einem
Block, mehreren Labels und Anweisungen. Wichtig ist, dass es sich um
einen konstanten Ausdruck (constant_expression) hinter dem
Schlüsselwort case handelt (keine Verwendung von Variablen,
Funktionsaufrufen etc.). Es können beliebig viele case
verwendet werden. Der optionale case default wird
aufgerufen, wenn keiner der anderen Fälle zutrifft. Für
benutzerdefinierte Datentypen ist switch-case nicht
benutzbar. Dann wird eine Kette von if-else-Anweisungen
benutzt.
Beispiel:
char c = 'u';
switch (c)
{
// x wird durch * ersetzt
case 'x':
c = '*';
break;
// Vokale werden durch ? ersetzt
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
c = '?';
break;
default: // Alle anderen werden durch Leerzeichen ersetzt
c = ' ';
break;
}Bei case handelt es sich um ein Label, von dem aus der
Programmablauf fortgesetzt wird. Insbesondere werden auch die
Anweisungen der nachfolgenden case-Labels ausgeführt,
solange bis die Ausführung der switch-Struktur durch ein
break-Statement abgebrochen wird (fall-through).
Im Regelfall wird daher jeder Block hinter einem case-Label
durch ein break-Statement abgeschlossen.
while-AnweisungBei dieser Schleifenstruktur wird zunächst der Ausdruck bewertet. Ist dieser wahr, so wird die abhängige Anweisung ausgeführt. Nach der Ausführung wird der Ausdruck erneut bewertet. Dieser Vorgang wird so lange durchgeführt, bis die Bewertung des Ausdrucks falsch ist. Dann wird mit der Anweisung, die hinter statement steht fortgefahren.
while ( expression ) statementBeispiel:
int b=5;
while (b-- > 0)
{
...
}for-AnweisungDie for-Schleife ist wie die while-Schleife
kopfgesteuert, d.h. die Bedingung (hier: expression_2) wird
vor dem ersten Ausführen der bedingten Anweisung geprüft. Zusätzlich
enthält die for-Schleife einen Initialisierungsausdruck
(expression_1), der vor der Schleife einmalig vor Beginn
ausgeführt wird. Weiterhin einen Iterations-Ausdruck
(expression_3), der zusätzlich am Ende jeder Ausführung der
bedingten Anweisung durchgeführt wird.
for ( expression_1; expression_2; expression_3 ) statementEine for-Schleife entspricht also im Prinzip einer
while-Schleife:
// Diese for-Schleife ist identisch ...
for (a; b; c)
d;
// ... mit einer solchen while-Schleife
a;
while (b)
{
d;
c;
}Beispiel für die Anwendung der for-Schleife:
int k = 5;
int fak = 1;
for (i = k; i > 1 ; --i)
fak *= i; // fak = Fakultät von kfor-Schleife auf
Range-BasisSoll über einen Datentyp mit mehreren gleichen Elementen z.B. Felder
iteriert werden, können range-basierte for-Schleifen
genutzt werden. Die Syntax lautet:
for ( range_declaration : range_datatype ) statementBeispiel mit einem Feld:
int v[] = {0, 1, 2, 3, 4, 5};
for (auto &i : v) // Schleife über alle Elemente in v, das Schleifenelement wird als Referenz über i angesprochen
cout << i << ' ';
// Ausgabe: 0 1 2 3 4 5Bei auto werden die Elemente kopiert, bei
auto& referenziert.
do-while-AnweisungIm Gegensatz zu den bisher genannten Schleifenkonstruktionen ist die
do-while-Schleife fußgesteuert. Die bedingte Anweisung wird
mindestens einmal beim Start der Schleife ausgeführt. Nach dieser
Ausführung wird der Ausdruck expression ausgewertet. Ist das
Ergebnis wahr, so wird die bedingte Anweisung erneut durchgeführt.
do statement while ( expression );Man beachte das Semikolon am Ende der Konstruktion.
Wie bereits erwähnt, existieren in C++ mehrere Sprunganweisungen
(engl: jump-statements), die vor allem bei den Schleifen und
der switch-Anweisung zum Einsatz kommen. Durch den Einsatz
der Sprunganweisungen wechselt der Programmablauf unbedingt.
break
Dieser Sprung darf nur innerhalb einer Schleifenstruktur oder einer
switch-Aweisung benutzt werden. In beiden Fällen wird die
Struktur verlassen und mit der folgenden Anweisung fortgefahren. Sind
mehrere solcher Strukturen ineinander verschachtelt, so bezieht sich die
Sprunganweisung nur auf die kleinste umgebende Struktur.
continue
continue darf nur in Schleifenstrukturen verwendet
werden. In allen while-Schleifen wird die bedingte
Anweisung verlassen und mit dem Ausdruck, der über Durchführung der
Schleife entscheidet, fortgefahren. Wird die Sprunganweisung in einer
for-Schleife verwendet, so wird ein Sprung zum
Iterations-Ausdruck durchgeführt.
return opt-expression
Die return-Anweisung wird benutzt, um die Ausführung
einer Funktion zu beenden und zum Aufrufer zurückzukehren. Der optionale
Ausdruck opt-expression wird dem Aufrufer zurückgeliefert.
Erreicht der Programmablauf das Ende einer Funktion, so ist dies
äquivalent zu einer return-Anweisung.
extern-AnweisungDie extern-Anweisung wird verwendet um eine bereits
global definierte Variable in einer anderen Datei bekannt zu machen.
Dies ist dann nur eine weitere Deklaration und nicht eine neue
Definition dieser Variablen.
Datei1.cpp:
int x = 1;Datei2.cpp:
extern int x;
int y = ++x;
// x == 2
// y == 2Eine Funktion ist eine Zusammenfassung von Anweisungen zu einer Einheit. Sie erhält Objekte eines bestimmten Typs als Argumente und liefert wiederum als Ergebnis ein Objekt eines bestimmten Typs.
Eine Funktionsdefinition in C++ spezifiziert den Namen der Funktion,
den Typ des zurückgelieferten Objektes und die Typen und Namen der
Argumente. Das Zurückliefern eines Wertes aus einer Funktion geschieht
mit Hilfe der return-Anweisung. Die Funktion
main() hat eine spezielle Bedeutung, sie wird beim Start
des Programms aufgerufen.
long fak(int k) // Returnwert long
// Ein Argument int k
{
// ...
return erg; // Ergebnis
}
int main() // Hauptprogramm
{
long x;
x = fak(5); // Aufruf der Funktion
return 0; // Ende des Programms
}Funktionsparameter werden in C++ normalerweise als Wert übergeben (call by value). Soll eine Referenz übergeben werden (call by reference), so muss dazu der Übergabe-Parameter als Referenz deklariert werden. Wenn ein Pointer übergeben wird, wird zwar der Pointer kopiert, aber das Objekt nicht, sodass sich auch hier call by reference ergibt.
Bei call by value wird eine Kopie des Werts an die Funktion übergeben. Dies hat zur Folge, dass Änderungen an den Parameter-Variablen nach dem Rücksprung aus der Funktion verloren gehen.
void test(int x, int y)
{
x = 10;
y = 20; // x ist hier 10, y = 20
}
int main() // Hauptprogramm
{
int wert1 = 15;
int wert2 = 25;
test(wert1, wert2); // Aufruf der Funktion
// wert1, wert2 sind jetzt immer noch 15, 25
return 0;
}Möchte man Variablen per call by reference übergeben, so ist dies über Pointer möglich, aber das ist relativ umständlich:
void some_function(int* p) {
*p = 99;
}
int main()
{
int a;
some_function(&a);
// a hat jetzt den in some_function()
// zugewiesenen Wert
}In C++ kann dies einfacher über Referenzen realisiert werden. Das Beispiel mit der Funktion ließe sich mit Referenzen folgendermaßen umschreiben:
void some_function(int& i) {
i = 99;
}
int main()
{
int a;
some_function(a);
// a hat jetzt den in some_function()
// zugewiesenen Wert
}Für Funktionsparameter sollten die folgenden Konventionen verwendet werden:
Ausgehende Parameter: Rückgabewerte sollten immer
Werte oder Referenzen sein: X f() oder
X& f(). Bei Referenzen ist darauf zu achten, dass dann
die Referenz auch außerhalb der Funktion gültig sein muss.
Ein- und Ausgehende Parameter: Parameter, die
geändert werden, sollten Referenzen sein: f(X&)
Eingehende Parameter: Parameter elementarer
Datentypen (z.B. int): f(X)
Parameter aus zusammengesetzten Datentypen oder Klassen (z.B.
vector): f(const X&)
In C++ ist es außerdem möglich, mehrere Funktionen mit gleichlautendem Funktionsnamen zu versehen. Dieses Überladen von Funktionsnamen unterscheidet der Compiler beim Aufruf und der Definition der Funktionen durch Anzahl, Typ und Reihenfolge ihrer Parameter.
double pos(double x) { // Funktion für "double"-Argumente
return (x < 0) ? -x : x; // liefert Absolutbetrag
}
int pos(int x) { // Funktion für "int"-Argumente
return (x < 0) ? 0 : x; // liefert für negative Werte 0
}
int main()
{
pos(-1); // pos(int)-Funktion wird aufgerufen
// (liefert 0)
pos(-1.0); // pos(double)-Funktion wird aufgerufen
// (liefert 1.0)
}Bei der Deklaration von Funktionen kann eine weitere Flexibilität durch Defaultparameter erreicht werden. Dabei wird einem Funktionsparameter standardmäßig ein Wert zugeordnet. Nur wenn die Funktion einen abweichenden Wert erhalten soll, muss dieser explizit angegeben werden.
void test(int x, int y=1); // Deklaration: Standardwert y=1
int main()
{
test(5); // x=5; y=1
test(7,4); // x=7; y=4
}
void test(int x, int y) // Definition der Funktion
{ ... }Die Definition der Defaultparameter erfolgt bei der Deklaration. Bei der Definition wird der Standardwert nicht mehr angegeben. Beachten Sie ggf. auftretende Mehrdeutigkeiten beim Überladen von Funktionen mit Defaultparametern. Defaultparameter können nur für hinten stehende Parameter benutzt werden.
Beispiele für unterschiedliche Funktionen bzgl. des Überladens und mit Defaultparametern:
double pos(double x, int y);
double pos(double x, double y); // y unterschiedlicher Typ
double pos(double x); // unterschiedliche Anzahl
double pos(int x, double y); // unterschiedliche Reihenfolge
double pos(double y, int x); // gleiche Funktion (Name der Argumente spielt keine Rolle) !!!
int pos(double y, int x); // falsche Überladung (Typ des Rückgabeparameters spielt keine Rolle) !!!
double pos(double x, int y=1); // gleiche Funktion (auch zu double pos(double x));Bei C++ gibt es einen Präprozessor, der vor der Compilierung
Ersetzungen durchführt. Zeilen, die im Sourcecode mit #
eingeleitet werden, heißen Steuerzeilen.
includeDer Präprozessor wird hauptsächlich dazu verwendet, Deklarationen aus
anderen Headerdateien verfügbar zu machen. Eine Steuerzeile der Form
#include <Dateiname> oder
#include "Dateiname" wird vom Präprozessor durch die
spezifizierte Datei ersetzt.
Dabei ist die Form mit "" relativ zur aktuellen Datei
und sollte für eigenen Quellcode benutzt werden. Ordner werden mit einem
Schrägstrich (/) abgetrennt, nicht wie bei Windows mit
einem umgekehrten Schrägstrich (\). Das übergeordnete
Verzeichnis kann mit .. ausgewählt werden. Dateien, die in
der anderen Form (<>) angegeben sind, werden in den
voreingestellten Include-Verzeichnissen gesucht und dienen zum Einbinden
von Systembibliotheken.
defineMit der Direktive #define können Konstanten definiert
werden. Es gilt folgende Syntax:
#define KONSTANTE WERTDer Präprozessor ersetzt vor der Compilierung jedes Auftreten von
KONSTANTE im Sourcecode durch den WERT.
Dadurch können diese Konstanten auch da benutzt werden, wo die Werte zur
Compilierzeit feststehen müssen. Beispiel:
#define ARRAY_SIZE 100
int array[ARRAY_SIZE];In C++ ist es allerdings besser, das const-Schlüsselwort
zu verwenden, bei dem die weitere Informationen z.B. über den Datentyp
nicht verloren gehen:
const int array_size = 100;
int array[array_size];Zur besseren Unterscheidung von Variablen und Funktionen sollten für Präprozessorkonstanten nur Großbuchstaben und Unterstriche verwendet werden.
Mit den bedingten Direktiven #ifdef und
#ifndef lässt sich prüfen, ob ein Makro gegenwärtig
definiert ist oder nicht. Das ist hilfreich, um Mehrfacheinbindungen von
Headerdateien zu verhindern:
#ifndef HEADER_H
#define HEADER_H
class Klasse
{
};
#endifAuch wenn der Header mehrmals eingebunden wird, wird nur eine Kopie des Inhalts eingefügt:
#include "header.h"
#include "header.h"Die Definition der Konstanten verhindert, dass die Headerdatei mehrmals eingefügt wird und dann mehrere Definitionen der Klasse sichtbar sind, was verboten ist.
Bei vielen Compilern kann dies einfacher durch die Direktive
#pragma once erreicht werden:
#pragma once
class Klasse
{
};Eine weitere Anwendung des Präprozessor sind funktionsartige Makros. Weil dabei keine Typ-Informationen vorhanden sind, sollten sie vermieden werden und werden hier daher nicht weiter betrachtet.