Home

07 Mehrsprachige Projekte Teil 1: C++ Entwicklungsgrundlagen

lecture cpp cmake google-test clang-format clang-tidy compilation build-systems

1. Einführung: Ihr Python-Code funktioniert, aber läuft er auch auf einem eingebetteten System?

Ihr Road Profile Viewer funktioniert perfekt. Der Python-Code ist sauber, getestet und folgt allen Qualitätsstandards, die Sie in diesem Kurs gelernt haben. Die Visualisierung ist schön, der Strahlschnitt-Algorithmus ist mathematisch korrekt, und die CI-Pipeline leuchtet grün.

Dann fragt Ihr Betreuer: “Können wir den Strahlschnitt-Algorithmus auf dem eingebetteten ECU des Fahrzeugs ausführen?”

Sie prüfen die ECU-Spezifikationen:

Ihr Python-Code kann hier nicht laufen.

Nicht weil er schlecht geschrieben ist, sondern weil Python selbst benötigt:

Das ist keine ungewöhnliche Situation. In der Automobil-, Robotik-, Luft- und Raumfahrt- sowie wissenschaftlichen Datenverarbeitung ist Python oft die Prototyping-Sprache, aber leistungskritischer Code läuft in C oder C++.

DAS SPRACHSPEKTRUM
🐍 Python
  • ✍️ Einfach zu schreiben
  • 🐢 Langsam in der Ausführung
  • 📦 Hoher Speicherverbrauch
  • 🔄 Interpretiert
  • 🎭 Dynamische Typisierung
  • ⏸️ GC-Pausen
  • 📚 Reiches Ökosystem
Ideal für:
Prototyping Data Science Web-Backends Automatisierung
C/C++
  • 🧩 Schwer zu schreiben
  • 🚀 Schnell in der Ausführung
  • 💾 Niedriger Speicherverbrauch
  • ⚙️ Kompiliert
  • 🔒 Statische Typisierung
  • 🎯 Manuelle Speicherverwaltung
  • 🔧 Hardware-nah
Ideal für:
Eingebettete Systeme Spiele-Engines Betriebssysteme Echtzeitsysteme

Die Frage ist: Wie verbinden Sie diese beiden Welten, ohne alles neu zu schreiben?

Diese zweiteilige Vorlesungsreihe beantwortet diese Frage:

In dieser Vorlesung werden Sie lernen:

  1. Den C++ Build-Prozess verstehen — Kompilierung, Linking und warum er sich von Python unterscheidet
  2. CMake verwenden als modernes C++ Build-System
  3. Code-Qualitätswerkzeuge anwenden (clang-format, clang-tidy) mit der gleichen Disziplin wie Ruff für Python
  4. Unit-Tests schreiben mit Google Test
  5. Coverage-Reports generieren für C++-Code

Am Ende werden Sie die Grundlagen haben, um produktionsreifen C++-Code zu schreiben, der später mit Python integriert werden kann.


2. Lernziele

Am Ende dieser Vorlesung werden Sie in der Lage sein:

  1. Den Unterschied zu erklären zwischen kompilierten und interpretierten Sprachen und warum das für eingebettete Systeme wichtig ist
  2. Ein C++-Projekt einzurichten mit CMake, einschließlich Abhängigkeiten und Build-Konfigurationen
  3. Code-Qualitätswerkzeuge anzuwenden auf C++-Code (clang-format, clang-tidy) mit der gleichen Disziplin wie Ruff für Python
  4. Compiler-Warnungen zu konfigurieren entsprechend verschiedener Projektanforderungen (Sicherheit vs. Geschwindigkeit)
  5. Unit-Tests zu schreiben für C++-Code mit Google Test
  6. Coverage-Reports zu generieren für C++-Code mit gcov/llvm-cov

2.1 Was Sie nicht lernen werden

Diese Themen verdienen eigene Kurse. Unser Fokus liegt auf Software-Engineering-Praktiken, die von Python auf C++ übertragbar sind.


3. Teil 1: C++ Entwicklungsgrundlagen

Bevor wir C++ mit Python integrieren können, müssen wir verstehen, wie C++-Entwicklung funktioniert. Wenn Sie nur Python geschrieben hast, wird sich C++ grundlegend anders anfühlen.

4. Der fundamentale Unterschied: Kompiliert vs. Interpretiert

4.1 Wie Python Code ausführt

Ein tieferer Blick unter die Haube

In diesem Abschnitt erkunden wir Pythons Ausführungsmodell detaillierter als zuvor. Das Verständnis von Bytecode, der Python Virtual Machine und dem Interpreter hilft uns, die fundamentalen Unterschiede zwischen Python und C++ zu würdigen—insbesondere warum kompilierte Sprachen 10-100x schneller sein können.

Für die meiste tägliche Entwicklung ist das, was Sie früher in diesem Kurs gelernt haben—uv für Abhängigkeitsverwaltung, virtuelle Umgebungen verstehen, sauberen Code schreiben—am wichtigsten. Sie können ein produktiver Python-Entwickler sein, ohne zu wissen, wie die PVM funktioniert.

Aber wenn Sie verstehen müssen, warum Python langsamer ist, warum C++ für leistungskritischen Code verwendet wird, oder wie Werkzeuge wie pybind11 die beiden Welten verbinden, wird dieses Grundlagenwissen essentiell.

4.1.1 Was ist ein Interpreter?

Ein Interpreter ist ein Programm, das Code ausführt, der in einer Programmiersprache geschrieben wurde. Anders als ein Compiler (der das gesamte Programm in Maschinencode übersetzt, bevor es läuft), verarbeitet ein Interpreter Code Zeile für Zeile (oder Anweisung für Anweisung) zur Laufzeit.

Der Python-Interpreter (CPython)

Wenn Leute “Python” sagen, meinen sie meist CPython—die Referenzimplementierung von Python, geschrieben in C. Es heißt CPython, weil der Interpreter selbst in C geschrieben ist, nicht weil es etwas mit C++ zu tun hat.

Andere Python-Implementierungen existieren:

Für diesen Kurs verwenden wir CPython (der Standard, wenn Sie Python installieren).

4.1.2 Python direkt ausführen

Wenn Sie ein Python-Programm ausführen:

python main.py

Folgendes passiert:

📄
main.py
Ihr Quellcode (Textdatei)
🐍
Python-Interpreter
Liest Ihren Code zur Laufzeit (CPython)
📦
Bytecode
Zwischendarstellung (.pyc in __pycache__/)
⚙️
Python Virtual Machine
Führt Bytecode Anweisung für Anweisung aus
Ergebnisse

4.1.3 Was ist Bytecode?

Sie fragen sich vielleicht: Wenn Python “interpretiert” ist, warum gibt es dann einen “Kompilierung zu Bytecode”-Schritt? Das ist eine häufige Quelle der Verwirrung.

Bytecode ist eine Zwischendarstellung — eine Menge von Low-Level-Anweisungen, die für die Python Virtual Machine (PVM) einfacher auszuführen sind als roher Quellcode. Stellen Sie es sich als eine “vereinfachte” Version Ihres Programms vor, die die PVM schnell verarbeiten kann.

Der Interpreter erstellt Bytecode beim ersten Durchlauf. Wenn Python Ihren Code zum ersten Mal ausführt:

  1. Der Interpreter liest Ihre .py-Datei
  2. Er kompiliert den Quellcode in Bytecode
  3. Er speichert den Bytecode in einer .pyc-Datei im __pycache__/-Verzeichnis
  4. Die PVM führt den Bytecode aus

Bytecode wird zwischengespeichert und wiederverwendet. Bei nachfolgenden Durchläufen optimiert Python den Start:

  1. Python prüft, ob eine .pyc-Datei für Ihr Modul existiert
  2. Python vergleicht Zeitstempel: Ist die .pyc neuer als die .py?
  3. Falls ja → Kompilierung überspringen, Bytecode direkt laden (schnellerer Start!)
  4. Falls nein → Neu kompilieren (Ihr Quellcode hat sich geändert)
🚀 Erster Durchlauf
📄main.py
⚙️Kompilieren
📦main.pyc
▶️Ausführen
⚡ Nachfolgende Durchläufe
📄main.py
.pyc neuer?
✓ Ja
📦.pyc laden
▶️Ausführen
✗ Nein
🔄Neu kompilieren
▶️Ausführen

Warum ist das wichtig?

Sie können Bytecode selbst inspizieren:

import dis

def add(a, b):
    return a + b

dis.dis(add)

Ausgabe:

  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE

Das sind die Bytecode-Anweisungen, die die PVM tatsächlich ausführt. Jede Anweisung ist einfach: einen Wert laden, zwei Werte addieren, zurückgeben.

Wichtige Unterscheidung:

Aspekt Python Bytecode C++ Maschinencode
Ausgeführt von Python Virtual Machine (Software) CPU direkt (Hardware)
Portabilität Gleicher Bytecode läuft auf jedem OS mit Python Unterschiedliche Binärdatei für jedes OS/Architektur
Geschwindigkeit Langsamer (interpretiert von PVM) Schneller (native Ausführung)
Dateiendung .pyc .exe, .out oder keine Endung

Deshalb wird Python oft “interpretiert” genannt, obwohl es einen Kompilierungsschritt gibt—der Bytecode benötigt immer noch die PVM zum Ausführen, anders als C++, das zu nativem Maschinencode kompiliert wird.

4.1.4 Was ist die Python Virtual Machine (PVM)?

Die Python Virtual Machine ist kein separates Programm, das Sie installieren—sie ist die Kern-Ausführungsmaschine, die in den CPython-Interpreter eingebaut ist. Wenn Sie Python installieren, bekommen Sie die PVM als Teil des Pakets.

Die PVM ist ein C-Programm (genauer gesagt, sie ist das Herz von CPython). Sie besteht aus:

  1. Einer Bytecode-Auswertungsschleife — Die Hauptfunktion, die Bytecode-Anweisungen eine nach der anderen liest
  2. Einem Stack — Wo Werte während der Berechnung gespeichert werden
  3. Frame-Objekten — Verfolgen Funktionsaufrufe, lokale Variablen und Ausführungszustand
  4. Speicherverwaltung — Behandelt Objektallokation und Garbage Collection

Wie die PVM Ihren Code ausführt:

🐍 Python Virtual Machine
📦
Bytecode
.pyc
🔄
Eval Loop
ceval.c
Aktion ausführen
push/pop/call
📚
Stack
[Werte]
💾
Speicher
[Objekte]

