Inhaltsverzeichnis
Übersetzungseinheiten
Divide et impera.— Römisches Sprichwort
Hinweis: Diese Seite beschreibt C++98 bis C++17, (noch) nicht die mit C++20 eingeführten Module.
Organisation
Modularisierung ist eine Technik zum Aufbrechen und Beherrschen der Komplexität von Programmieraufgaben. Die Abgrenzung von Programmteilen kann
- logisch über Namensräume und
- physisch durch die Aufteilung in mehrere Quelltext-Dateien erfolgen.
Inhaltlich zusammengehörige Datentypen und Funktionen lassen sich als Modul in einer Quelltextdatei bündeln. Vorspanndateien (Header) enthalten Deklarationen und Definitionen, z.B. Funktionsprototypen, die zur Nutzung der Moduls notwendig sind. Die Kommunikation mit dem Modul sollte nur über die dadurch festgelegte Schnittstelle erfolgen (Geheimnisprinzip).
Bei der Übersetzung entstehende Objektdateien können mit Bibliotheken zum ausführbaren Programm (Binärdatei) zusammengeführt werden. Das Modul kann als Objektdatei oder als Bibliothek weitergegeben und in mehrere Programme eingebunden werden, ohne geändert oder neu übersetzt werden zu müssen. Bibliotheken sind Sammlungen von Objektdateien, die mit einem Bibliothekar oder Archivar genannten Programm zusammengestellt und gepflegt werden. Auf der Ebene der Objektdateien und Bibliotheken sind Programmteile aus unterschiedlichen Programmiersprachen kombinierbar.
Die Dateinamen besitzen je nach System typische Endungen:
Headerdatei | *.h, *.hpp, *.hxx |
Quelltext | *.cc, *.cpp, *.cxx |
Objektdatei | *.o, *.obj |
Bibliothek | lib*.a, lib*.so, *.lib, *.dll |
Programm | a.out, *.exe |
Namensräume
Namenskonflikte können die Integration von Modulen vereiteln, wenn derselbe Name in ihnen mehrfach definiert wird. Namensräume (Schlüsselwort namespace) begrenzen die Verschmutzung des globalen Namensraumes.
Syntax:
namespace
Namensraumbezeichner
{
Deklarationen
Definitionen
}
Namensbereiche sind schachtelbar.
Ein Namensraum darf mehrfach,
auch in verschiedenen Dateien,
geöffnet, erweitert und geschlossen werden.
Der Namensraum std
ist jedoch als abgeschlossen zu betrachten.
Unbenannte Namensräume kapseln globale Namen,
die nur innerhalb eines Moduls sichtbar sein sollen
(ohne externe Bindung).
Lange Namensraumbezeichner können durch kürzere ersetzt werden:
namespace fs = std::filesystem;
Namen sind außerhalb ihres Namensraumes qualifiziert mit dem Namensraumbezeichner anzugeben. Einzelne Namen und ganze Namensräume können mit der using-Anweisung in einen Block oder global importiert werden.
std::cout << "Hallo" << std::endl; using std::cout; using namespace std; cout << "Hallo" << endl; // nun ohne std:: nutzbar
Header
Modulschnittstellen werden vor der Nutzung durch Einbinden von Deklarationsdateien (Header) bekannt gemacht. Standard-Header werden in spitzen Klammern angegeben. Eigene Header in Gänsefüßchen werden zuerst bei den Quelltexten (im aktuellen Projekt-Verzeichnis) gesucht, danach in den Standard-Include-Verzeichnissen.
#include <Standardheader> #include "mein_header.h"
Syntaktisch korrekte Header können in beliebiger Folge angegeben werden. Abhängigkeiten zwischen den Headern sollten in den Headern geregelt werden. Erfordern Deklarationen eines Headers die Kenntnis anderer Schnittstellen, muss dieser die anderen Header einbinden. Eventuelles mehrfaches Einbinden soll nicht zu Übersetzungsfehlern führen. Header werden deshalb mit "Include-Wächtern" versehen, deren eindeutige Namen dies durch bedingte Übersetzung verhindern:
#ifndef MEIN_HEADER_H #define MEIN_HEADER_H // ... Abhängigkeiten // ... Deklarationen #endif // MEIN_HEADER
Viele Compiler erlauben als Ersatz für den Include-Wächter auch die Nutzung der nicht standardisierten Präpozessoranweisung
#pragma once
.
Bindung
In einer Quelltextdatei definierte Funktionen und globale Variablen haben interne Bindung, wenn sie mit dem Spezifizierer static oder in einem namenlosen Namensraum deklariert wurden. Auf Bezeichner mit interner Bindung kann nur innerhalb dieses Quelltextes Bezug genommen werden. Definierte gleichnamige Bezeichner mit interner Bindung existieren in verschiedenen Übersetzungseinheiten unabhängig voneinander.
Alle anderen Funktionen und globalen Variablen haben externe Bindung. Sie dürfen im Programm nur ein einziges Mal definiert sein (one definition rule). Funktionen mit externer Bindung können aus anderen Quelltexten heraus aufgerufen werden. Lediglich ihr Prototyp muss zum Aufruf bekannt sein. In anderen Dateien definierte globale Variablen mit externer Bindung sind nutzbar, nachdem sie mit dem Spezifizierer extern angemeldet wurden.
extern int global; // modul1.h int minimum(int, int);
int global; // modul1.cpp int minimum(int x, int y) { return x<y ? x : y; }
#include "modul1.h" int main() // main.cpp { global = minimum(0, 1); return global; }
Anmerkung: Im allgemeinen sollten globale Variablen vermieden werden. Sie stellen ein Problem bei der Wartung und bei der Parallelisierung von Programmen dar.
Sollen Funktionen aus / in anderen Programmiersprachen aufgerufen werden, muss der Compiler darauf vorbereitet werden, weil unterschiedliche Sprachen unterschiedliche Konventionen der Parameterübergabe, Wertrückgabe und Stackbereinigung haben. Da andere Programmiersprachen bestimmte Merkmale von C++ nicht unterstützen, sind Überladen u.ä. für solche Funktionen nicht nutzbar.
extern "C" int minimum(int, int);
Übersetzung
<...> \ *.h => Präprozessor => Compiler => *.o => Linker => Programm *.cpp / | ^ Archivar <=> Bibliotheken
Ein Programm entsteht in einem mehrstufiger Prozess, auch wenn dieser mit einem einzigen Befehl angestoßen wird.
- Für jede Quelltextdatei (Übersetzungseinheit) einzeln (getrennte Übersetzung):
- Vorverarbeiten mit dem Präprozessor
cpp
:- Einbinden von Dateien (
#include
…), - und Textersatz (
#define
…) ausführen.
- Übersetzen: Mit dem Ergebnis der Vorverarbeitung wird der Compiler gefüttert.
Für jeden erfolgreich übersetzten Quelltext entsteht eine gleichnamige Objektdatei.
- Verbinden: Objektdateien und Bibliotheken werden vom Linker zum ausführbaren Programm zusammengeführt.
Projektverwaltung
Die Übersetzung und Projektverwaltung erfolgt systemabhängig
- über Kommandozeilenwerkzeuge und Steuerdateien oder
- in integrierten Entwicklungsumgebungen (z.B. Code::Blocks).
Integrierte Entwicklungsumgebungen sind Arbeitswerkzeuge, die den Programmierer bei der Erstellung von Quelltexten und Programmen, bei der Fehlersuche und Dokumentation unterstützen (können), wenn man weiß, wie. Jede Entwicklungsumgebung wird anders bedient. Die Beschreibung einzelner Entwicklungsumgebungen erfolgt hier nicht. Zudem erfordern Entwicklungsumgebungen zumeist die Anwesenheit eines Tastatur und Maus bedienenden Programmierers. Mausklicks sind schwer automatisierbar.
Dagegen können Kommandozeilenbefehle leicht in Skripte gefasst und automatisiert werden. GNU-Werkzeuge g++ und make dienen hier als Beispiel. CMake (cross-platform make) geht noch einen Schritt weiter. Es konfiguriert Projekte für die gängigen Compiler auf verschiedenen Systemen.
Übersetzen an der Kommandozeile
Ein Programm aus 2 Quelltexten wird erstellt:
g++ -o progname main.cpp modul1.cpp
Fehlt die Angabe -o progname
(o wie output),
findet sich die ausführbare Datei als a.out
("Assembler Output").
Die Übersetzung einzelner Quelltexte erfolgt mit dem Schalter -c
(compile):
g++ -c main.cpp g++ -c modul1.cpp
Die entstehenden Objektdateien werden zum Programm zusammengeführt (gelinkt):
g++ -o progname main.o modul1.o
Erfordert ein Programm bestimmte Bibliotheken
(lib*.a
oder lib*.so
),
ist beim Linken -l name
anzugeben,
Der name
ist dabei der nach code|lib| stehende Teil des Bibliotheksnamens,
z.B. ist libm.a
die Mathematik-Bibliothek.
g++ prog.cpp -lm
Die Kombination unterschiedlichster Übersetzungsstufen ist erlaubt:
g++ -o progname main.cpp modul1.o -lm
Archivieren in Bibliotheken
Mit dem Archivar ar
lassen sich Bibliotheken aus Objektdateien zusammenstellen,
pflegen und bereinigen.
Die Symboltabelle für den Linker wird mit dem Befehl ranlib
erzeugt.
Zur Dokumentation siehe man ar
bzw. man ranlib
.
Automatisieren mit make
Das Werkzeug make
unterstützt die Automatisierung.
Mit einem einfachen Kommando
make
kann ein Projekt aktualisiert werden. Der Aufruf
make test
könnte einen Regressionstest (Fehlerprüfung nach Wartung) veranlassen. Mit der Zielangabe
make clean
könnten Zwischendateien beräumt werden.
Voraussetzung ist eine Datei Makefile
im (aktuellen) Projektverzeichnis.
Beim Anlegen der Datei ist darauf zu achten,
dass Tabulatoren auch wirklich als Tabs in der Datei gespeichert werden.
Im Makefile
werden die verschiedenen Ziele definiert.
Syntax:
Ziel : Tabulator Voraussetzung
Tabulator Befehl
Zu jedem Ziel wird angegeben, welche Voraussetzungen oder Abhängigkeiten erfüllt sein müssen, um die nachfolgenden Befehle ausführen zu können.
Zuerst versucht make
, alle Voraussetzungen zu erfüllen.
Voraussetzungen sind Teilziele oder Dateien.
Ist eine Ausgangsdatei jünger als die Zieldatei,
müssen die zu dem Ziel angegebenen Befehle ausgeführt werden.
Bei aktuellen Dateien unterbleibt die Ausführung der Befehle,
und das Ziel gilt als erfüllt.
Tritt irgendwo ein Fehler auf, bricht make
den Aktualisierungsprozess ab.
# Kommentar - Beispiel Makefile all : progname progname : main.o modul1.o g++ -o progname main.o modul1.o main.o : main.cpp g++ -c main.cpp modul1.o : modul1.cpp g++ -c modul1.cpp # Regressionstest test : progname input.txt soll.txt progname < input.txt > output.txt diff output.txt soll.txt # Saubermachen clean : rm *.o output.txt
Der make
-Aufruf ohne Parameter führt das oberste Ziel aus.
Bei unbekannten Zielen gibt sich make
spröde:
make love make: *** No rule to make target 'love'. Stop.