Inhaltsverzeichnis

minikurs(8): Mehrere Quelldateien

Wir brauchen eine 360°-Sicht.
— Manager-Sprech für: Ich habe keinen Überblick.

Aufteilen

Große Programme bestehen aus Millionen Quelltextzeilen. Es ist unvorstellbar, dass all diese Zeilen in einer Quelldatei stehen. Laden, Speichern, Suchen und Bearbeiten in großen Dateien dauert zu lange. Editoren beschränken die Dateigröße, ältere gar nur auf 64 Kilobyte. Mehrere Programmierer müssen gleichzeitig verschiedene Programmteile bearbeiten. Meist wird nur ein kleiner Teil des Programms verändert. Das Übersetzen großer Programme kann sehr lange dauern. Manche Teile eines Programms lassen sich auch für andere Programme als Bibliothek nutzen. Kopieren und Einfügen ist aber zu umständlich. Wichtige Änderungen würden nicht in die Kopien übernommen, es sei denn, jemand wird beauftragt, alle Kopien zu finden und zu ändern. Eine undankbare Aufgabe, bei der man so viel falsch machen kann…

Funktionen bündeln Anweisungen. Wir gehen nun eine Stufe höher. Funktionen lassen sich zu Programmbausteinen (Modulen) zusammenfassen. Ein Modul1) umfasst alle inhaltlich zusammengehörenden Funktionen. Wir nehmen die große Schere und zerschneiden den Papierausdruck vom vorigen Quelltext an geeigneten Stellen…

Um die Funktionen eines Bausteins in anderen Quelltexten nutzen zu können, werden übergreifende Typdefinitionen und die Funktionsköpfe in Vorspanndateien (Kopfdateien) untergebracht. Üblich ist die Endung *.h wie Header für solche Dateien. Typdefinitionen dürfen nur einmal je Übersetzerlauf vorkommen. Deshalb werden die Inhalte von Headern von einem "Include-Wächter" eingeschlossen:

#ifndef HEADERNAME_H
#define HEADERNAME_H
  ...
#endif // HEADERNAME_H

Dieser sorgt dafür, dass bei versehentlicher zweiter Begegnung der Compiler diesen Inhalt nicht noch einmal verarbeitet. Ohne Include-Wächter müsste man für alle Quelldateien überlegen: Muss dieser Header jetzt eingebunden werden oder ist er durch andere bereits eingebunden?

Ein Header für das Parkplatzprogramm kann so aussehen:

funktionen.h
//: funktionen.h : Autos und Zweiraeder - R.Richter 2007-01-07
//////////////////////////////////////////////////////////////
 
#ifndef FUNKTIONEN_H
#define FUNKTIONEN_H
 
int autos (int fahrzeuge, int reifen);
int mopeds(int fahrzeuge, int reifen);
 
#endif // FUNKTIONEN_H

Übersetzungseinheiten

Die einzelnen Funktionen des Programms können auf mehrere Quelltexte verteilt werden. Die Anmeldung der Funktionen erfolgt durch Einbinden der Vorspanndatei. Dort steht alles, was das Hauptprogramm über die aufzurufenden Funktionen wissen muss.

parkplatz6.cpp
//: parkplatz6.cpp : Autos und Zweiraeder - R.Richter 2007-01-07
////////////////////////////////////////////////////////////////
 
#include <iostream>
#include "funktionen.h"
 
int main()
{
  int fahrzeuge, reifen;
 
  std::cout << "Fahrzeuge: ";
  std::cin >> fahrzeuge;
 
  std::cout << "Reifen: ";
  std::cin >> reifen;
 
  std::cout << "Autos: "       << autos (fahrzeuge, reifen) << '\n'; 
  std::cout << "Motorraeder: " << mopeds(fahrzeuge, reifen) << '\n'; 
 
  return 0;
}

Die Implementierung der Funktionen wird in einer anderen Übersetzungseinheit untergebracht:

funktionen.cpp
//: funktionen.cpp : Autos und Zweiraeder - R.Richter 2007-01-07
////////////////////////////////////////////////////////////////
 
#include "funktionen.h"
 
int autos (int fahrzeuge, int reifen)
{
  return fahrzeuge - mopeds(fahrzeuge, reifen);
}
 
int mopeds(int fahrzeuge, int reifen)
{
  return (4*fahrzeuge - reifen) / 2;
}
                                                             //~