Die Auswertungsschleife in Aktion:

Wenn Sie result = 3 + 5 ausführst, macht die PVM folgendes:

Bytecode-Anweisung        Stack-Zustand    Aktion
─────────────────────     ───────────      ──────
LOAD_CONST 3              [3]              3 auf Stack legen
LOAD_CONST 5              [3, 5]           5 auf Stack legen
BINARY_ADD                [8]              Zwei Werte entnehmen, addieren, Ergebnis legen
STORE_NAME 'result'       []               8 entnehmen, in Variable 'result' speichern

Die PVM ist eine Stack-basierte virtuelle Maschine. Jede Operation legt Werte auf einen Stack, führt Berechnungen durch und entnimmt Ergebnisse.

Das mentale Modell für Studierende:

Wenn Sie ein Python-Skript ausführen, stellen Sie sich einen kleinen Roboter (die PVM) in Ihrem Computer vor:

  1. Der Roboter erhält eine Liste einfacher Anweisungen (Bytecode)
  2. Der Roboter hat einen Notizblock (den Stack), auf dem er Zwischenwerte notiert
  3. Der Roboter liest eine Anweisung nach der anderen, führt sie aus und geht zur nächsten
  4. Der Roboter verwaltet Speicher: erstellt Objekte bei Bedarf, räumt sie auf, wenn sie nicht mehr gebraucht werden

Dieser Roboter läuft etwa 10-100x langsamer als nativer Maschinencode, weil:

Warum das für diese Vorlesung wichtig ist:

Wenn wir C++-Code schreiben und zu Maschinencode kompilieren, umgehen wir den Roboter vollständig. Die CPU führt unsere Anweisungen direkt aus—keine Interpretation, kein Stack-Manipulations-Overhead, keine Laufzeit-Typprüfung. Deshalb kann C++ für rechenintensive Aufgaben 10-100x schneller sein.

Der tatsächliche Quellcode:

Der Kern der PVM ist in Python/ceval.c — eine riesige Switch-Anweisung mit tausenden Zeilen:

// Simplified view of ceval.c
for (;;) {
    switch (opcode) {
        case LOAD_CONST:
            value = constants[oparg];
            PUSH(value);
            break;
        case BINARY_ADD:
            right = POP();
            left = POP();
            result = PyNumber_Add(left, right);
            PUSH(result);
            break;
        // ... hundreds more cases ...
    }
}

Diese Schleife läuft kontinuierlich, während Ihr Python-Programm ausgeführt wird.

4.1.5 Die Schritte des Interpreters

Der Interpreter macht mehrere Dinge:

  1. Lexikalische Analyse: Zerlegt Quellcode in Token
  2. Parsing: Baut einen Abstract Syntax Tree (AST)
  3. Kompilierung zu Bytecode: Übersetzt AST in Bytecode-Anweisungen
  4. Ausführung: Die PVM führt Bytecode aus

4.1.6 Python mit uv ausführen

In diesem Kurs verwenden wir uv zur Verwaltung von Python-Umgebungen. Wenn Sie ausführen:

uv run main.py

Folgendes passiert bevor Python überhaupt startet:

┌─────────────────┐
│   uv run        │  ← uv-Befehl
│   main.py       │
└────────┬────────┘
         │
         ▼
┌─────────────────────────────────────────────────────┐
│ 1. uv liest pyproject.toml                          │
│ 2. uv prüft, ob virtuelle Umgebung existiert        │
│ 3. uv erstellt/aktualisiert venv bei Bedarf         │
│ 4. uv installiert fehlende Abhängigkeiten           │
│ 5. uv aktiviert die virtuelle Umgebung              │
└────────┬────────────────────────────────────────────┘
         │
         ▼
┌─────────────────┐
│ python main.py  │  ← Jetzt läuft Python (wie oben)
│ (in venv)       │
└─────────────────┘

Hauptunterschiede zwischen python main.py und uv run main.py:

Aspekt python main.py uv run main.py
Umgebung Verwendet das Python im PATH Verwendet die virtuelle Umgebung des Projekts
Abhängigkeiten Müssen manuell installiert werden Automatisch aus pyproject.toml installiert
Reproduzierbarkeit Hängt vom Systemzustand ab Konsistent über Maschinen hinweg
Isolation Kann mit Systempaketen kollidieren Isolierte virtuelle Umgebung
Erster Durchlauf Schlägt fehl, wenn Abhängigkeiten fehlen Installiert Abhängigkeiten automatisch

Warum das für mehrsprachige Projekte wichtig ist:

Wenn wir C++-Code mit pybind11 hinzufügen, stellt uv run sicher:

Hauptmerkmale der interpretierten Ausführung:

4.1.7 Vertiefende Ressourcen

Wenn Sie Pythons Interna besser verstehen möchtest:

Offizielle Dokumentation:

Bücher:

Artikel und Vorträge:

Quellcode:

4.2 Wie C++ Code ausführt

Voraussetzungen für diesen Abschnitt

Dieser Abschnitt setzt voraus, dass Sie bereits kleine C++-Programme geschrieben hast—vielleicht ein “Hello World”, eine einfache Schleife oder eine Funktion, die etwas berechnet. Wir lehren hier keine C++-Syntax; dieser Kurs ist kein Ersatz für einen dedizierten C++-Programmierkurs.

Stattdessen erkunden wir die Mechanismen, die C++ grundlegend von Python unterscheiden: wie Ihr Quellcode zu einem ausführbaren Programm wird, das direkt auf Hardware läuft. Das Verständnis dieses Prozesses ist essentiell, bevor wir Python und C++ in der nächsten Vorlesung verbinden können.

4.2.1 Was ist ein Compiler?

Ein Compiler ist ein Programm—ja, nur ein weiteres Stück Software—das Quellcode, geschrieben in einer höheren Programmiersprache, in Maschinencode übersetzt, den der Prozessor eines Computers direkt ausführen kann.

Das fundamentale Problem, das Compiler lösen:

Menschen denken in Abstraktionen: Variablen, Funktionen, Schleifen, Bedingungen. CPUs verstehen nur binäre Anweisungen: “lade diesen Wert von Speicheradresse X”, “addiere diese beiden Register”, “springe zu Anweisung Y, wenn das Ergebnis Null ist.”

Programme direkt in Maschinencode zu schreiben (oder sogar in Assemblersprache) ist mühsam, fehleranfällig und architekturspezifisch. Ein Programm, das für eine Intel-CPU geschrieben wurde, läuft nicht auf einem ARM-Prozessor ohne vollständig neu geschrieben zu werden.

Die Aufgabe des Compilers ist es, diese Lücke zu überbrücken:

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│   Menschenlesbarer Code          Maschinenausführbarer Code         │
│                                                                     │
│   int add(int a, int b) {      01001000 10001001 11111000           │
│       return a + b;      →     00001001 11110000                    │
│   }                            11000011                             │
│                                                                     │
│   (C++ Quellcode)              (x86-64 Maschinencode)               │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Historischer Kontext: Der erste Compiler wurde von Grace Hopper in den frühen 1950er Jahren entwickelt. Bevor Compiler existierten, schrieben Programmierer Maschinencode von Hand—ein unglaublich langsamer und fehleranfälliger Prozess. Hoppers Einsicht war revolutionär: lass ein Programm die Übersetzung automatisch machen.

4.2.2 Verschiedene C++ Compiler

Anders als bei Python, wo CPython die dominante Implementierung ist, hat das C++-Ökosystem mehrere wichtige Compiler:

Compiler Befehl Plattform Anmerkungen
GCC (GNU Compiler Collection) g++ Linux, macOS, Windows (MinGW) Open Source, weit verbreitet in Akademie und Linux-Entwicklung
Clang (LLVM) clang++ Linux, macOS, Windows Modern, exzellente Fehlermeldungen, verwendet von Apple/Google
MSVC (Microsoft Visual C++) cl.exe Windows Integriert in Visual Studio, dominant unter Windows

Warum ist das wichtig?

  1. Portabilität: Gut geschriebener C++-Code kompiliert mit jedem standardkonformen Compiler
  2. Verhaltensunterschiede: Compiler können leicht unterschiedliche StandarIhrstellungen oder Erweiterungen haben
  3. Fehlermeldungen: Clang ist bekannt für klarere, hilfreichere Fehlermeldungen als GCC
  4. Optimierung: Verschiedene Compiler können unterschiedlich optimierten Code produzieren
  5. Plattformbeschränkungen: Ihre Zielplattform kann den Compiler diktieren (eingebettete Systeme verwenden oft spezifische Toolchains)

In diesem Kurs werden wir GCC (g++) verwenden, weil er auf allen wichtigen Plattformen verfügbar ist und auf den meisten Linux-Systemen der Standard ist. Die Konzepte gelten gleichermaßen für Clang und MSVC.

4.2.3 Überblick über den Build-Prozess

Wenn Sie ein C++-Programm bauen und ausführen, verwenden Sie zwei separate Befehle:

g++ -o main main.cpp   # Schritt 1: Kompilieren (erstellt Executable)
./main                  # Schritt 2: Ausführen (führt das Programm aus)

Schritt 1: Der Kompilierbefehl

Teil Bedeutung
g++ Der Compiler-Befehl (GCCs C++-Compiler). Sie könnten auch clang++ für Clang verwenden.
-o main Das Ausgabe-Flag (-o) gefolgt vom gewünschten Namen des Executables (main). Ohne dies erstellt GCC standardmäßig eine Datei namens a.out.
main.cpp Die Quelldatei zum Kompilieren. Das ist Ihr C++-Code.

Schritt 2: Das Executable ausführen

Nach erfolgreicher Kompilierung haben Sie eine neue Datei namens main (oder main.exe unter Windows). Das ist ein eigenständiges Executable—nativer Maschinencode, der direkt auf Ihrer CPU läuft.

Teil Bedeutung
./main Führe das Executable aus. Das ./-Präfix sagt der Shell, im aktuellen Verzeichnis zu suchen (Linux/macOS). Unter Windows würden Sie einfach main.exe oder .\main.exe eingeben.

Wichtig: Anders als bei Python, wo Sie jedes Mal python script.py ausführst, braucht ein kompiliertes C++-Programm den Compiler nicht zum Ausführen. Einmal kompiliert, können Sie main auf eine andere Maschine kopieren (mit dem gleichen OS/Architektur) und direkt ausführen—kein Compiler oder Entwicklungswerkzeuge erforderlich.

