Einführung in C++ (Erinnerungen an C)
Für C wurde ein paar Jahre nach der Entstehung ein objektorientierter Aufsatz entwickelt: C++. Beide Sprachversionen werden aktiv weiterentwickelt, vor allem in C++ gibt es ca. alle 3 Jahre einen neuen Standard mit teilweise recht umfangreichen Ergänzungen. Hier fließen analog zu Java immer mehr Programmierkonzepte mit ein, die aus anderen Sprachen stammen (etwa funktionale Programmierung). Das macht das Erlernen und Beherrschen der Sprache nicht unbedingt leichter. Die für uns wichtigsten Neuerungen kamen mit C11 und C++11 bzw. C++14.
C und C++ versuchen (im Gegensatz zu Java) ressourcenschonende Sprachen zu sein: Ein korrektes Programm soll so schnell wie möglich ausgeführt werden können und dabei so effizient wie möglich sein (etwa in Bezug auf den Speicherbedarf). Deshalb gibt es keine Laufzeitumgebung, der Quellcode wird direkt in ein ausführbares (und damit Betriebssystem-abhängiges) Binary compiliert. Beide Sprachen erlauben dem Programmierer den Zugriff auf die Speicherverwaltung und damit viele Freiheiten. Die Kehrseite ist natürlich, dass Programmierfehler (etwa bei der Speicherallokation oder bei Indexberechnungen) nicht von der Laufzeitumgebung entdeckt und abgefangen werden können.
C-Programme sehen auf den ersten Blick Java-Code relativ ähnlich. Das ist nicht verwunderlich, da Java zeitlich nach C/C++ entwickelt wurde und die Syntax und große Teile der Schlüsselwörter von C und C++ übernommen hat. C++ hat die C-Syntax übernommen und fügt neue objektorientierte Konzepte hinzu. Mit gewissen Einschränkungen funktioniert also C-Code auch in C++.
In C++ gibt es Klassen (mit Methoden und Attributen), und zusätzlich gibt es Funktionen. Der
Einsprungpunkt in ein Programm ist (analog zu Java) die Funktion main()
, die ein int
als
Ergebnis zurückliefert. Dieser Integer kann vom Aufrufer ausgewertet werden, wobei der Wert 0
typischerweise als Erfolg interpretiert wird. Achtung: Das ist eine Konvention, d.h. es kann
Programme geben, die andere Werte zurückliefern. Die Werte müssen dokumentiert werden.
Bevor der Compiler den Quelltext "sieht", wird dieser von einem Präprozessor bearbeitet. Dieser
hat verschiedene Aufgaben, unter anderem das Einbinden anderer Dateien. Dabei wird ein
#include "dateiname"
(sucht im aktuellen Ordner) bzw. #include <dateiname>
(sucht im
Standardverzeichnis) ersetzt durch den Inhalt der angegebenen Datei.
C++-Code muss kompiliert werden. Dabei entsteht ein ausführbares Programm. Mit Make kann man den Kompiliervorgang über Regeln automatisieren (denken Sie an ANT in der Java-Welt, nur ohne XML). Eine Regel besteht aus einem Ziel (Target), einer Liste von Abhängigkeiten sowie einer Liste mit Aktionen (Anweisungen). Um ein Ziel zu "bauen" müssen zunächst alle Abhängigkeiten erfüllt sein (bzw. falls sie es nicht sind, erst noch "gebaut" werden - es muss entsprechend weitere Regeln geben, um diese Abhängigkeiten "bauen" zu können). Dann wird die Liste der Aktionen abgearbeitet. Ziele und Abhängigkeiten sind in der Regel Namen von Dateien, die existieren müssen bzw. über die Aktionen erzeugt werden sollen. Die Aktionen sind normale Befehlssequenzen, die man auch in einer Konsole eingeben könnte. Make berücksichtigt den Zeitstempel der Dateien: Ziele, die bereits existieren und deren Abhängigkeiten nicht neuer sind, werden nicht erneut gebaut.
Die gute Nachricht: In Bezug auf Variablen, Operatoren und Kontrollfluss verhalten sich C und C++ im Wesentlichen wie Java.
Es gibt in C++ den Typ bool
mit den Werten true
und false
. Zusätzlich werden Integerwerte
im boolschen Kontext (etwa in einer if
-Abfrage) ausgewertet, wobei der Wert 0 einem false
entspricht und alle anderen Integer-Werte einem true
. (Dies steht etwas im Widerspruch zu den
Werten, die in der main
-Funktion per return
zurückgeliefert werden: Hier bedeutet 0 in der
Regel, dass alles OK war.)
Die Basisdatentypen sind (bis auf char
und bool
) in ihrer Größe maschinenabhängig. Es kann
also sein, dass Code, der auf einem 64bit-Laptop ohne Probleme läuft, auf einem Raspberry PI
Überläufe verursacht! Um besonders ressourcenschonend zu arbeiten, kann man die Speichergröße
für einige Basisdatentypen durch die Typmodifikatoren short
und long
beeinflussen sowie
die Interpretation von Zahlenwerten mit oder ohne Vorzeichen (signed
, unsigned
) einstellen.
Die Anzahl der für einen Typ oder eine Variable/Struktur benötigten Bytes bekommt man mit
dem Operator sizeof
heraus.
Mit typedef
kann man einen neuen Namen für bereits existierende Typen vergeben.
In C++ gibt es Funktionen (analog zu Methoden in Java), diese existieren unabhängig von Klassen.
Wenn eine Funktion aufgerufen wird, muss dem Compiler die Signatur zur Prüfung bekannt sein. Das bedeutet, dass die Funktion entweder zuvor komplett definiert werden muss oder zumindest zuvor deklariert werden muss (die Definition kann auch später in der Datei kommen oder in einer anderen Datei). Das Vorab-Deklarieren einer Funktion nennt man auch "Funktionsprototypen".
Eine Deklaration darf (so lange sie konsistent ist) mehrfach vorkommen, eine Definition immer nur exakt einmal. Dabei werden alle Code-Teile, die zu einem Programm zusammencompiliert werden, gemeinsam betrachtet. => Das ist auch als One-Definition-Rule bekannt.
In C++ gilt beim Funktionsaufruf immer zunächst immer die Parameterübergabe per call-by-value (dito bei der Rückgabe von Werten). Wenn Referenzen oder Pointer eingesetzt werden, wird dagegen auch ein call-by-reference möglich. (Dazu später mehr.)
Unterscheidung in globale, lokale und lokale statische Variablen mit unterschiedlicher Lebensdauer und unterschiedlicher Initialisierung durch den Compiler.
- (K1) Wie hängen C und C++ zusammen?
- (K1) Wichtigste Unterschiede und Gemeinsamkeiten zu Java
- (K1) Wichtigste Aufgaben des Präprozessors
- (K3) Aufbau, Übersetzen und Starten von einfachen C++-Programmen
- (K3) Standard-Ein-/Ausgabe-Kanäle in C++ sowie die Operatoren
>>
und<<
- (K3) Nutzung der Basisdatentypen einschließlich der Modifikatoren
- (K3) Deklaration von Variablen, Nutzung von Kontrollstrukturen und Operatoren
- (K3) Interpretation von Integers im booleschen Kontext
- (K3) Nutzung des Scope-Operators
::
, Namensräume - (K3) Benutzung von
sizeof
zur Bestimmung des Speicherbedarfs - (K3) Benutzung von
typedef
zur Definition neuer Typen (Aliase bestehender Typen) - (K3) Erinnerung: Automatisiertes Übersetzen mit Hilfe von GNU Make und einfachsten Makefiles
- (K2) Unterschied zwischen Deklaration und Definition, One Definition Rule
- (K2) Problematik bei der Deklaration parameterloser Funktionen
- (K2) Call-by-Value-Semantik bei der Parameterübergabe
- (K2) Sichtbarkeit und Initialisierung von Variablen
- (K3) Definition und Deklaration von Funktionen
- (K3) Nutzung lokaler und globaler und lokaler statischer Variablen
Warum?
- C++ erlaubt ressourcenschonende Programmierung
- Objektorientierter "Aufsatz" auf C
- Verbreitet bei hardwarenaher und/oder rechenintensiver Software
Sie werden C++ im Modul "Computergrafik" brauchen!
Geschichte
- 1971-73: Ritchie entwickelt die Sprache C
- Ab 1979: Entwicklung von C++ durch Bjarne Stroustrup bei AT&T
- Erweiterung der prozeduralen Sprache C
- Ursprünglich "C mit Klassen", später "C++" (Inkrement-Operator)
- Bis heute: Fortlaufende Erweiterungen: alle 3 Jahre neuer Standard (C++11, C++14, ...)
- Java: Fokus auf Sicherheit und Robustheit
- Diverse Sicherheitschecks durch Compiler und VM (zb. Array-Zugriff)
- Speicherverwaltung (Garbage Collection), kein Speicherzugriff über Pointer
- Automatische Initialisierung von Variablen
- C/C++: Fokus auf Effizienz (Speicher, Laufzeit) für korrekte Programme
- Vergleichsweise schwache Sicherheitschecks durch Compiler, keine VM (d.h. keine Prüfung von Array-Indizes u.a.)
- Keine Garbage Collection, Programmierer hat direkten Zugriff auf Speicher
- Keine automatische Initialisierung von Variablen
Hello World!
/*
* HelloWorld.cpp (g++ -Wall HelloWorld.cpp)
*/
#include <cstdio>
#include <iostream>
#include <cstdlib>
using namespace std;
int main() {
printf("Hello World from C++ :-)\n");
cout << "Hello World from C++ :-)" << endl;
std::cout << "Hello World from C++ :-)" << std::endl;
return EXIT_SUCCESS;
}
Beobachtungen
Jedes (ausführbare) C++-Programm hat genau eine main()
-Funktion. Die main()
-Funktion ist
keine Methode einer Klasse: In C/C++ gibt es Funktionen auch außerhalb von Klassen.
In C++ gibt es Namespaces (dazu später mehr). Die aus der Standardbibliothek importierten
Funktionen sind in der Regel im Namespace std
definiert. Mit using namespace std;
können
Sie auf die Elemente direkt zugreifen. Wenn Sie das using namespace std;
weglassen, müssten
Sie bei jeder Verwendung eines Symbols den Namensraum explizit dazu schreiben
std::cout << "Hello World from C++ :-)" << std::endl;
.
Sie können im C++-Code auch Funktionen aus C benutzen, d.h. Sie können für die Ausgabe
beispielsweise printf
nutzen (dazu müssen Sie den Header <cstdio>
importieren). Die
"richtige" Ausgabe in C++ ist aber die Nutzung des Ausgabestreams cout
und des
Ausgabeoperators <<
. Das endl
sorgt für einen zum jeweiligen Betriebssystem passenden
Zeilenumbruch.
Der Rückgabewert signalisiert Erfolg bzw. Fehler der Programmausführung. Dabei steht der Wert
0 traditionell für Erfolg (Konvention!). Besser Makros nutzen: EXIT_SUCCESS
bzw.
EXIT_FAILURE
(in cstdlib
).
Präprozessor
Der Präprozessor transformiert den Quellcode vor dem Compiler-Lauf. Zu den wichtigsten
Aufgaben gehören dabei die Makrosubstitution (#define Makroname Ersatztext
) und das Einfügen
von Header-Dateien (und anderen Dateien) per #include
. Es gibt dabei zwei Formen, die an
unterschiedlichen Orten nach der angegebenen Datei suchen:
#include "dateiname"
sucht im aktuellen Ordner#include <dateiname>
sucht im Standardverzeichnis
Das #include
kann wie in C genutzt werden, aber es gibt auch die Form ohne die Dateiendung
".h". Da es in C keine Funktionsüberladung gibt (in C++ dagegen schon), müssen die C-Header
speziell markiert sein, um sie in C++ verwenden zu können. Für die Standard-Header ist dies
bereits erledigt, Sie finden diese mit einem "c" vorangestellt:
- Include in C:
#include <stdio.h>
- Include in C++:
#include <cstdio>
Übersetzen, Linken, Ausführen
C++-Dateien werden üblicherweise mit der Endung ".cpp" oder ".cxx" oder ".cc" abgespeichert, Header-Dateien mit den Endungen ".hpp" oder ".hxx" oder ".hh".
Zum Übersetzen und Linken in einem Arbeitsschritt rufen Sie den Compiler auf:
g++ HelloWorld.cpp
bzw. besser g++ -Wall -o helloworld HelloWorld.cpp
. Die Option
-Wall
sorgt dafür, dass alle Warnungen aktiviert werden.
Ausführen können Sie das erzeugte Programm in der Konsole mit: ./helloworld
. Der aktuelle
Ordner ist üblicherweise (aus Sicherheitsgründen) nicht im Suchpfad für ausführbare Dateien
enthalten. Deshalb muss man explizit angeben, dass ein Programm im aktuellen Ordner (.
)
ausgeführt werden soll.
Variablen, Operatoren, Kontrollfluss
Im Wesentlichen wie von C und Java gewohnt ... :-)
-
Wichtig(st)e Abweichung:
Im booleschen Kontext wird
int
als Wahrheitswert interpretiert: Alle Werte ungleich 0 entsprechentrue
(!)Anmerkung: Dies steht im Widerspruch zu den Werten, die in der
main
-Funktion perreturn
zurückgeliefert werden: Hier bedeutet 0 in der Regel, dass alles OK war.
=> Vorsicht mit
int c;
if (c=4) { ... }
Ein- und Ausgabe mit printf und cin/cout
-
printf(formatstring, ...)
string foo = "fluppie"; printf("hello world : %s\n", foo.c_str());
-
Einbinden über
#include <cstdio>
-
Format-String: Text und Formatierung der restlichen Parameter:
%[flags][width][.precision]conversion
-
flags
: hängt von der konkreten Ausgabe ab -
width
: Feldbreite -
precision
: Anzahl der Dezimalstellen -
conversion
: (Beispiele)c Zeichen (Char) d Integer (dezimal) f Gleitkommazahl
-
-
-
Standardkanäle:
cin
(Standardeingabe),cout
(Standardausgabe),cerr
(Standardfehlerausgabe)- Genauer:
cout
ist ein Ausgabestrom, auf dem der Operator<<
schreibt - Einbinden über
#include <iostream>
- Implementierung der Ein- und Ausgabeoperatoren (
>>
,<<
) für Basistypen und Standardklassen vorhanden - Automatische Konvertierungen für Basistypen und Standardklassen
// Ausgabe, auch verkettet string foo = "fluppie"; cout << "hello world : " << foo << endl; // liest alle Ziffern bis zum ersten Nicht-Ziffernzeichen // (fuehrende Whitespaces werden ignoriert!) int zahl; cin >> zahl;
// Einzelne Zeichen (auch Whitespaces) lesen char c; cin.get(c);
- Genauer:
Sichtbarkeit und Gültigkeit und Namespaces
Wie in Java:
- Namen sind nur nach Deklaration und innerhalb des Blockes, in dem sie deklariert wurden, gültig
- Namen sind auch gültig für innerhalb des Blockes neu angelegte innere Blöcke
- Namen in inneren Blöcken können Namen aus äußeren Scopes überdecken
Zusätzlich gibt es noch benannte Scopes und einen Scope-Operator.
-
C++ enthält den Scope-Operator
::
=> Zugriff auf global sichtbare Variablenint a=1; int main() { int a = 10; cout << "lokal: " << a << "global: " << ::a << endl; }
-
Alle Namen aus
XYZ
zugänglich machen:using namespace XYZ;
using namespace std; cout << "Hello World" << endl;
-
Alternativ gezielter Zugriff auf einzelne Namen:
XYZ::name
std::cout << "Hello World" << std::endl;
-
Namensraum
XYZ
deklarierennamespace XYZ { ... }
Arrays und Vektoren in C++
-
Syntax:
Typ Name[AnzahlElemente];
int myArray[100]; int myArray2[] = {1, 2, 3, 4};
-
Compiler reserviert sofort Speicher auf dem Stack => statisch: im Programmlauf nicht änderbar
-
Zugriff über den Indexoperator []
-
Achtung: "roher" Speicher, d.h. keinerlei Methoden
-
Größe nachträglich bestimmen mit
sizeof
:int myArray[100], i; int cnt = sizeof(myArray)/sizeof(myArryay[0]);
Guter Stil: Anzahl der Elemente als Konstante deklarieren: Statt
int myArray[100];
besser#define LENGTH 100 int myArray[LENGTH];
-
-
Vordefinierter Vektor-Datentyp
vector
- Einbinden über
#include <vector>
- Parametrisierter Datentyp (C++: Templates) - Nutzung analog wie in Java (Erstellung von Templateklassen und -methoden aber deutlich anders!)
- Anlegen eines neuen Arrays mit 10 Elementen für Integer:
vector<int> v(10); vector<double> meinVektor = {1.1, 2.2, 3.3, 4.4}; meinVektor.push_back(5.5); cout << meinVektor.size() << endl;
- Zugriff auf Elemente:
cout << v[0] << endl; // ohne Bereichspruefung! cout << v.at(1000) << endl; // mit interner Bereichspruefung
- Zuweisung (mit Kopieren):
vector<double> andererVektor; andererVektor = meinVektor;
- Dynamische Datenstruktur:
vector<int> meineDaten; // initiale Groesse: 0 meineDaten.push_back(123); // Wert anhaengen meineDaten.pop_back(); // Wert loeschen meineDaten.empty(); // leer?
- Einbinden über
Vorsicht! vector<int> arr();
ist kein Vektor der Länge 0,
sondern deklariert eine neue Funktion!
Alias-Namen für Typen mit typedef und using
-
Syntax:
typedef existTyp neuerName;
(C, C++)typedef unsigned long uint32; uint32 x, y, z;
Im Beispiel ist
uint32
ein neuer Name für den existierenden Typunsigned long
, d.h. die Variablenx
,y
undz
sindunsigned long
. -
Syntax:
using neuerName = existTyp;
(C++)typedef unsigned long uint32; // C, C++ using uint32 = unsigned long; // C++11 typedef std::vector<int> foo; // C, C++ using foo = std::vector<int>; // C++11 typedef void (*fp)(int,double); // C, C++ using fp = void (*)(int,double); // C++11
Seit C++11 gibt es das Schlüsselwort
using
für Alias-Deklarationen (analog zutypedef
). Dieses funktioniert im Gegensatz zutypedef
auch für Templates mit (teilweise) gebundenen Template-Parametern.
Erinnerungen an C - Vergleich mit C++
Wrap-Up
-
C/C++ sind enge Verwandte: kompilierte Sprachen, C++ fügt OO hinzu
-
Funktionsweise einfachster Make-Files
-
Wichtigste Unterschiede zu Java
- Kontrollfluss wie in Java
- Basisdatentypen vorhanden
- Typ-Modifikatoren zur Steuerung des Speicherbedarfs/Wertebereich
- Integer können im booleschen Kontext ausgewertet werden
- Operator
sizeof
zur Bestimmung des Speicherbedarfs - Alias-Namen für existierende Typen mit
typedef
definierbar - Funktionen mit Default-Parametern und Überladung
-
Wie groß ist der Bereich der Basisdatentypen (Speicherbedarf, Zahlenbereich)? Wie können Sie das feststellen?
unsigned char a; int b; long long x[10]; long long y[] = {1, 2, 3}; long long z[7] = {3};
-
Erklären Sie den Unterschied
sizeof(x)
vs.sizeof(x)/sizeof(x[0])
! -
Warum ist der folgende Code-Schnipsel gefährlich?
if (i=3) printf("Vorsicht"); else printf("Vorsicht (auch hier)");
-
Limits kennen: Datentypen, Wertebereiche
Schreiben Sie ein C-Programm, welches die größtmögliche
unsigned int
Zahl auf Ihrem System berechnet.Verwenden Sie hierzu nicht die Kenntnis der systemintern verwendeten Bytes (
sizeof
, ...). Nutzen Sie auch nicht die Konstanten/Makros/Funktionen auslimits.h
oderfloat.h
oder anderen Headerdateien! -
Erklären Sie die Probleme bei folgendem Code-Schnipsel:
int maximum(int, int); double maximum(int, int); char maximum(int, int, int=10);
-
Erklären Sie die Probleme bei folgendem Code-Schnipsel:
int maximum(int, int); double maximum(double, double); int main() { cout << maximum(1, 2.2) << endl; }
-
Erklären Sie den Unterschied zwischen
int a=1; int main() { extern int a; return 0; }
und
int a=1; int main() { int a = 4; return 0; }
- [Breymann2011] Der C++ Programmierer
Breymann, U., Hanser, 2011. ISBN 978-3-446-42691-7. - [cppreference.com] C and C++ Reference
, cppreference.com. - [cprogramming.com] C Programming and C++ Programming
Allain, A. und Hoffer, A..