Hier wäre es nicht unbedingt erforderlich, den Header einzubinden. Es erspart aber, darüber nachdenken zu müssen, welche der Funktionen zuoberst stehen muss. So bleibt der Kopf frei für andere Aufgaben. Bevor der Quelltext weiter entwickelt werden kann, muss noch eine Aufgabe gelöst werden.

Ein Programm aus mehreren Dateien kann nicht mehr so einfach übersetzt werden. Jede Funktion darf im Projekt nur genau einmal definiert sein. Eine der Quellen muss die Funktion main() enthalten. Wie wird dem Compiler mitgeteilt, welche der vielen Quelldateien zusammen ein Programm bilden? Diese Verwaltungsinformation ist nicht im Quelltext abgelegt.

Projektverwaltung

Makefile

Eine Makefile genannte Textdatei ohne Endung(!) enthält Regeln für das make-Werkzeug in der Form

Ziel: Voraussetzungen
        Befehle

Beachte, dass vor den Befehlen ein Tabulatorzeichen muss. (Legt Dein Editor wirklich Tabulatoren als ein Tab-Zeichen in der Datei ab oder ersetzt er sie durch 8 Leerzeichen?) Beim Aufruf

make

sucht das Werkzeug im aktuellen Verzeichnis nach dem Makefile. Es versucht dann, das angegebene Ziel zu erreichen. Für das Parkplatzprogramm könnten die Regeln so beschaffen sein:

all: parkplatz

parkplatz: funktionen.h funktionen.cpp parkplatz6.cpp
        $(CXX) -o parkplatz funktionen.cpp parkplatz6.cpp

Ist dieser Text für Dich verständlich? Alles, was zu tun ist, ist, das Programm parkplatz zu bauen. Dafür sind die 3 angegebenen Quelldateien notwendig. Fehlt eine, schlage Krach. Sind die Quellen aktueller als das Ziel parkplatz, dann übersetze die beiden Dateien mit der Endung *.cpp. Das make-Werkzeug kennt den Compilernamen als Zeichenkette $(CXX).

Getrennte Übersetzung und Testautomatisierung

Vor allem in großen Projekten ist es günstig, mit Zwischendateien zu arbeiten. Nicht bei jeder Änderung müssen alle Dateien neu übersetzt werden. Die Übersetzung mit der Option -c prüft einen einzelnen Quelltext. Bei korrekter Syntax entsteht eine gleichnamige Objektdatei. Diese Zwischendateien werden im zweiten Schritt zum lauffähigen Programm verbunden ("gelinkt").

funktionen.o: funktionen.h funktionen.cpp 
        $(CXX) -c funktionen.cpp

parkplatz6.o: funktionen.h parkplatz6.cpp
        $(CXX) -c parkplatz6.cpp

parkplatz: funktionen.o parkplatz6.o
        $(CXX) -o parkplatz funktionen.o parkplatz6.o

Leider ist das Makefile jetzt plattformabhängig: Die Dateinamen *.o ist bei Unix-Compilern üblich, *.obj bei Windows-Compilern.

Nimm kleine Veränderungen an der *.h-Datei oder an einer *.cpp-Datei vor. Rufe make auf und beobachte an den Ausschriften, welche Dateien neu übersetzt werden. Beachte dabei: Unter DOS / Windows tragen die ausführbaren Programme die Endung *.exe. Möglicherweise stellt sich make komisch an, wenn diese Endung fehlt.

Ein Makefile enthält meist noch ein Ziel clean, das Zwischendateien löscht. Hier ein Beispiel für Unixe:

clean:
        -rm -f parkplatz *.o

Auch Testprogramme für einzelne Bausteine und das Komplettsystem lassen sich im Makefile unterbringen. Regressionstests stellen sicher, dass ein Programm auch nach einer Quelltextänderung wieder stabil läuft. Ein automatisierter Test startet dann zum Beispiel mit

make test

Entwicklungsumgebung

Integrierte Entwicklungsumgebungen verwalten die Informationen, was zu einem Programm gehört, in einem "Projekt". Die Benennungen sind bei jedem Produkt etwas anders. Suche nach einem Projektfenster oder Menüeintragen wie

Meist muss man sich hier durch mehrere Bildschirmfenster klicken. Wie übersichtlich und zuverlässig ist dagegen ein Makefile…

Weiter: Teil 9.

1)
Ab C++20 gibt es eine neue Weise, Module zu organisieren. Hier wird noch die bisher übliche Technik beschrieben.