Aber warte—was ist mit Header-Dateien?

Wenn Sie schon C++ geschrieben hast, wissen Sie, dass Projekte typischerweise Header-Dateien (.hpp oder .h) haben, die Funktionen, Klassen und Typen deklarieren. Wo passen die hin?

Es gibt viele weitere Compiler-Optionen:

Der einfache Befehl oben versteckt viel Komplexität. In echten Projekten werden Sie zusätzliche Flags verwenden:

Für jetzt konzentrieren wir uns auf das große Ganze. Dieser einzelne Befehl löst tatsächlich einen mehrstufigen Prozess aus:

📄
main.cpp
Ihr Quellcode (Textdatei)
📋
Präprozessor
Behandelt #include, #define (Textersetzung)
Compiler
Konvertiert C++ zu Maschinencode (g++, clang)
📦
Objektdateien
Binärcode für jede Quelldatei (.o, .obj)
🔗
Linker
Kombiniert Objektdateien, löst Referenzen auf
🚀
Executable
Nativer Maschinencode (läuft direkt auf CPU)
Ergebnisse

Hauptmerkmale:

4.3 Warum das für eingebettete Systeme wichtig ist

Aspekt Python C++
Laufzeitanforderungen Python-Interpreter (10-50 MB) Keine (eigenständige Binärdatei)
Speicher-Overhead Hoch (Objekte haben Metadaten) Niedrig (Rohdaten)
Ausführungsgeschwindigkeit 10-100x langsamer Native Geschwindigkeit
Startzeit Langsam (Interpreter-Initialisierung) Schnell (sofortige Ausführung)
Vorhersagbarkeit GC-Pausen können Jitter verursachen Deterministisches Timing

Für die 200 MHz ECU mit 256 KB RAM passt Python einfach nicht. Aber ein kompiliertes C++-Programm kann in Kilobytes RAM mit Mikrosekunden-genauem Timing laufen.


5. Der Build-Prozess: Von Quellcode zum Executable

Eine Anmerkung zum Umfang

Dies ist ein Software-Engineering-Kurs—kein Kurs über Compiler oder Computerarchitektur. Wir versuchen nicht, Sie zu Compiler-Ingenieuren zu machen oder Ihnen beizubringen, wie Sie Ihren eigenen Compiler schreibst.

Stattdessen konsolidiert dieser Abschnitt Wissen, das Sie wahrscheinlich in anderen Kursen oder durch Selbststudium kennengelernt hast. Das Ziel ist, ein gemeinsames mentales Modell zu etablieren, sodass wenn wir mehrsprachige Projekte, Build-Systeme und Integrationsherausforderungen besprechen, alle auf dem gleichen Stand sind.

Wenn einiges davon Wiederholung ist, prima—nutzen Sie es, um Ihr Verständnis zu verstärken. Wenn es neu ist, machen Sie sich keine Sorgen, jedes Detail zu memorieren. Konzentrieren Sie sich auf das große Ganze: wie Quellcode zu einem Executable wird und warum das wichtig ist, wenn Python und C++ kombiniert werden.

Lass uns jeden Schritt des C++ Build-Prozesses durchgehen.

5.1 Präprozessierung

Der Präprozessor behandelt Direktiven, die mit # beginnen:

// main.cpp
#include <iostream>           // ← Standard-Bibliotheks-Header einbinden
#include "geometry.hpp"       // ← Unseren Projekt-Header einbinden

#define PI 3.14159265359      // ← Textersetzung

int main() {
    std::cout << "Pi = " << PI << std::endl;
    return 0;
}

Der Präprozessor:

  1. Kopiert den gesamten Inhalt von <iostream> in Ihre Datei
  2. Kopiert den gesamten Inhalt von geometry.hpp in Ihre Datei
  3. Ersetzt jedes PI durch 3.14159265359

Die Ausgabe ist eine einzelne, erweiterte Datei ohne #include oder #define übrig.

5.2 Kompilierung

Erinnern Sie sich an Abschnitt 4.2.3: Überblick über den Build-Prozess, dass Kompilierung eine Stufe in einem dreistufigen Prozess ist: Präprozessierung → Kompilierung → Linking. Hier untersuchen wir die Kompilierungsstufe genauer.

Der Compiler konvertiert vorverarbeiteten C++-Code zu Objektcode—Maschinenanweisungen für eine spezifische Prozessorarchitektur (x86-64, ARM, etc.).

Der Nur-Kompilieren-Befehl:

g++ -c main.cpp -o main.o

Das -c-Flag sagt dem Compiler: “nur kompilieren, nicht linken.” Das produziert eine Objektdatei (main.o) statt eines Executables.

Was der Compiler intern macht:

  1. Lexikalische Analyse: Zerlegt Quellcode in Token (Schlüsselwörter, Bezeichner, Operatoren)
  2. Parsing: Baut einen Abstract Syntax Tree (AST), der die Code-Struktur repräsentiert
  3. Semantische Analyse: Prüft Typen, löst Namen auf, setzt Sprachregeln durch
  4. Optimierung: Verbessert Performance (wenn -O1, -O2 oder -O3-Flags verwendet werden)
  5. Code-Generierung: Gibt Maschinenanweisungen für die Zielarchitektur aus

Das ist komplexer als Pythons Bytecode-Kompilierung, weil die Ausgabe direkt auf der CPU laufen muss—es gibt keine virtuelle Maschine, die sie interpretiert.

Was in einer Objektdatei (.o) ist:

Das produziert main.o, eine Binärdatei, die enthält:

Warum Objektdateien NICHT ausführbar sind:

Objektdateien können Funktionen referenzieren, die in anderen Dateien definiert sind. Zum Beispiel könnte main.o calculate_ray_line() aufrufen, die in geometry.o definiert ist. Der Compiler weiß nicht, wo diese Funktion im Speicher sein wird—nur der Linker löst diese dateiübergreifenden Referenzen auf.

🔗 Vor dem Linken: Unaufgelöste Referenzen
📄 main.o
main()
call ??? (unbekannt)
📄 geometry.o
calculate_ray_line()
✓ hier definiert
🔧 Linker löst "???" zur tatsächlichen Adresse auf

Diese Trennung ermöglicht inkrementelle Builds: wenn Sie geometry.cpp änderst, muss nur geometry.o neu kompiliert werden. Die unveränderte main.o wird wiederverwendet.

5.3 Linken

Nach dem Kompilieren jeder Quelldatei zu einer Objektdatei (mit g++ -c) kombiniert der Linker sie zu einem einzelnen Executable.

Der vollständige manuelle Workflow:

So bauen Sie ein Mehrfachdatei-Projekt Schritt für Schritt:

# Schritt 1: Jede Quelldatei zu einer Objektdatei kompilieren
g++ -c main.cpp -o main.o           # Erstellt main.o
g++ -c geometry.cpp -o geometry.o   # Erstellt geometry.o

# Schritt 2: Alle Objektdateien zu einem Executable linken
g++ main.o geometry.o -o main       # Erstellt Executable 'main'

# Schritt 3: Das Executable ausführen
./main                              # Programm ausführen

Beachten Sie, dass wir in Schritt 2 g++ wieder verwenden, aber ohne das -c-Flag. Wenn Sie .o-Dateien an g++ übergibst, weiß es, den Linker statt den Compiler aufzurufen.

Was der Linker macht:

  1. Liest alle Objektdateien: Lädt Maschinencode und Symboltabellen aus jeder .o-Datei
  2. Löst Symbol-Referenzen auf: Ordnet Funktionsaufrufe ihren Definitionen zu
    • main.o ruft calculate_ray_line() auf → Linker findet sie in geometry.o
  3. Weist finale Speicheradressen zu: Entscheidet, wo jede Funktion und Variable im Speicher leben wird
  4. Schreibt das Executable: Kombiniert alles in einer einzelnen Binärdatei
⚙️ Linken: Objektdateien kombinieren
📄 main.o
main()
ruft calculate_ray() auf
📄 geometry.o
calculate_ray()
🎯
main (Executable)
✓ Aller Code gelinkt ✓ Alle Adressen aufgelöst

Häufige Linker-Fehler:

Wenn das Linken fehlschlägt, kommen die Fehlermeldungen vom Linker, nicht vom Compiler. Hier sind die häufigsten:

Fehler Bedeutung Typische Ursache
undefined reference to 'function_name' Linker kann die Definition der Funktion nicht finden Vergessen, die .cpp-Datei zu kompilieren/linken, die sie definiert, oder Funktionsname falsch geschrieben
multiple definition of 'function_name' Gleiche Funktion in mehreren Objektdateien definiert Funktion in Header-Datei definiert (sollte nur deklariert sein) oder gleiche .cpp zweimal eingebunden
undefined reference to 'main' Keine main()-Funktion gefunden Vergessen, die Datei mit main() einzubinden, oder main falsch geschrieben

Warum separate Kompilier- und Link-Schritte?

In Abschnitt 5.2 haben wir inkrementelle Builds erwähnt. Hier ist der praktische Nutzen:

# Initialer Build: alles kompilieren
g++ -c main.cpp -o main.o           # 2 Sekunden
g++ -c geometry.cpp -o geometry.o   # 2 Sekunden
g++ main.o geometry.o -o main       # 0.5 Sekunden
# Gesamt: 4.5 Sekunden

# Nach Änderung NUR von geometry.cpp:
g++ -c geometry.cpp -o geometry.o   # 2 Sekunden (geänderte Datei neu kompilieren)
g++ main.o geometry.o -o main       # 0.5 Sekunden (neu linken)
# Gesamt: 2.5 Sekunden (main.o wiederverwendet!)

Für große Projekte mit Hunderten von Dateien spart das enorme Zeit. Build-Systeme wie Make und CMake automatisieren dieses Abhängigkeits-Tracking.

Die Abkürzung (für kleine Projekte):

Für einfache Projekte können Sie die manuellen Schritte überspringen und g++ alles handhaben lassen:

# Das macht Präprozessierung, Kompilierung UND Linken in einem Befehl
g++ main.cpp geometry.cpp -o main

Das ist bequem, gibt Ihnen aber keine inkrementellen Builds—jede Datei wird jedes Mal neu kompiliert.

5.4 Vertiefende Ressourcen

