Modulweises Kompilieren

Ein Hauptprogramm h.cpp und die beiden Module up.cpp/up.h (mit irgendeinem Unterprogramm up()) sowie db.c/db.h (mit Funktionen für einen Datenbankzugriff) soll geschrieben werden.

Zuerst werden die beiden Module vorgestellt. Das Modul db simuliert die ,,Datenbank`` nur, indem es einen Typ für die Datensätze definiert (record_t), und davon einfach ein Feld anlegt. Die einzige Funktion lesedb() füllt das Feld ganz stumpf mit lauter gleichen Werten:

// Time-stamp: "(21.11.01 20:24) db.cpp [Klaus Wachtler (aw38)]"
// 
// Implementationsdatei (Funktionsdefinitionen) für Modul db

// Damit der Compiler die Deklarationen in db.h mit den Definitionen
// in dieser Datei vergleichen kann, sicherheitshalber die zugehörigen
// Deklarationen einfügen (hier sind der Typ record_t und das Feld
// arr[] deklariert):
#include "db.h"

// Das Feld wird hier auch definiert:
record_t arr[N_RECORDS];

// könnte Werte aus einer Datenbank in das Feld arr[] lesen; setzt
// hier aber nur ein paar fiktive Werte:
void lesedb( void )
{
  for( int i=0; i<N_RECORDS; i++ )
    {
      arr[i].i = 12;
    }
}

Die zugehörigen Deklarationen für die Anwender des Moduls stehen in db.h:

// Time-stamp: "(21.11.01 20:31) db.h [Klaus Wachtler (aw38)]"
// 
// Funktionsdeklarationen für Modul up

// Damit mehrfaches #includen nicht zu Problemen führt:
#ifndef _DB_H_
#define _DB_H_

#define N_RECORDS  (1000)           // soviele Datensätze

typedef struct { int i; } record_t; // Datentyp für einen Record

extern record_t arr[N_RECORDS];     // alle Records in einem Feld

void lesedb( void );

#endif /// ifndef _DB_H_

Das Modul up enthält nur ein Unterprogramm, um das Feld von lesedb() füllen zu lassen; hier die Implementationsdatei:

// Time-stamp: "(21.11.01 20:26) up.cpp [Klaus Wachtler (aw38)]"
// 
// Implementationsdatei (Funktionsdefinitionen) für Modul up

// Damit der Compiler die Deklarationen in up.h mit den Definitionen
// in dieser Datei vergleichen kann, sicherheitshalber die zugehörigen
// Deklarationen einfügen:
#include "up.h"

void up( void )
{
  lesedb();  // in Modul db.cpp/db.h
}

Und wieder die zugehörigen Deklarationen für die Anwender von up:

// Time-stamp: "(21.11.01 20:27) up.h [Klaus Wachtler (aw38)]"
// 
// Funktionsdeklarationen für Modul up

// Damit mehrfaches #includen nicht zu Problemen führt:
#ifndef _UP_H_
#define _UP_H_

// Deklarationen für Modul db, weil up() eine der db-Funktionen
// benötigt:
#include "db.h"

void up( void );

#endif /// ifndef _UP_H_

Das Hauptprogramm benötigt Deklarationen aus beiden Modulen (weil es erst up() aufruft, sowie das Feld arr[] verwendet), und bindet deshalb auch beide Headerdateien ein:

// Time-stamp: "(21.11.01 20:31) haupt.cpp [Klaus Wachtler (aw38)]"
// 
// Hauptprogramm eines kleinen Beispielprojekts

#include <iostream>

using namespace std;

#include "up.h"  // Deklarationen für Modul up
#include "db.h"  // Deklarationen für Modul db

int main( int nargs, char **args )
{

  up();      // in Modul up.cpp/up.h

  cout << " Wert = "
       << arr[0].i // arr ist in db.h deklariert
       << endl;

  return 0;
}

(Daß dadurch versehentlich db.h doppelt eingebunden wird, soll nicht weiter stören; hier greift der Mechanismus mit #ifndef _DB_H_....)

Theoretisch könnte man jetzt das fertige Programm mit einem einzigen Compiler- und Linkeraufruf übersetzen und linken lassen:

g++ -Wall haupt.cpp up.cpp db.cpp -o haupt

Das ist zwar soweit korrekt, aber bei größeren Projekten nicht wünschenswert. Wenn es nicht wie hier nur um wenige Zeilen Quelltext geht, sondern um Dutzende Quelldateien mit jeweils einigen Tausend Zeilen, dann benötigt das Übersetzen doch eine gewisse Zeit. Wenn man aber nur an wenigen Stellen etwas geändert hat, wäre es interessant, auch nur die zugehörigen Module zu kompilieren und dann alles zu linken.

Dazu übersetzt man beim ersten Mal nicht mit dem einen obigen Aufruf, sondern Modul für Modul:

g++ -Wall -c haupt.cpp
g++ -Wall -c up.cpp
g++ -Wall -c db.cpp
Dadurch werden die beiden Module und das Hauptprogramm nur jeweils einzeln kompiliert (wegen der Option -c), und in je einer Objektdatei abgelegt (haupt.o, up.o, db.o).

Erst anschließend fügt man diese Objektdateien mit dem Linker zu einem ausführbaren Programm zusammen:

g++ -Wall haupt.o up.o db.o -o haupt

Insofern hat man bisher nichts gewonnen; mit mehr Kommandos hat man das selbe Ergebnis wie in einfachen Fall zu Beginn. Der Vorteil ist aber, daß die Objektdateien (haupt.o, up.o, db.o) nach dem Linken noch erhalten bleiben, und bei Bedarf wieder verwendet werden können.

Das ist der Fall, wenn man jetzt an einzelnen Stellen im Quelltext etwas ändert.

Dafür ein paar einfache Beispiele:

Insoweit ist die Lage recht übersichtlich. Was passiert aber, wenn man an den Headerdateien Änderungen vornimmt?

AnyWare@Wachtler.de