**Dies ist eine alte Version des Dokuments!**
C++ Grundlagen
Installation
Es gibt verschiedene Möglichkeiten, um C++-Code zu kompilieren und auszuführen. Gerade für den Anfang ist es aber empfohlen, es auf die 'traditionelle' Art und Weise zu machen. Dazu soll der GNU Compiler installiert werden. Der Code wird dann in der Konsole zuerst kompiliert (und ein paar weitere Schritte) und danach separat ausgeführt.
Unix (Mac & Linux)
Auf Unix-basierten Systemen (Mac und Linux) sollte der GNU Compiler bereits installiert sein. Mit g++ -v
kannst du dies sicherstellen und sehen, welche Version installiert ist.
Windows
- MinGW: http://www.mingw.org
- Video: https://youtu.be/sXW2VLrQ3Bs
- (nicht explizit getestet)
C++ Programmieren
Grundlegender Synax
Es wird angenommen, dass du bereits über Vorwissen in einer anderen Programmiersprache verfügst, optimalerweise in einer typensicheren Sprache wie C# oder Java. Deshalb solltest du in der Lage sein, dir selbst den wichtigsten Syntax zu erarbeiten.
Hier einige Quellen:
- Video Tutorial Reihe von The Cherno: https://www.youtube.com/playlist?list=PLlrATfBNZ98dudnM48yfGUldqGD0S4FFb
Besonderheiten von C++
Essentiell für das Programmieren in C++ ist das Verständnis von:
- Header-Files: https://youtu.be/9RJTQmK0YPI
- Pointer: https://youtu.be/DTxHyVn0ODg
- References: https://youtu.be/IzoFn3dfsPA
- Kompilieren & Ausführen von Code in C++ (siehe Kapitel unten)
Kompilieren & Ausführen von Code in C++
Ziel dieses Kapitels ist es, ein Verständnis zu entwickeln für die verschiedenen Schritte, die zwischen dem Code in C++ und der ausführbaren Datei liegen.
Testprogramm
Dazu betrachten wir ein kleines (und ziemlich sinnloses) Programm, welches auf 3 Files aufgeteilt wurde. Stelle sicher, dass du genau verstehst, was das Programm macht. Auch sollte dir klar sein, warum es die Headerdatei calc.h
braucht.
- prog.cpp
#include "calc.h" int main() { add(3,7); sub(3,7); mul(3,7); return 0; }
- calc.cpp
#include <iostream> using namespace std; void add(int x, int y) { int r = x + y; cout << r << endl; } void sub(int x, int y) { int r = x - y; cout << r << endl; } void mul(int x, int y) { int r = x * y; cout << r << endl; }
- h.cpp
void add(int,int); void sub(int,int); void mul(int,int);
Build-Prozess
Quellen:
- Übersicht GNU Compiler, einzelne Schritte: https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html
- Video-Tutorial von The Cherno
- How C++ Works: https://youtu.be/SfGuIVzE_Os
- How the C++ Compiler Works: https://youtu.be/3tIqpEmWMLI
- How the C++ Linker Works: https://youtu.be/H4s55GgAg0I
Möchte man das Programm einfach ausführen, ohne sich mit den Details auseinander zu setzen, so kann man diese wie folgt tun: In einem ersten Schritt wird der Code in eine ausführbare Datei umgewandelt:
# Windows: g++ -o main.exe -Wall prog.cpp calc.cpp # Unix: g++ -o main -Wall prog.cpp calc.cpp
Diese kann man dann ganz einfach ausführen:
# Windows: main.exe # Unix: ./main
Möchte man dies genauer verstehen, so muss man die folgenden 4 Schritte dieses Prozesses anschauen:
- Preprocess
- Kompilieren
- Linken
- Ausführen
In Folgenden wird auf die einzelnen Schritte eingegangen.
In den verschiedenen Schritten wird der ursprüngliche C++-Code in Dateien verschiedener Formate umgewandelt. Man kann diese Zwischenprodukte auch mit einem einzelnen Befehl erstellen. Füge einfach die Flag -save-temps
hinzu:
g++ -o main -save-temps prog.cpp calc.cpp
Schritt 1: Preprocess
Preprozessor Statements wie include oder if werden ausgeführt. Z.B. resultiert #include <iostream>
, dass der gesamte Code aus iostream in den Code hinein kopiert wird.
Die Flag -E
bewirkt, dass nur der Preprocess-Schritt ausgeführt wird. Beachte, dass der Output nur in der Konsole ausgegeben wird und nicht in ein File geschrieben wird. Möchte man dies, muss man dies mit -o outputfilename
explizit angeben.
g++ -E -o prog.ii prog.cpp g++ -E -o calc.ii calc.cpp
Das File prog.ii sieht wie folgt aus:
- prog.ii
# 1 "prog.cpp" # 1 "<built-in>" 1 # 1 "<built-in>" 3 # 379 "<built-in>" 3 # 1 "<command line>" 1 # 1 "<built-in>" 2 # 1 "prog.cpp" 2 # 1 "./calc.h" 1 void add(int,int); void sub(int,int); void mul(int,int); # 2 "prog.cpp" 2 int main() { add(3,7); sub(3,7); mul(3,7); return 0; }
Beachte, dass in den Zeilen 8-10 die Zeilen aus calc.h
einfach hinein kopiert wurden.
Das File calc.ii
wird hier nicht gezeigt, da es mehrere 10'000 Zeilen lang ist! Der Grund ist, dass der gesamte Inhalt von iostream hinein kopiert wurde.
Ein solches .ii
File (je nach OS auch mit Endung .i
) wird auch Translation Unit genannt und beinhaltet nichts weiteres als C++-Code - auch wenn das File eine andere Endung hat.
Schritt 2: Kompilieren
Danach kommt der eigentliche Kompilierungsschritt. Der preprozessierte Code wird in Maschinencode kompiliert. Dieser wird in einem sogenannten Object-File (Endung: .o
oder .out
) gespeichert. Dieser Maschinencode beinhaltet direkte Instruktionen an die CPU. Der Die -c
Flag bewirkt, dass der C++ Code nur preprozessiert und kompiliert wird:
g++ -c prog.cpp g++ -c calc.cpp
Der Inhalt von Object-Files ist in Binärform, kann also nicht von uns gelesen werden.
Genau genommen verläuft diese Kompilierung in zwei Schritten ab:
Schritt 2A: Kompilieren C++ zu Assembly-Code
Kompilation mit GNU-Compiler in Assembly-Code (Endung: .s
). Assembler ist eine sehr hardwarenahe Programmiersprache und ist unterschiedlich für verschiedene CPU-Typen.
Möchte man nur diesen Schritt machen, verwendet man die Flag -S
:
g++ -S prog.cpp g++ -S calc.cpp
Das Resultat vom ersten Schritt (preprocessierten C++-Code in Assembly-Code):
- prog.s
.section __TEXT,__text,regular,pure_instructions .build_version macos, 10, 15 sdk_version 10, 15, 6 .globl _main ## -- Begin function main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## %bb.0: pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp subq $16, %rsp movl $0, -4(%rbp) movl $3, %edi movl $7, %esi callq __Z3addii movl $3, %edi movl $7, %esi callq __Z3subii movl $3, %edi movl $7, %esi callq __Z3mulii xorl %eax, %eax addq $16, %rsp popq %rbp retq .cfi_endproc ## -- End function .subsections_via_symbols
Schritt 2B: Kompilieren Assembly-Code zu Maschinencode
Die Assembly-Datei (.s
) wird dann in Maschinencode umgewandelt. Dies passiert nicht mehr mit dem GNU-Compiler, sondern mit dem Assembler-Compiler:
as -o prog.o prog.s as -o calc.o calc.s
Schritt 3: Linking
Damit das Programm ausgeführt werden muss, müssen die einzelnen Object-Files miteinander verknüpft (also gelinked) werde. Das Resultat ist dann eine ausführbare Datei (hier main
genannt)
# Windows: g++ -o main prog.o calc.o # Unix: g++ -o main prog.o calc.o
Doch wozu braucht man diesen Linking-Schritt? Machen wir ein Beispiel:
- in
prog.cpp
wird die FunktionAdd(...)
aufgerufen, welche im Headerfilecalc.h
deklariert ist. Im Preprocess-Schritt wird diese Deklaration dann inprog
hinein kopiert. Die Definition fehlt aber! Das Programmprog.o
kann alleine nicht ausgeführt werden, da sich die Definition der Funktion incalc.o
befindet. Im Linking-Schritt wird dann die Deklaration von Add inprog.o
mit deren Definition incalc.o
verknüpft. Man sieht also, dass man eine Datei auch schon kompilieren kann, wenn nur die Deklaration einer Funktion, aber nicht deren Definition enthalten ist. - Auch muss der Einstiegspunkt ins Programm, sprich die main-Funktion gefunden werden. Auch dies geschieht im Linking-Schritt.
Den Linking-Schritt kann man auch einzeln machen, der Befehl ist aber etwas mühsam und hängt auch von der Plattform ab. Deshalb wird auf diesen hier verzichtet.
Schritt 4: Ausführen
Nun können wir unser Programm ausführen:
# Windows: main.exe # Unix: ./main
Makefile
Grosse Projekte können sehr schnell aus sehr vielen einzelnen Files bestehen. Da jeweils alle dem Compiler übergeben werden müssen (gcc -o main file1.cpp file2.cpp file3.cpp ...
), kann dies ziemlich mühsam werden. Hier kann ein Makefile helfen, siehe z.B. hier https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html#zz-2.
Ein einfaches Makefile für unser Beispielprojekt sieht wie folgt aus: (siehe auch: https://youtu.be/_r7i5X0rXJk)
- makefile
# AUFBAU # Ziel: Abhaengigkeiten # Befehl main: prog.o calc.o # wenn prog.o oder calc.o veraendert wurden -> erstelle Ausfuehrbare Datei main neu, indem der Befehl unten (g++ ...) ausgefuehrt wird g++ -o main prog.o calc.o prog.o: prog.cpp calc.h # wenn prog.cpp oder calc.h veraendert wurden -> prog.o neu erstellen g++ -c prog.cpp calc.o: calc.cpp calc.h # analog g++ -c calc.cpp clean: # mit 'make clean', loesche alle .o und das main File rm *.o main
Eine etwas 'professionellere' Variante ist das folgende Makefile:
- makefile
# Definiere Variablen für Compiler, Flags und ausfuehrbare Datei CXX = g++ CXXFLAGS = -g -Wall EXEC = main # AUFBAU # Ziel: Abhaengigkeiten # Befehl ${EXEC}: prog.o calc.o # wenn prog.o oder calc.o veraendert wurden -> erstelle Ausfuehrbare Datei main neu, indem der Befehl unten (g++ ...) ausgefuehrt wird ${CXX} ${CXXFLAGS} -o ${EXEC} prog.o calc.o prog.o: prog.cpp calc.h # wenn prog.cpp oder calc.h veraendert wurden -> prog.o neu erstellen ${CXX} ${CXXFLAGS} -c prog.cpp calc.o: calc.cpp calc.h # analog ${CXX} ${CXXFLAGS} -c calc.cpp clean: # mit 'make clean', loesche alle .o und das main File rm *.o ${EXEC}
Das Makefile kannst du dann mit folgenden Befehlen verwenden:
# ausführbare Datei erstellen (Kompilieren usw.) make # ausführen ./main # main und .o Dateien löschen make clean