Der Build-Prozess ist ein reichhaltiges Thema, das Compiler, Betriebssysteme und Computerarchitektur umspannt. Hier sind sorgfältig ausgewählte Ressourcen, wenn Sie tiefer gehen möchtest:

Offizielle Dokumentation:

Bücher (für ernsthaftes Studium):

Artikel und Tutorials:

Objektdatei-Formate verstehen:

Verschiedene Betriebssysteme verwenden verschiedene Executable-Formate:

Format Plattform Werkzeuge zur Inspektion
ELF (Executable and Linkable Format) Linux, BSD, eingebettete Systeme readelf, objdump, nm
Mach-O macOS, iOS otool, nm, lipo
PE/COFF (Portable Executable) Windows dumpbin (MSVC), objdump (MinGW)

Probiere es selbst aus:

# Mit Debug-Info kompilieren
g++ -c -g main.cpp -o main.o

# Symbole in der Objektdatei auflisten
nm main.o

# Abschnitts-Header und Größen anzeigen
objdump -h main.o

# Disassemblieren, um den tatsächlichen Maschinencode zu sehen
objdump -d main.o

Diese Befehle lassen Sie in Objektdateien hineinschauen und genau sehen, was der Compiler produziert hat.


6. Make und Makefiles: Das traditionelle Build-Werkzeug

Bevor wir CMake einführen, lassen Sie uns verstehen, warum Build-Automatisierungswerkzeuge überhaupt erfunden wurden. Dieser Kontext hilft Ihnen zu verstehen, was CMake leistet—und warum Sie Makefiles nicht von Hand für neue Projekte schreiben solltest.

Hinweis zu Build-Systemen: CMake ist heute eines der am weitesten verbreiteten Build-Systeme im C++-Ökosystem und wird von großen Projekten wie LLVM, Qt und OpenCV verwendet. Es ist jedoch nicht die einzige Option—Alternativen wie Bazel (Google), Meson und xmake gewinnen an Bedeutung. Wir konzentrieren uns auf CMake wegen seiner weiten Verbreitung in der Industrie und der hervorragenden Werkzeugunterstützung.

6.1 Das Problem: Manuelle Kompilierung skaliert nicht

In Abschnitt 5.3 haben wir den manuellen Workflow für ein Zwei-Dateien-Projekt gezeigt:

g++ -c main.cpp -o main.o
g++ -c geometry.cpp -o geometry.o
g++ main.o geometry.o -o main

Drei Befehle. Überschaubar. Aber was passiert, wenn Ihr Projekt wächst?

Ein realistisches kleines Projekt (10 Dateien):

g++ -c -Wall -Wextra -std=c++20 -I./include src/main.cpp -o build/main.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/geometry.cpp -o build/geometry.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/road_profile.cpp -o build/road_profile.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/camera.cpp -o build/camera.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/ray_tracer.cpp -o build/ray_tracer.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/visualization.cpp -o build/visualization.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/config.cpp -o build/config.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/utils.cpp -o build/utils.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/file_io.cpp -o build/file_io.o
g++ -c -Wall -Wextra -std=c++20 -I./include src/math_helpers.cpp -o build/math_helpers.o
g++ build/main.o build/geometry.o build/road_profile.o build/camera.o \
    build/ray_tracer.o build/visualization.o build/config.o build/utils.o \
    build/file_io.o build/math_helpers.o -o build/road_profile_viewer

Das sind 11 Befehle, die Sie jedes Mal eingeben musst, wenn Sie neu baust. Und wir haben noch nicht einmal hinzugefügt:

Der echte Alptraum: Abhängigkeitsverfolgung

Angenommen, Sie ändern geometry.hpp. Welche Dateien müssen neu kompiliert werden? Jede .cpp-Datei, die sie einbindet—direkt oder indirekt. Können Sie sich merken, welche das sind? Können Sie sich selbst vertrauen, alle davon und nur diese neu zu kompilieren?

geometry.hpp wird eingebunden von:
├── geometry.cpp (direkt)
├── ray_tracer.cpp (direkt)
├── visualization.cpp (bindet ray_tracer.hpp ein, die geometry.hpp einbindet)
└── main.cpp (bindet visualization.hpp ein, die...)

Wenn Sie vergessen, visualization.cpp neu zu kompilieren, könnte Ihr Programm:

Deshalb wurde Make erfunden.

6.2 Was ist Make?

make ist ein Build-Automatisierungswerkzeug, das 1976 bei Bell Labs entwickelt wurde. Es löst die zwei grundlegenden Probleme der manuellen Kompilierung:

  1. Automatisierung: Sie definieren die Build-Regeln einmal, dann führen Sie einen einzelnen Befehl aus
  2. Inkrementelle Builds: Make verfolgt Datei-Änderungszeiten und baut nur das neu, was sich geändert hat

Die zentrale Erkenntnis: Make behandelt das Bauen von Software als einen Abhängigkeitsgraphen. Jede Datei hängt von anderen Dateien ab. Wenn sich eine Datei ändert, muss alles, was von ihr abhängt, neu gebaut werden.

              ┌─────────────┐
              │    main     │  (ausführbare Datei)
              │  (Ziel)     │
              └──────┬──────┘
                     │ hängt ab von
         ┌───────────┼───────────┐
         │           │           │
         ▼           ▼           ▼
    ┌─────────┐ ┌─────────┐ ┌─────────┐
    │ main.o  │ │geometry.o│ │ utils.o │  (Objektdateien)
    └────┬────┘ └────┬────┘ └────┬────┘
         │           │           │
         ▼           ▼           ▼
    ┌─────────┐ ┌─────────┐ ┌─────────┐
    │main.cpp │ │geometry │ │utils.cpp│  (Quelldateien)
    │         │ │.cpp/.hpp│ │         │
    └─────────┘ └─────────┘ └─────────┘

Wenn sich geometry.hpp ändert:

  1. Make sieht, dass geometry.o davon abhängt → geometry.cpp neu kompilieren
  2. Make sieht, dass main von geometry.o abhängt → neu linken
  3. Make sieht, dass utils.o nicht davon abhängt → überspringen (Zeit gespart!)

6.3 Grundlegende Makefile-Struktur

# Makefile

# Compiler-Einstellungen
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20

# Ziel: Abhängigkeiten
#     Befehl zum Bauen des Ziels

main: main.o geometry.o
	$(CXX) $(CXXFLAGS) main.o geometry.o -o main

main.o: main.cpp geometry.hpp
	$(CXX) $(CXXFLAGS) -c main.cpp -o main.o

geometry.o: geometry.cpp geometry.hpp
	$(CXX) $(CXXFLAGS) -c geometry.cpp -o geometry.o

clean:
	rm -f *.o main

Wie es funktioniert:

  1. Sie führen make main aus
  2. Make prüft, ob main älter ist als main.o oder geometry.o
  3. Falls ja, prüft es rekursiv diese Abhängigkeiten
  4. Es baut nur das neu, was nötig ist

6.4 Warum Makefiles nicht ausreichen

Make war 1976 revolutionär. Aber die Softwareentwicklung hat sich seitdem dramatisch verändert. Hier ist, warum das manuelle Schreiben von Makefiles für moderne Projekte problematisch ist:

Problem 1: Nicht portabel zwischen Betriebssystemen

Make verwendet Shell-Befehle direkt. Dieses Makefile funktioniert auf Linux/macOS:

clean:
	rm -f *.o main

Aber auf Windows gibt es keinen rm-Befehl. Sie bräuchten:

clean:
	del /Q *.o main.exe

Jetzt brauchen Sie zwei verschiedene Makefiles oder komplexe bedingte Logik. Und das ist nur für ein einfaches clean-Ziel.

Problem 2: Manuelle Header-Abhängigkeitsverfolgung

Erinnern Sie sich an unsere Regel?

geometry.o: geometry.cpp geometry.hpp
	$(CXX) $(CXXFLAGS) -c geometry.cpp -o geometry.o

Was, wenn geometry.hpp math_types.hpp einbindet? Sie müssen die Regel aktualisieren:

geometry.o: geometry.cpp geometry.hpp math_types.hpp
	$(CXX) $(CXXFLAGS) -c geometry.cpp -o geometry.o

Stell dir jetzt 50 Quelldateien vor, jede bindet 5-10 Header ein, von denen einige andere Header einbinden. Sie müssen jede transitive Abhängigkeit manuell verfolgen. Wenn Sie eine vergessen, wird das Ändern eines Headers keine Neukompilierung auslösen, und du bekommst mysteriöse Bugs.

(Es gibt Workarounds mit GCCs -MMD-Flag zur automatischen Abhängigkeitsgenerierung, aber sie sind umständlich und erfordern zusätzliche Makefile-Komplexität.)

Problem 3: Ausführlich und repetitiv

Unser einfaches Makefile hat explizite Regeln für jede Quelldatei. Ein echtes Projekt mit 100 Quelldateien würde… 100 Regeln brauchen. Sie können “Pattern Rules” verwenden, um Wiederholungen zu reduzieren:

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

Aber jetzt verlierst du explizite Abhängigkeitsverfolgung. Und Pattern Rules haben subtile Fallstricke, die selbst erfahrene Entwickler verwirren.

Problem 4: Verschiedene Compiler brauchen verschiedene Flags

Was, wenn Ihr Projekt sowohl GCC als auch Clang auf Linux unterstützen muss, plus MSVC auf Windows?

# Das wird schnell hässlich
ifeq ($(CXX),g++)
    WARNINGS = -Wall -Wextra
else ifeq ($(CXX),clang++)
    WARNINGS = -Wall -Wextra
else ifeq ($(CXX),cl)
    WARNINGS = /W4
endif

Multipliziere das jetzt mit jeder Compiler-Flag-Kategorie (Optimierung, Debugging, Standardkonformität…).

Problem 5: Kein standardisierter Weg, externe Bibliotheken zu finden

Willst du OpenCV in Ihrem Projekt verwenden? Auf Linux könnten die Header in /usr/include/opencv4 sein. Auf macOS mit Homebrew sind sie in /opt/homebrew/include/opencv4. Auf Windows, wer weiß?

# Hardcodierte Pfade, die auf anderen Maschinen fehlschlagen
OPENCV_INCLUDE = /usr/include/opencv4
OPENCV_LIBS = -lopencv_core -lopencv_imgproc

Dieses Makefile funktioniert auf Ihrer Maschine, scheitert aber auf dem Laptop Ihres Kollegen.

Problem 6: Keine IDE-Integration

Moderne IDEs (VS Code, CLion, Visual Studio) können Makefiles nicht lesen, um Ihre Projektstruktur zu verstehen. Sie können nicht bieten:

Das Fazit:

Make löste das Problem von 1976 brillant. Aber moderne C++-Entwicklung braucht:

Deshalb wurde CMake entwickelt—und warum praktisch alle modernen C++-Projekte es verwenden.

6.5 Weiterführende Literatur

Wenn du Make tiefergehend verstehen willst (nützlich für die Wartung von Legacy-Projekten):

Es folgt CMake.


7. CMake: Plattformübergreifende Build-Konfiguration

In Abschnitt 6.4 haben wir sechs grundlegende Probleme mit Makefiles identifiziert:

  1. Nicht portabel zwischen Betriebssystemen
  2. Manuelle Header-Abhängigkeitsverfolgung
  3. Ausführlich und repetitiv
  4. Verschiedene Compiler brauchen verschiedene Flags
  5. Kein standardisierter Weg, externe Bibliotheken zu finden
  6. Keine IDE-Integration

CMake wurde speziell entwickelt, um diese Probleme zu lösen. Lass uns sehen, wie.

7.1 Was ist CMake?

CMake ist ein Meta-Build-System. Es kompiliert Ihren Code nicht direkt—stattdessen generiert es Build-Dateien für andere Werkzeuge:

┌─────────────────────────────────────────────────────────────────────┐
│                        CMakeLists.txt                               │
│              (eine Datei, funktioniert auf allen Plattformen)       │
└─────────────────────────┬───────────────────────────────────────────┘
                          │
                          │  cmake  (liest CMakeLists.txt)
                          │
        ┌─────────────────┼─────────────────┐
        │                 │                 │
        ▼                 ▼                 ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│   Makefile    │ │  Ninja-Dateien│ │ Visual Studio │
│   (Linux)     │ │  (Linux/Mac)  │ │   Projekt     │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
        │                 │                 │
        ▼                 ▼                 ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│     make      │ │    ninja      │ │   MSBuild     │
│ (kompiliert)  │ │ (kompiliert)  │ │ (kompiliert)  │
└───────────────┘ └───────────────┘ └───────────────┘

Was ist Ninja?

Du wirst “Ninja” im Diagramm bemerken. Ninja ist ein kleines, schnelles Build-System, das 2012 von einem Google-Ingenieur entwickelt wurde. Während Make aus dem Jahr 1976 stammt und viele Funktionen hat, konzentriert sich Ninja auf eine Sache: Geschwindigkeit.

Aspekt Make Ninja
Design-Ziel Allgemeine Build-Automatisierung Maximale Build-Geschwindigkeit
Menschenlesbare Dateien Ja (Makefiles sind zum Bearbeiten gedacht) Nein (Ninja-Dateien werden generiert, nicht von Hand geschrieben)
Startzeit ~100-500ms für große Projekte ~10-50ms (10x schneller)
Parallele Builds Erfordert make -j Flag Standardmäßig parallel
Typische Verwendung Eigenständig oder mit CMake Immer mit einem Generator (CMake, Meson, gn)

Warum ist Geschwindigkeit wichtig? Bei großen Projekten (Millionen von Codezeilen) kann Make Sekunden brauchen, nur um festzustellen, dass nichts neu gebaut werden muss. Ninja macht das in Millisekunden. Für iterative Entwicklung—eine Zeile ändern, neu bauen, testen—summiert sich das schnell.

Empfehlung: Für neue Projekte verwende Ninja als Ihren CMake-Generator, wenn verfügbar. Auf Linux/macOS: cmake -G Ninja ... Auf Windows mit Visual Studio ist der Standard-Generator normalerweise ausreichend.

Sie schreiben eine CMakeLists.txt-Datei, und CMake generiert das passende Build-System für die Plattform, auf der Sie sich befinden.

7.2 Wie CMake unsere Probleme löst

Für Python-Entwickler: CMakeLists.txt ist wie pyproject.toml

Wenn du von Python kommst, verstehst du bereits das Konzept einer Projektdefinitionsdatei. Hier ist, wie die pyproject.toml des Road-Profile-Viewers auf eine äquivalente C++ CMakeLists.txt abgebildet wird:

Konzept pyproject.toml (Python) CMakeLists.txt (C++)
Projekt-Metadaten [project]
name = "road-profile-viewer"
version = "0.1.0"
project(road_profile_viewer
VERSION 1.0.0
LANGUAGES CXX)
Sprachversion requires-python = ">=3.12" set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Abhängigkeiten dependencies = [
"numpy>=1.26.0",
"dash>=2.14.0",
]
find_package(OpenCV REQUIRED)
oder
FetchContent_Declare(json ...)
Build-System [build-system]
requires = ["uv_build"]
build-backend = "uv_build"
cmake_minimum_required(VERSION 3.16)
(CMake generiert Makefiles, Ninja, VS-Projekte)
Einstiegspunkt [project.scripts]
road-profile-viewer = "..."
add_executable(main src/main.cpp)
Code-Qualitätswerkzeuge [tool.ruff]
[tool.pyright]
.clang-format
.clang-tidy
(separate Konfigurationsdateien)
Entwicklungs-Abhängigkeiten [dependency-groups]
dev = ["pytest", "ruff"]
FetchContent_Declare(googletest ...)
(oft bedingt eingebunden)

Die zentrale Erkenntnis: CMakeLists.txt dient dem gleichen Zweck wie pyproject.toml—sie ist die einzige Quelle der Wahrheit dafür, wie Ihr Projekt gebaut wird, wovon es abhängt und wie es konfiguriert werden soll. Die Syntax ist anders, aber die Konzepte lassen sich direkt übertragen.

Lass uns nun sehen, wie CMake jedes der Make-Probleme löst, die wir in Abschnitt 6.4 identifiziert haben.

Problem 1: Nicht portabel zwischen Betriebssystemen

Erinnern Sie sich an unser Makefile-clean-Ziel?

# Makefile (Linux/macOS)           # Makefile (Windows)
clean:                              clean:
    rm -f *.o main                      del /Q *.o main.exe

Mit CMake beschreibst du was gebaut werden soll, nicht wie es gebaut werden soll:

# CMakeLists.txt (funktioniert überall!)
add_executable(main src/main.cpp src/geometry.cpp)

CMake weiß, dass es auf Windows main.exe generieren soll, cl.exe für die Kompilierung verwenden und eine Visual-Studio-Solution erstellen soll. Auf Linux generiert es ein Makefile, das g++ verwendet und main erstellt. Sie schreiben niemals plattformspezifische Befehle.

Problem 2: Manuelle Header-Abhängigkeitsverfolgung

In Make mussten wir jeden Header manuell auflisten:

# Makefile - DU musst alle Header manuell verfolgen
geometry.o: geometry.cpp geometry.hpp math_types.hpp utils.hpp
    $(CXX) $(CXXFLAGS) -c geometry.cpp -o geometry.o

CMake scannt Ihre Quelldateien automatisch und verfolgt Abhängigkeiten:

# CMakeLists.txt - CMake behandelt Abhängigkeiten automatisch
add_library(geometry src/geometry.cpp)
target_include_directories(geometry PUBLIC include)

Wenn du math_types.hpp änderst, weiß CMake (über das generierte Build-System) genau, welche Dateien neu kompiliert werden müssen. Du pflegst niemals manuell Abhängigkeitslisten.

Problem 3: Ausführlich und repetitiv

In Abschnitt 6.1 erforderte unser 10-Dateien-Makefile 11 explizite Befehle. Selbst für unseren einfacheren Road-Profile-Viewer C++-Port (derzeit nur geometry.cpp und main.cpp) ist der Makefile-Ansatz ausführlich:

# Makefile für road-profile-viewer
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -I./include

geometry.o: src/geometry.cpp include/geometry.hpp
	$(CXX) $(CXXFLAGS) -c src/geometry.cpp -o geometry.o

main.o: src/main.cpp include/geometry.hpp
	$(CXX) $(CXXFLAGS) -c src/main.cpp -o main.o

main: main.o geometry.o
	$(CXX) main.o geometry.o -o main

clean:
	rm -f *.o main

Mit CMake wird das gleiche Projekt deklarativ beschrieben:

# CMakeLists.txt - gleiches Projekt, viel sauberer
add_library(geometry_core src/geometry.cpp)
target_include_directories(geometry_core PUBLIC include)

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE geometry_core)

Vier Zeilen statt zwölf. Und wenn Ihr Projekt auf 10, 50 oder 100 Dateien wächst, skaliert CMake elegant—du fügst einfach Dateien zum entsprechenden add_library()- oder add_executable()-Befehl hinzu.

Problem 4: Verschiedene Compiler brauchen verschiedene Flags

Erinnern Sie sich an die hässlichen Bedingungen für GCC vs. Clang vs. MSVC?

# Makefile - manuelle Compiler-Erkennung
ifeq ($(CXX),g++)
    WARNINGS = -Wall -Wextra
else ifeq ($(CXX),cl)
    WARNINGS = /W4
endif

CMake bietet “Generator Expressions”, die sich automatisch an den Compiler anpassen:

# CMakeLists.txt - CMake behandelt Compiler-Unterschiede
target_compile_options(geometry PRIVATE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

Oder noch einfacher, CMakes moderner Ansatz verwendet abstrakte Eigenschaften:

# Lass CMake passende Flags für den Standard wählen
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

CMake weiß, dass GCC -std=c++20 braucht, Clang -std=c++20, und MSVC /std:c++20.

Problem 5: Kein standardisierter Weg, externe Bibliotheken zu finden

In Make haben wir Pfade hardcodiert, die auf anderen Maschinen fehlschlugen:

# Makefile - hardcodierte Pfade (scheitert auf Kollegen-Maschine)
OPENCV_INCLUDE = /usr/include/opencv4
OPENCV_LIBS = -lopencv_core -lopencv_imgproc

CMake hat ein Paket-Erkennungssystem:

# CMakeLists.txt - funktioniert auf jeder Maschine mit installiertem OpenCV
find_package(OpenCV REQUIRED)
target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})

find_package() durchsucht Standard-Speicherorte auf jeder Plattform:

Ihr Kollege führt cmake .. aus und es funktioniert einfach.

Problem 6: Keine IDE-Integration

Moderne IDEs verstehen CMake nativ:

Das liegt daran, dass CMake eine compile_commands.json-Datei generiert, die IDEs mitteilt:

# compile_commands.json für IDE-Integration generieren
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..

Alles zusammenfügen: Die vollständige CMakeLists.txt

Nachdem wir gesehen haben, wie CMake jedes Problem löst, hier die vollständige CMakeLists.txt für unseren Road-Profile-Viewer C++-Port. Das ist die Datei, die du in Ihrem Projekt erstellen wirst:

# CMakeLists.txt für road-profile-viewer C++-Port
cmake_minimum_required(VERSION 3.16)
project(road_profile_viewer_cpp VERSION 1.0.0 LANGUAGES CXX)

# C++ Standard-Einstellungen
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Bibliothek aus Geometry-Code erstellen
add_library(geometry_core
    src/geometry.cpp
)

# Angeben, wo Header-Dateien zu finden sind
target_include_directories(geometry_core PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# Haupt-Executable erstellen
add_executable(main
    src/main.cpp
)

# Bibliothek mit Executable verlinken
target_link_libraries(main PRIVATE geometry_core)

Vergleiche das mit dem, was wir in Make bräuchten:

Aspekt Makefile CMakeLists.txt
Codezeilen ~15 Zeilen (mit clean-Ziel) ~15 Zeilen, aber deklarativer
Neue Datei hinzufügen Regel hinzufügen, Abhängigkeiten hinzufügen, Link-Schritt aktualisieren Zu add_library() hinzufügen
Compiler wechseln Flags manuell aktualisieren CMake passt sich automatisch an
Plattformübergreifend Separate Makefiles schreiben Gleiche Datei funktioniert überall
IDE-Unterstützung Manuelle Konfiguration Automatisch via compile_commands.json

Diese CMakeLists.txt macht alles, was unser Makefile tat, plus behandelt Abhängigkeiten, plattformübergreifende Builds und IDE-Integration—alles aus einer Datei, die elegant wachsen wird, wenn das Projekt expandiert.

7.3 Der Workflow: Make vs. CMake

Lass uns den Unterschied in der Entwickler-Erfahrung visualisieren:

Mit Make (der alte Weg):

┌─────────────────────────────────────────────────────────────────────┐
│  Entwickler ändert geometry.hpp                                     │
└─────────────────────────┬───────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│  "Welche Dateien hängen von geometry.hpp ab?"                       │
│  Entwickler muss sich MANUELL erinnern oder prüfen                  │
│  - geometry.cpp? Ja                                                 │
│  - ray_tracer.cpp? Wahrscheinlich...                                │
│  - main.cpp? Vielleicht indirekt?                                   │
└─────────────────────────┬───────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Hoffe, Sie haben dich richtig erinnert, oder stehe mysteriösen       │
│  Laufzeitfehlern gegenüber                                          │
└─────────────────────────────────────────────────────────────────────┘

Mit CMake (der moderne Weg):

┌─────────────────────────────────────────────────────────────────────┐
│  Entwickler ändert geometry.hpp                                     │
└─────────────────────────┬───────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Entwickler führt aus: cmake --build build                          │
└─────────────────────────┬───────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│  CMake AUTOMATISCH:                                                 │
│  ✓ Erkennt, dass geometry.hpp geändert wurde                        │
│  ✓ Findet alle Dateien, die sie einbinden (direkt oder transitiv)  │
│  ✓ Kompiliert nur diese Dateien neu                                 │
│  ✓ Linkt die ausführbare Datei neu                                  │
└─────────────────────────────────────────────────────────────────────┘

Die mentale Last verschiebt sich von “was muss ich neu bauen?” zu einfach “neu bauen” und dem Werkzeug vertrauen.

7.4 CMakeLists.txt Struktur

Hier ist eine grundlegende CMake-Konfiguration:

# CMakeLists.txt

# Mindestens erforderliche CMake-Version
cmake_minimum_required(VERSION 3.16)

# Projektname und Sprache
project(road_profile_viewer_cpp VERSION 1.0.0 LANGUAGES CXX)

# C++ Standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Erstelle eine Bibliothek aus unseren Quelldateien
add_library(geometry_core
    src/geometry.cpp
)

# Gib an, wo Header-Dateien zu finden sind
target_include_directories(geometry_core PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# Erstelle eine ausführbare Datei
add_executable(main
    src/main.cpp
)

# Linke die Bibliothek zur ausführbaren Datei
target_link_libraries(main PRIVATE geometry_core)

7.5 CMake-Befehle erklärt

Lassen Sie uns die wichtigsten CMake-Befehle aufschlüsseln:

cmake_minimum_required(VERSION 3.16)

project(name VERSION x.y.z LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)

add_library(name source_files...)

target_include_directories(target PUBLIC dirs...)

add_executable(name source_files...)

target_link_libraries(target PRIVATE libs...)

7.6 Bauen mit CMake

Der typische CMake-Workflow:

# 1. Erstelle ein Build-Verzeichnis (Out-of-Source Build)
mkdir build
cd build

# 2. Generiere Build-Dateien
cmake ..

# 3. Baue das Projekt
cmake --build .

# Oder, unter Unix mit Make:
make

# Oder, mit Ninja (schneller):
cmake -G Ninja ..
ninja

Warum Out-of-Source Builds?

7.7 CMake-Ressourcen

CMake hat eine ausgezeichnete Dokumentation und ein großes Ökosystem. Hier sind sorgfältig ausgewählte Ressourcen für den tieferen Einstieg:

Offizielle Dokumentation:

Bücher:

Video-Ressourcen:

Moderne CMake Best Practices:

Das CMake-Ökosystem hat sich signifikant weiterentwickelt. “Modernes CMake” (3.0+) betont:

Altes CMake (vermeiden) Modernes CMake (bevorzugen)
include_directories() target_include_directories()
add_definitions() target_compile_definitions()
link_libraries() target_link_libraries()
Globale Variablen Ziel-Properties (PUBLIC/PRIVATE/INTERFACE)

Die wichtigste Erkenntnis: Verwenden Sie immer target_*-Befehle. Sie machen Abhängigkeiten explizit und helfen CMake, die Struktur Ihres Projekts zu verstehen.

Ninja-Ressourcen:


8. Umgang mit externen Bibliotheken

In Abschnitt 6.4 haben wir Problem 5: Keine Standardmethode zum Finden externer Bibliotheken identifiziert. Erinnern Sie sich an den Schmerz?

# Makefile - hartcodierte Pfade, die auf dem Rechner des Kollegen nicht funktionieren
OPENCV_INCLUDE = /usr/include/opencv4
OPENCV_LIBS = -lopencv_core -lopencv_imgproc

Jetzt sehen wir, wie CMake dies richtig löst—und warum es für unseren road-profile-viewer wichtig ist.

8.1 Das Problem: Ihr Projekt braucht externen Code

Unser Python road-profile-viewer verwendet drei externe Bibliotheken:

# pyproject.toml
dependencies = [
    "dash>=2.14.0",
    "plotly>=5.18.0",
    "numpy>=1.26.0",
]

Wenn Sie uv sync ausführen, werden diese automatisch heruntergeladen. Aber in C++ gibt es keinen einzelnen Paketmanager wie PyPI. Externer Code kommt von:

  1. Systembibliotheken — Installiert über apt, brew oder vcpkg
  2. Header-only Bibliotheken — Einfach #include und los
  3. Quellabhängigkeiten — Heruntergeladen und mit Ihrem Projekt kompiliert

CMake handhabt alle drei. Sehen wir uns jede einzelne an.

8.2 FetchContent: CMakes Abhängigkeitsmanager

Bevor wir in spezifische Lösungen eintauchen, stellen wir das Schlüsselwerkzeug vor, das das C++ Abhängigkeitsmanagement verändert hat: FetchContent.

Die Geschichte

Vor CMake 3.11 (veröffentlicht März 2018) hatten C++-Entwickler wenige gute Optionen für Abhängigkeiten:

Im März 2018 führte Kitware (das Unternehmen, das CMake seit 2000 pflegt) FetchContent als Teil von CMake 3.11 ein. Die Motivation war klar:

“Wir brauchen eine Möglichkeit, Abhängigkeiten zu deklarieren, die zur Konfigurationszeit heruntergeladen werden (nicht zur Build-Zeit), nahtlos integriert, als wären sie Teil Ihres Projekts.”

Was FetchContent macht

FetchContent ist CMakes Antwort auf uv sync. Es lädt Abhängigkeiten automatisch während des cmake ..-Konfigurationsschritts herunter und integriert sie:

# CMakeLists.txt
include(FetchContent)  # Lade das Modul

# Deklariere, was du brauchst (wie das Hinzufügen zu pyproject.toml)
FetchContent_Declare(
    dependency_name
    GIT_REPOSITORY https://github.com/example/library.git
    GIT_TAG v1.0.0  # Fixiere auf spezifische Version
)

# Lade herunter und mache verfügbar (wie das Ausführen von uv sync)
FetchContent_MakeAvailable(dependency_name)

Der FetchContent-Workflow:

Erster cmake-Lauf:
┌─────────────────────────────────────────────────────────────────┐
│  cmake ..                                                        │
│    │                                                             │
│    ├── Liest CMakeLists.txt                                     │
│    ├── Sieht FetchContent_Declare(...)                          │
│    ├── Lädt von GitHub nach build/_deps/<name>-src/ herunter    │
│    ├── Konfiguriert Abhängigkeit als Unterprojekt               │
│    └── Macht Targets verfügbar (wie library::library)           │
└─────────────────────────────────────────────────────────────────┘

Nachfolgende cmake-Läufe:
┌─────────────────────────────────────────────────────────────────┐
│  cmake ..                                                        │
│    │                                                             │
│    └── Bereits heruntergeladen, überspringt Abruf (schnell!)    │
└─────────────────────────────────────────────────────────────────┘

Warum FetchContent wichtig ist:

Wann FetchContent NICHT ideal ist: CI/CD-Überlegungen

Während FetchContent ausgezeichnet für das Lernen und kleine Projekte ist, vermeiden viele produktive CI/CD-Umgebungen es aus mehreren Gründen:

Problem Problem in CI Alternative
Netzwerkabhängigkeit Jeder Build erfordert GitHub-Zugang. Wenn GitHub nicht erreichbar ist, schlägt Ihre CI fehl. Vorkompilierte Binärdateien im Artefakt-Repository
Kein Binary-Caching FetchContent lädt Quellcode herunter und kompiliert neu. Bei 100+ Builds/Tag verschwendet das Stunden. vcpkg/Conan mit Binary-Caching
Docker-Layer-Ineffizienz FetchContent läuft zur Konfigurationszeit, innerhalb Ihres Builds. Abhängigkeiten können nicht in Basis-Image-Layern gecacht werden. Abhängigkeiten in Dockerfile-Basis-Layer installieren
Sicherheit/Compliance Einige Organisationen verlangen, dass alle Abhängigkeiten vor der Verwendung Sicherheitsscans bestehen. FetchContent holt direkt von der Quelle. Artefakt-Repository (Artifactory, Nexus) mit genehmigten Paketen
Air-Gapped-Umgebungen Einige CI-Umgebungen haben keinen Internetzugang (Regierung, Finanzen). Vendored Dependencies oder interne Mirrors

Beispiel: Docker-Build mit FetchContent-Problem

# ❌ Ineffizient: Abhängigkeiten werden bei JEDEM Build heruntergeladen
FROM ubuntu:22.04
COPY . /app
WORKDIR /app/build
RUN cmake .. && cmake --build .  # FetchContent läuft hier, kein Caching
# ✅ Besser: Abhängigkeiten im Basis-Image-Layer (gecacht)
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y libgtest-dev  # Gecachter Layer
COPY . /app
WORKDIR /app/build
RUN cmake .. && cmake --build .  # Verwendet System-gtest, schneller Rebuild

Fazit: FetchContent ist perfekt für:

Für produktive CI/CD mit vielen täglichen Builds sollten Sie vcpkg oder Conan mit Binary-Caching in Betracht ziehen, oder Abhängigkeiten in Ihren Docker-Basis-Images vorinstallieren.

Sehen wir uns nun an, wie FetchContent (und andere Tools) verschiedene Arten von Abhängigkeiten handhaben.

8.3 Systembibliotheken mit find_package()

Nicht alle Bibliotheken sollten abgerufen werden. Einige sind auf dem System vorinstalliert—große Frameworks wie OpenCV, Qt oder Boost, die Sie einmal über Ihren Paketmanager installieren.

CMakes find_package() findet diese systeminstallierten Bibliotheken automatisch.

Beispiel: Verwendung von OpenCV für Bildverarbeitung

# CMakeLists.txt
find_package(OpenCV REQUIRED)

add_executable(image_processor src/main.cpp)
target_link_libraries(image_processor PRIVATE ${OpenCV_LIBS})
target_include_directories(image_processor PRIVATE ${OpenCV_INCLUDE_DIRS})

Was im Hintergrund passiert:

  1. CMake durchsucht Standardorte nach OpenCV:
    • Linux: /usr/lib, /usr/local/lib, pkg-config-Pfade
    • macOS: /opt/homebrew/lib, /usr/local/lib, Framework-Pfade
    • Windows: Program Files, vcpkg-Registry, Umgebungsvariablen
  2. CMake setzt Variablen wie OpenCV_LIBS und OpenCV_INCLUDE_DIRS

  3. Ihr Kollege führt cmake .. aus und es funktioniert einfach—keine hartcodierten Pfade

Vergleich mit Make:

# Makefile - funktioniert nicht auf verschiedenen Maschinen
OPENCV_INCLUDE = /usr/include/opencv4          # Meine Maschine
# OPENCV_INCLUDE = /opt/homebrew/include/opencv4  # Mac des Kollegen

target:
    g++ -I$(OPENCV_INCLUDE) main.cpp -lopencv_core

Wann find_package() vs FetchContent verwenden:

Verwende find_package() Verwende FetchContent
Große Frameworks (OpenCV, Qt, Boost) Kleinere Bibliotheken
Systemabhängigkeiten Test-Frameworks (Google Test)
Vorkompilierte Binärdateien Header-only Bibliotheken
Bibliotheken mit komplexen Builds Bibliotheken, die Sie versionsfixiert haben möchten

8.4 Beispiel: Header-Only Bibliotheken mit FetchContent

Einige moderne C++-Bibliotheken sind “header-only”—keine Kompilierung nötig, einfach einbinden und verwenden. FetchContent macht deren Integration trivial.

Beispiel: nlohmann/json für JSON-Parsing

Unser road-profile-viewer muss möglicherweise Konfigurationsdateien lesen. Anstatt JSON manuell zu parsen, können wir eine populäre Header-only-Bibliothek verwenden:

# CMakeLists.txt
include(FetchContent)

FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(json)

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE nlohmann_json::nlohmann_json)

Verwendung im Code:

#include <nlohmann/json.hpp>
#include <fstream>

int main() {
    std::ifstream config_file("config.json");
    nlohmann::json config = nlohmann::json::parse(config_file);

    double camera_height = config["camera"]["height"];
    // ...
}

Warum Header-only Bibliotheken praktisch sind:

8.5 Beispiel: Test-Frameworks mit FetchContent

Für unseren road-profile-viewer brauchen wir Google Test. Dies ist ein perfekter FetchContent-Anwendungsfall—wir wollen die gleiche Test-Framework-Version auf allen Entwicklermaschinen und in der CI.

# CMakeLists.txt
include(FetchContent)

# Deklariere Google Test Abhängigkeit
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)

# Herunterladen und integrieren
FetchContent_MakeAvailable(googletest)

# Verwende es in Ihrem Test-Target
add_executable(geometry_tests tests/test_geometry.cpp)
target_link_libraries(geometry_tests PRIVATE
    geometry_core
    GTest::gtest_main
)

Mehrere Abhängigkeiten zusammen:

include(FetchContent)

# Deklariere alle Abhängigkeiten
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)

FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.2
)

# Alle auf einmal abrufen (effizient!)
FetchContent_MakeAvailable(googletest json)

8.6 Vergleich: Python vs C++ Abhängigkeitsmanagement

Aspekt Python (uv/pip) C++ (CMake)
Abhängigkeiten deklarieren pyproject.toml FetchContent_Declare()
Installieren/Abrufen uv sync cmake .. (auto-abruf)
Zentrales Repository PyPI Keines (GitHub, vcpkg, Conan)
Versionsfixierung numpy>=1.26.0 GIT_TAG v1.14.0
Lock-Datei uv.lock Kein Standard (CMake Presets entstehen)

Wichtige Erkenntnis: C++ Abhängigkeitsmanagement ist manueller als das von Python, aber CMakes FetchContent bringt es näher an die moderne Python-Erfahrung.

8.7 Ressourcen zum Abhängigkeitsmanagement

C++ Abhängigkeitsmanagement ist ein sich schnell entwickelnder Bereich. Hier sind Ressourcen, um auf dem Laufenden zu bleiben:

Offizielle Dokumentation:

Paketmanager (Alternativen zu FetchContent):

Wann was verwenden:

Tool Am besten für Kompromisse
FetchContent Einfache Projekte, Ausbildung, volle Quellkontrolle Langsamer initialer Build, kein Binary-Caching
vcpkg Windows-Entwicklung, Microsoft-Ökosystem Erfordert Manifest-Modus für Reproduzierbarkeit
Conan Große Projekte, Binary-Caching, CI/CD Steilere Lernkurve, Python-Abhängigkeit
CPM.cmake FetchContent mit Caching Drittanbieter-Tool, erhöht Komplexität

Artikel und Tutorials:

FetchContent vs ExternalProject:

Zu verstehen, warum FetchContent ExternalProject ersetzt hat, hilft Ihnen, sein Design zu schätzen:

Aspekt ExternalProject (alt) FetchContent (modern)
Wann es läuft Build-Zeit Konfigurationszeit
Target-Sichtbarkeit Targets nicht im Hauptprojekt sichtbar Targets vollständig integriert
Typischer Einsatz Superbuild-Muster Direkte Abhängigkeitseinbindung
Eingeführt CMake 2.8 (2009) CMake 3.11 (2018)


9. Codeorganisation: Header- und Quelldateien

In Abschnitt 6.4 haben wir Problem 2: Manuelle Header-Abhängigkeitsverfolgung identifiziert. Erinnern Sie sich?

# Sie mussten manuell jeden Header auflisten, von dem eine Datei abhängt
geometry.o: geometry.cpp geometry.hpp math_types.hpp utils.hpp

CMake löst das Tracking, aber es gibt eine tiefere Frage: Warum teilt C++ Code überhaupt in Header-Dateien und Quelldateien auf? Das Verständnis dessen verbindet sich mit allem, was wir über Kompilierung und Linking gelernt haben.

9.1 Warum zwei Dateitypen? Das Kompilierungsmodell

Erinnern Sie sich an Abschnitt 5.2: Der Compiler verarbeitet eine Quelldatei nach der anderen und erzeugt je eine Objektdatei:

geometry.cpp  ──→  g++ -c  ──→  geometry.o
main.cpp      ──→  g++ -c  ──→  main.o

Aber main.cpp muss Funktionen aus geometry.cpp aufrufen. Woher weiß der Compiler, dass diese Funktionen existieren?

Die Header-Datei löst dies: Sie enthält Deklarationen, die dem Compiler sagen “vertrau mir, diese Funktion existiert irgendwo.”

┌─────────────────────────────────────────────────────────────────┐
│  Beim Kompilieren von main.cpp:                                   │
│                                                                  │
│  1. Compiler liest main.cpp                                      │
│  2. Sieht: #include "geometry.hpp"                               │
│  3. Liest geometry.hpp → erfährt, dass calculate_ray_line() existiert     │
│  4. Kompiliert main.cpp, vertrauend dass die Funktion gelinkt wird     │
│  5. Später verbindet der Linker die tatsächliche Funktion aus geometry.o  │
└─────────────────────────────────────────────────────────────────┘

Python braucht das nicht, weil es interpretiert wird—der Interpreter kann Funktionen zur Laufzeit nachschlagen. C++ muss alles zur Kompilierzeit auflösen.

9.2 Header-Dateien: Die öffentliche Schnittstelle

Header-Dateien (.hpp oder .h) enthalten Deklarationen—sie beschreiben was existiert, nicht wie es funktioniert:

// geometry.hpp - Die ÖFFENTLICHE Schnittstelle unseres Geometrie-Moduls
#ifndef ROAD_PROFILE_VIEWER_GEOMETRY_HPP
#define ROAD_PROFILE_VIEWER_GEOMETRY_HPP

#include <vector>
#include <optional>

namespace road_profile_viewer {

// Datenstrukturen (vollständig definiert - Benutzer müssen das Layout kennen)
struct RayLine {
    std::vector<double> x;
    std::vector<double> y;
};

struct IntersectionResult {
    double x;
    double y;
    double distance;
};

// Funktions-DEKLARATIONEN (keine Implementierung hier!)
RayLine calculate_ray_line(
    double angle_degrees,
    double camera_x = 0.0,
    double camera_y = 2.0,
    double x_max = 80.0
);

std::optional<IntersectionResult> find_intersection(
    const std::vector<double>& x_road,
    const std::vector<double>& y_road,
    double angle_degrees,
    double camera_x = 0.0,
    double camera_y = 1.5
);

}  // namespace road_profile_viewer

#endif  // ROAD_PROFILE_VIEWER_GEOMETRY_HPP

Wichtige Elemente erklärt:

1. Include Guards (#ifndef, #define, #endif)

#ifndef ROAD_PROFILE_VIEWER_GEOMETRY_HPP  // Falls noch nicht definiert...
#define ROAD_PROFILE_VIEWER_GEOMETRY_HPP  // ...jetzt definieren

// ... Inhalt ...

#endif  // Ende des Guards

Ohne Guards, wenn zwei Dateien beide #include "geometry.hpp" haben, sieht der Compiler die Deklarationen zweimal und meldet “Redefinition”-Fehler. Guards stellen sicher, dass der Inhalt nur einmal verarbeitet wird.

2. Namespace (namespace road_profile_viewer)

Gruppiert zusammengehörigen Code und verhindert Namenskollisionen. Wenn eine andere Bibliothek auch eine calculate_ray_line()-Funktion hat, halten Namespaces sie getrennt:

road_profile_viewer::calculate_ray_line(45.0);  // Unsere
other_library::calculate_ray_line(45.0);         // Deren

3. Nur Deklarationen — keine Funktionskörper

Der Header sagt “diese Funktion existiert mit dieser Signatur”, zeigt aber nicht die Implementierung. Das ist beabsichtigt:

9.3 Quelldateien: Die Implementierung

Quelldateien (.cpp) enthalten Definitionen—den tatsächlichen Code:

// geometry.cpp - Die PRIVATE Implementierung
#include "geometry.hpp"
#include <cmath>
#include <numbers>

namespace road_profile_viewer {

RayLine calculate_ray_line(
    double angle_degrees,
    double camera_x,
    double camera_y,
    double x_max
) {
    // Grad in Bogenmaß umrechnen
    const double angle_rad = -angle_degrees * std::numbers::pi / 180.0;

    // Strahl-Endpunkt berechnen
    double x_end, y_end;

    if (std::abs(std::cos(angle_rad)) < 1e-10) {
        // Vertikaler Strahl (senkrecht nach unten schauend)
        x_end = camera_x;
        y_end = 0.0;
    } else {
        // Strahl schneidet Boden bei y = 0
        const double tan_angle = std::tan(angle_rad);
        x_end = camera_x + camera_y / tan_angle;
        y_end = 0.0;

        // Auf x_max begrenzen
        if (x_end > x_max) {
            x_end = x_max;
            y_end = camera_y - (x_end - camera_x) * tan_angle;
        }
    }

    return RayLine{
        .x = {camera_x, x_end},
        .y = {camera_y, y_end}
    };
}

// ... find_intersection Implementierung ...

}  // namespace road_profile_viewer

Beachten Sie:

9.4 Wie das mit dem Build-System zusammenhängt

Nun ergibt die CMakeLists.txt mehr Sinn:

# Eine Bibliothek aus der QUELL-Datei erstellen (nicht dem Header!)
add_library(geometry_core
    src/geometry.cpp    # <-- Die Implementierung
)

# CMake mitteilen, wo Header sind (damit #include funktioniert)
target_include_directories(geometry_core PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# Die ausführbare Datei muss nur gegen die Bibliothek gelinkt werden
add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE geometry_core)

Der Ablauf:

┌──────────────────────────────────────────────────────────────────────┐
│ cmake --build .                                                       │
│                                                                       │
│  1. Kompiliere geometry.cpp → geometry_core Bibliothek                     │
│     (Header geometry.hpp wird während der Kompilierung gelesen)                  │
│                                                                       │
│  2. Kompiliere main.cpp → main.o                                        │
│     (main.cpp macht #include "geometry.hpp" um die API zu kennen)          │
│                                                                       │
│  3. Linke main.o + geometry_core → main ausführbare Datei                    │
│     (Linker verbindet main's Aufrufe mit geometry's Implementierungen)     │
└──────────────────────────────────────────────────────────────────────┘

9.5 Python vs. C++ Module

Aspekt Python-Modul C++-Modul
Dateien Einzelne .py-Datei .hpp (Schnittstelle) + .cpp (Implementierung)
Schnittstelle Implizit (was immer definiert ist) Explizit (Header deklariert öffentliche API)
Import from geometry import func #include "geometry.hpp"
Namespace Dateiname ist Namespace Explizites namespace-Schlüsselwort
Sichtbarkeit _private-Konvention Header exponiert nur öffentliche API
Kompilierungsauswirkung N/A (interpretiert) Header-Änderungen lösen Neukompilierung aller Einbinder aus

Der Kompromiss: C++ erfordert mehr Dateien und explizite Struktur, gewinnt aber Kompilierzeitprüfung, schnellere Ausführung und explizite Schnittstellen.

9.6 Projektstruktur für road-profile-viewer C++

Alles zusammengefasst, so ist unsere C++-Portierung organisiert:

road-profile-viewer/
├── cpp/
│   ├── CMakeLists.txt           # Build-Konfiguration
│   ├── include/
│   │   └── geometry.hpp         # Öffentliche Schnittstelle (Deklarationen)
│   ├── src/
│   │   ├── geometry.cpp         # Implementierung (Definitionen)
│   │   └── main.cpp             # Einstiegspunkt
│   └── tests/
│       └── test_geometry.cpp    # Unit-Tests

Diese Struktur:

9.7 Ressourcen zu Header und Modulen

Das Verständnis der C++-Codeorganisation ist für größere Projekte essentiell. Hier sind Ressourcen zur Vertiefung Ihres Wissens:

Offizielle Dokumentation:

Die Zukunft: C++20-Module

C++20 führte ein neues Modulsystem ein, das möglicherweise die Header/Quell-Aufteilung ersetzen wird:

// math.cppm (Modul-Schnittstelle)
export module math;

export int add(int a, int b) {
    return a + b;
}

// main.cpp
import math;

int main() {
    return add(1, 2);
}

Warum wir noch Header lehren:

Header (traditionell) Module (C++20)
Universelle Compiler-Unterstützung Begrenzte Compiler-Unterstützung (verbessert sich)
Funktioniert mit allem bestehenden Code Erfordert Migrationsaufwand
CMake unterstützt vollständig CMake-Unterstützung experimentell
Alle Tutorials/Bücher verwenden Header Noch wenige Lernressourcen

Module werden irgendwann Standard werden, aber vorerst (2024-2026) bleiben Header die praktische Wahl für die meisten Projekte.

Artikel und Tutorials:

Bücher zur Vertiefung:

Häufige Header-Fehler:

Fehler Problem Lösung
Fehlende Include Guards Redefinitions-Fehler Immer #ifndef oder #pragma once verwenden
Implementierungen in Headern Langsame Kompilierung, Code-Aufblähung Nur Deklarationen in .hpp
Unnötige Header einbinden Langsame Kompilierung, versteckte Abhängigkeiten Forward-Deklarationen, IWYU-Tool
Zirkuläre Includes Kompilierung schlägt fehl Forward-Deklarationen, Code umstrukturieren

10. Zusammenfassung

In dieser Vorlesung haben Sie die Grundlagen der C++-Entwicklung gelernt—das Fundament, das Sie benötigen, um C++ mit Ihren Python-Projekten zu integrieren.

10.1 Wichtige Erkenntnisse

  1. C++ ist kompiliert, Python ist interpretiert: Dieser fundamentale Unterschied erklärt Performanzunterschiede (10-100x schneller) und Deployment-Einschränkungen (keine Laufzeitumgebung benötigt).

  2. Der Build-Prozess hat drei Stufen: Präprozessierung, Kompilierung und Linking transformieren Quellcode in native ausführbare Dateien.

  3. CMake ist das plattformübergreifende Build-Konfigurationswerkzeug: Es generiert plattformspezifische Build-Dateien (Makefiles, Ninja, Visual Studio-Projekte) aus einer einzigen Konfiguration.

  4. Abhängigkeitsmanagement entwickelte sich mit CMake: Von manuellen Downloads über find_package() zu modernem FetchContent wurde die Verwaltung externer Bibliotheken zunehmend automatisiert.

  5. Header/Quell-Datei-Trennung: Anders als Pythons einzelne .py-Dateien trennt C++ Deklarationen (.hpp) von Implementierungen (.cpp), um getrennte Kompilierung zu ermöglichen.

  6. Das Kompilierungsmodell erfordert Deklarationen: Beim Kompilieren einer Datei muss der Compiler wissen, welche Funktionen in anderen Dateien existieren—Header liefern diese Information.

10.2 Was Sie jetzt können


11. Reflexionsfragen

  1. Warum erfordert C++ explizite Typdeklarationen, während Python dynamische Typisierung erlaubt? Was sind die Kompromisse?

  2. Der Python-Interpreter enthält Garbage Collection. C++ nicht. Welche Herausforderungen schafft das für C++-Entwickler?

  3. CMake generiert Makefiles (oder Ninja-Dateien). Warum ein “Meta-Build-System” verwenden statt Makefiles direkt zu schreiben?

  4. FetchContent lädt Abhängigkeiten zur Konfigurationszeit herunter. Wann könnte das problematisch sein? Welche Alternativen gibt es?

  5. Warum trennen C++-Projekte Deklarationen (Header) von Implementierungen (Quelldateien)? Python macht das nicht—was ist anders?


12. Nächste Schritte

In der nächsten Vorlesung (Teil 2) werden wir:

Sie werden alles, was Sie heute gelernt haben, anwenden, um die Geometrie-Funktionen Ihres Road Profile Viewers sowohl von Python als auch von C++ aus aufrufbar zu machen.


13. Weiterführende Literatur

13.1 C++ Grundlagen

13.2 Build-Prozess Vertiefung

13.3 Abhängigkeitsmanagement


Teil 2: Testing und CI/CD →
© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk