16 Aufteilung auf mehrere Quelltexte

Ab einer gewissen Programmgröße ist es sinnvoll, den Quelltext auf mehrere Dateien aufzuteilen. Dies ist mit mehreren Vorteilen verbunden:

Auf der anderen Seite handelt man sich beim Aufteilen auf mehrere Quelltexte etliche neue Fehlerquellen ein, da der C-Compiler immer nur einen Quelltext auf einmal ,,sieht`` und über Quelldateien hinweg keine Fehlerüberprüfung machen kann. Abgesehen von Hilfsprogrammen dazu (lint unter Unix) kann man durch Einhaltung einer gewissen Systematik die Gefahren aber relativ leicht und effektiv umschiffen. So ist dieser Abschnitt weitgehend als Empfehlung zu verstehen (aber als dringende!).

Die allgemein nötige Verwaltungsarbeit der Modularisierung (klare Definition der Schnittstellen, Dokumentation der black boxes, etc. sei hier vorausgesetzt; ich will nur die für C spezifischen Punkte aufführen.

C unterstützt generell die getrennte Kompilierung eines Programms in mehreren Teilen. Die kleinste Einheit, die unteilbar in einem Quelltext stehen muß, ist für Daten eine Variable (einfache, Struktur oder Feld) und für Programmcode eine vollständige Funktion. Das heißt, daß beispielsweise ein Feld nicht teilweise in einer Quelldatei und teilweise in einer anderen definiert werden kann.

Alle in einer Quelldatei definierten globalen Objekte (Variablen und Funktionen), die nicht als static deklariert sind, können von allen anderen Quelltexten aus als extern deklariert und angesprochen werden. Die Definition eines jeden Objektes darf allerdings in allen Quelltexten nur einmal erfolgen.

Dieser Mechanismus unterscheidet sich also von FORTRAN: Dort werden die globalen Daten in allen Quelltexten gleich vereinbart, nämlich als COMMON.

In C dagegen muß man dafür sorgen, daß alle Objekte in allen Dateien gleich deklariert werden, außer in einer Datei, die das Objekt auch definiert30.

In welchen Quelldateien man seine globalen Variablen definiert, ist formal gleichgültig. Nach Belieben kann man auch separate Quelltexte schreiben, in denen nur Datendefinitionen stehen; ein C-Quelltext muß keine Funktionen enthalten.

Um den Mechanismus zu verdeutlichen, soll ein kleines Projekt vorgestellt werden:

Dieses Projekt läßt sich natürlich auch leicht in einem Quelltext realisieren:

/* sp1q.c 30. 7.95 kw
 */

#include <stdio.h>

#define     PI      3.14159265  /* Kreiszahl                    */
#define     LENG    20          /* solange darf der Name sein   */

char        name[LENG];
char        masseinheit[LENG];

FILE       *log_f;

/* Holt eine double von der Tastatur:
 */
double hole_double( const char *s1 )
{
  double wert;

  printf( "Bitte %s [%s] eingeben, lieber %s: ",
          s1,          masseinheit,
          name
          );
  scanf( "%lf", &wert );
  return wert;
}

/* Holt einen String:
 */
void hole_string( const char *s, char *wohin )
{
  printf( "Bitte %s eingeben: ", s );
  scanf( "%s", wohin );
}

/* gibt einen Radius in die Datei log_f aus:
 */
void schreibe_radius( double radius )
{
  fprintf( log_f, "Radius %f [%s]\n", radius, masseinheit );
}

void schreibe_umfang_flaeche( double umfang, double flaeche )
{
  printf( "\nUmfang = %f [%s], Flaeche = %f [%s*%s]\n",
          umfang,
          masseinheit,
          flaeche,
          masseinheit,
          masseinheit
          );
  fprintf( log_f,
           "\nUmfang = %f [%s], Flaeche = %f [%s*%s]\n",
           umfang,
           masseinheit,
           flaeche,
           masseinheit,
           masseinheit
           );
}

/* Macht alle Initialisierungen:
 */
void start( void )
{
  /* Die Protokolldatei oeffnen:
   */
  if( (log_f=fopen( "RADIUS.LOG", "w" ))==NULL )
  {
    printf( "Ich kann die "
            "Protokolldatei nicht oeffnen!\n" );
    exit( 3 );
  }

  /* Den Benutzernamen holen:
     */
  hole_string( "Ihren Namen", name );

  /* dto. die Einheit:
     */
  hole_string( "die Masseinheit", masseinheit );

  /* Den Kopf der Protokolldatei schreiben:
     */
  fprintf( log_f,
           "Protokolldatei Kreisberechnung\n"
           "Benutzer %s, Masseinheit [%s]\n",
           name,
           masseinheit
           );
}

/* Raeumt alles wieder auf:
 */
void ende( int rueckgabe )
{
  fprintf( log_f, "\nProgrammende!\n" );
  fclose( log_f );
  printf( "\nSchoenen Tag noch, %s!\n", name );
  exit( rueckgabe );
}

/* Berechnet zu einem Radius den Kreisumfang:
 */
double rechne_umfang( double r )
{
  return 2.0*r*PI;
}

/* Berechnet zu einem Radius die Kreisflaeche:
 */
double rechne_flaeche( double r )
{
  return r*r*PI;
}

int main()
{
  double
    radius,
    umfang,
    flaeche;

  start();   /* Datei Oeffnen etc.                        */

  /* Solange Radius positiv: Lesen ...
   */
  while(   (radius=hole_double( "Radius" ))
           > 0.0
           )
  {
    /* ... Rechnen ...
     */
    umfang  = rechne_umfang( radius );
    flaeche = rechne_flaeche( radius );
    /* ... und Ausgeben:
     */
    schreibe_radius( radius );
    schreibe_umfang_flaeche( umfang, flaeche );
  }

  /* alles aufraeumen und Programm beenden.
   */
  ende( 0 );
}

Globale Objekte in diesem Programm sind die Variablen name, masseinheit und log_f sowie alle Funktionsnamen.

Das Programm soll nun sinnvoll auf kleinere Quelldateien aufgeteilt werden, da es dem hochgeschätzten Vorgesetzten zu groß ist und er schon mal etwas von modularer Programmierung gehört hat. Dazu muß man die globalen Variablen in jeder Datei als extern deklarieren, außer in einer. Dort muß die jeweilige Variable wie sonst auch definiert werden.

Wir teilen das Programm in

Die aufgerufenen Funktionen sollten vor ihrer ersten Verwendung in jeder Quelldatei deklariert werden. Dann sieht das erste Modul (Ein-/Ausgabe) so aus:

/* spio1.c 30. 7.95 kw
 */

#include <stdio.h>

#define     PI      3.14159265  /* Kreiszahl                    */
#define     LENG    20          /* solange darf der Name sein   */

extern char     name[LENG];
extern char     masseinheit[LENG];

extern FILE    *log_f;

/* Holt eine double von der Tastatur:
 */
double hole_double( const char *s1 )
{
  double wert;

  printf( "Bitte %s [%s] eingeben, lieber %s: ",
          s1,
          masseinheit,
          name
          );
  scanf( "%lf", &wert );
  return wert;
}

/* Holt einen String:
 */
void hole_string( const char *s, char *wohin )
{
  printf( "Bitte %s eingeben: ", s );
  scanf( "%s", wohin );
}

/* gibt einen Radius in die Datei log_f aus:
 */
void schreibe_radius( double radius )
{
  fprintf( log_f, "Radius %f [%s]\n", radius, masseinheit );
}

void schreibe_umfang_flaeche( double umfang, double flaeche )
{
  printf( "\nUmfang = %f [%s], Flaeche = %f [%s*%s]\n",
          umfang,
          masseinheit,
          flaeche,
          masseinheit,
          masseinheit
          );
  fprintf( log_f,
           "\nUmfang = %f [%s], Flaeche = %f [%s*%s]\n",
           umfang,
           masseinheit,
           flaeche,
           masseinheit,
           masseinheit
           );
}

Das Modul sprech1.c könnte so aussehen:

/* sprech1.c 30. 7.95 kw
 */

#define     PI      3.14159265  /* Kreiszahl                    */
#define     LENG    20          /* solange darf der Name sein   */

extern char     name[LENG];
extern char     masseinheit[LENG];

/* Berechnet zu einem Radius den Kreisumfang:
 */
double rechne_umfang( double r )
{
  return 2.0*r*PI;
}

/* Berechnet zu einem Radius die Kreisflaeche:
 */
double rechne_flaeche( double r )
{
  return r*r*PI;
}

Das Hauptmodul enthält von den aufgerufenen Funktionen nur noch die Deklarationen (Funktionsprototypen). Die globalen Variablen sind hier dagegen definiert, während sie in den beiden obigen Quelltexten als extern deklariert sind:

/* sphaupt1.c 30. 7.95 kw
 */

#include <stdio.h>
#include <stdlib.h>

#define     PI      3.14159265  /* Kreiszahl                    */
#define     LENG    20          /* solange darf der Name sein   */

/* Die Variablen hier nicht als extern deklarieren, sondern
 * definieren!!!
 */

char        name[LENG];
char        masseinheit[LENG];

FILE       *log_f;

/* Die aufgerufenen Funktionen deklarieren:
 */
double hole_double( const char *s1 );
void hole_string( const char *s, char *wohin );
void schreibe_radius( double radius );
void schreibe_umfang_flaeche( double umfang, double flaeche );
double rechne_umfang( double r );
double rechne_flaeche( double r );

/* Macht alle Initialisierungen:
 */
void start( void )
{
  /* Die Protokolldatei oeffnen:                          */
  if( (log_f=fopen( "RADIUS.LOG", "w" ))==NULL )
  {
    printf( "Ich kann die "
            "Protokolldatei nicht oeffnen!\n" );
    exit( 3 );
  }

  /* Den Benutzernamen holen:
     */
  hole_string( "Ihren Namen", name );

  /* dto. die Einheit:
     */
  hole_string( "die Masseinheit", masseinheit );

  /* Den Kopf der Protokolldatei schreiben:
     */
  fprintf( log_f,
           "Protokolldatei Kreisberechnung\n"
           "Benutzer %s, Masseinheit [%s]\n",
           name,
           masseinheit                        );
}

/* Raeumt alles wieder auf:
 */
void ende( int rueckgabe )
{
  fprintf( log_f, "\nProgrammende!\n" );
  fclose( log_f );
  printf( "\nSchoenen Tag noch, %s!\n", name );
  exit( rueckgabe );
}

int main()
{
  double
    radius,
    umfang,
    flaeche;

  /* Datei oeffnen etc.
   */
  start();

  /* Solange Radius positiv: Lesen ...
   */
  while(   (radius=hole_double( "Radius" ))
           > 0.0
           )
  {
    /* ... Rechnen ...
     */
    umfang  = rechne_umfang( radius );
    flaeche = rechne_flaeche( radius );
    /* ... und Ausgeben:
     */
    schreibe_radius( radius );
    schreibe_umfang_flaeche( umfang, flaeche );
  }

  /* alles aufrauemen und Programm beenden.
   */
  ende( 0 );
}

Nach dem getrennten Kompilieren dieser drei Quelltexte werden die erzeugten Objektdateien mit einem Linker zu einem lauffähigen Programm zusammengebunden.

Diese Vorgehensweise funktioniert zwar, hat aber einige Nachteile:

Diese Nachteile lassen sich durch gezielten Einsatz des Praeprozessors eliminieren. Zuerst kann man die identischen Teile der Quelltexte in einer sogenannten header-Datei zusammenfassen und mit #include in alle Module einbinden. Wenn ein Objekt (Variable oder Funktion) vor seiner Definition schon deklariert wird, dann ist das kein Fehler. So kann man die Headerdatei mit den Deklarationen auch in den Modulen #includen, in denen die Objekte danach noch definiert werden.

Die Vereinbarungen:

    /* Deklarationen:              */
    extern int variable;
    void funktion( int i );

    /* Definitionen:               */
    int variable;
    void funktion( int i )
        {   printf( "%d", i );
        }
sind also zulässig.

Die Headerdatei spkreis.h in unserem Beispiel könnte also so aussehen:

/* spkreis.h 30. 7.95 kw
 */

#define     PI      3.14159265  /* Kreiszahl                    */
#define     LENG    20          /* solange darf der Name sein   */

/* globale Variablen:                                           */
extern char        name[LENG];
extern char        masseinheit[LENG];

extern FILE       *log_f;

/* Die aufgerufenen Funktionen deklarieren:
 */
double hole_double( const char *s1 );
void hole_string( const char *s, char *wohin );
void schreibe_radius( double radius );
void schreibe_umfang_flaeche( double umfang, double flaeche );
double rechne_umfang( double r );
double rechne_flaeche( double r );
void start( void );
void ende( int rueckgabe );


Diese Datei kann in alle Quelldateien mit #include eingebunden werden. Eigene Vereinbarungen werden üblicherweise im gleichen Verzeichnis bzw. directory stehen wie die Quelltexte; dann muß man die Form #include ¨...¨ verwenden. Nur die Includedateien, welche bei den Standardheaderdateien (z.B. stdio.h) zu finden sind, werden mit #include <...> eingefügt.

Die Namen von Headerdateien enden fast immer auf .h, aber das ist nur Konvention. Die Dateinamen für eigene Headerdateien sind (je nach Betriebssystem) mehr oder weniger frei wählbar.

Die Quelldateien spio2.c, sprech2.c und sphaupt2.c sehen nun so aus (die einzelnen Dateien sind mit einem Strich voneinander abgesetzt):

/* spio2.c 30. 7.95 kw
 */

#include <stdio.h>

#include "kreis.h"

/* Holt eine double von der Tastatur:
 */
double hole_double( const char *s1 )
{
  double wert;

  printf( "Bitte %s [%s] eingeben, lieber %s: ",
          s1,
          masseinheit,
          name
          );
  scanf( "%lf", &wert );
  return wert;
}

/* Holt einen String:
 */
void hole_string( const char *s, char *wohin )
{
  printf( "Bitte %s eingeben: ", s );
  scanf( "%s", wohin );
}

/* gibt einen Radius in die Datei log_f aus:
 */
void schreibe_radius( double radius )
{
  fprintf( log_f, "Radius %f [%s]\n", radius, masseinheit );
}

void schreibe_umfang_flaeche( double umfang, double flaeche )
{
  printf( "\nUmfang = %f [%s], Flaeche = %f [%s*%s]\n",
          umfang,
          masseinheit,
          flaeche,
          masseinheit,
          masseinheit
          );
  fprintf( log_f,
           "\nUmfang = %f [%s], Flaeche = %f [%s*%s]\n",
           umfang,
           masseinheit,
           flaeche,
           masseinheit,
           masseinheit
           );
}

/* sprech2.c 30. 7.95 kw
 */

#include <stdio.h>

#include "kreis.h"

/* Berechnet zu einem Radius den Kreisumfang:
 */
double rechne_umfang( double r )
{
  return 2.0*r*PI;
}

/* Berechnet zu einem Radius die Kreisflaeche:
 */
double rechne_flaeche( double r )
{
  return r*r*PI;
}

/* sphaupt2.c 30. 7.95 kw
 */

#include <stdio.h>
#include <stdlib.h>

#include "kreis.h"

/* globale Variablen definieren:
 */
char        name[LENG];
char        masseinheit[LENG];

FILE       *log_f;

/* Macht alle Initialisierungen:
 */
void start( void )
{
  /* Die Protokolldatei oeffnen:
   */
  if( (log_f=fopen( "RADIUS.LOG", "w" ))==NULL )
  {
    printf( "Ich kann die "
            "Protokolldatei nicht oeffnen!\n"
            );
    exit( 3 );
  }

  /* Den Benutzernamen holen:
     */
  hole_string( "Ihren Namen", name );

  /* dto. die Einheit:
     */
  hole_string( "die Masseinheit", masseinheit );

  /* Den Kopf der Protokolldatei schreiben:
   */
  fprintf( log_f,
           "Protokolldatei Kreisberechnung\n"
           "Benutzer %s, Masseinheit [%s]\n",
           name,
           masseinheit
           );
}

/* Raeumt alles wieder auf:
 */
void ende( int rueckgabe )
{
  fprintf( log_f, "\nProgrammende!\n" );
  fclose( log_f );
  printf( "\nSchoenen Tag noch, %s!\n", name );
  exit( rueckgabe );
}

int main()
{
  double
    radius,
    umfang,
    flaeche;

  /* Datei oeffnen etc.
   */
  start();

  /* Solange Radius positiv: Lesen ...
   */
  while(   (radius=hole_double( "Radius" ))
           > 0.0
           )
  {
    /* ... Rechnen ...
     */
    umfang  = rechne_umfang( radius );
    flaeche = rechne_flaeche( radius );
    /* ... und Ausgeben:
     */
    schreibe_radius( radius );
    schreibe_umfang_flaeche( umfang, flaeche );
  }

  /* alles aufrauemen und Programm beenden.
   */
  ende( 0 );
}

Damit ist die Redundanz deutlich geringer geworden: Alle Vereinbarungen kommen nur noch an zwei Stellen vor, nämlich in der Headerdatei als Deklaration und im jeweiligen Quelltext als Definition. Durch den Praeprozessor definierte Konstanten (z.B. PI) stehen nur in kreis.h.

Zumindest bei Datenvereinbarungen läßt sich aber auch dieses doppelte Vorkommen noch mit Hilfe des Praeprozessors und einem kleinen Trick beseitigen. Der einzige Unterschied zwischen einer Deklaration und der Definition liegt in der Verwendung des Schlüsselworts extern. Dieses wird vom Compiler ausgewertet. Überredet man den Praeprozessor jetzt dazu, in einer Datei alle extern-Schlüsselworte zu entfernen, dann werden die Variablen allein durch Einfügen der Headerdatei auch definiert.

Den Anfang von sphaupt2.c ändert man also zu:

/* sphaupt3.c
 */

#include <stdio.h>
#include <stdlib.h>

#define extern
#include "kreis.h"
#undef extern

/* globale Variablen brauchen jetzt nicht mehr definiert zu
 * werden!
 */

/* Macht alle Initialisierungen:
 */
void start( void )
    {
        ...

Zur Erklärung: Durch #define extern werden alle folgenden Vorkommen von extern durch nichts ersetzt, auch während kreis.h eingefügt wird. Das nachfolgende #undef extern hebt das #define wieder auf (für den Fall, daß im weiteren Quelltext doch noch extern benötigt wird).

Ein Problem taucht auf, wenn globale Variablen gleich mit der Vereinbarung initialisiert werden sollen. Dies darf nur bei der Definition geschehen; bei der reinen Deklaration ist die Initialisierung verboten. Das Problem kann man aber auch mit dem Praeprozessor lösen, nämlich mit bedingter Kompilierung.

Beispiel: Eine globale Variable i soll in einer Headerdatei vereinbart werden und gleich mit dem Wert 10 initialisiert werden.

Dazu schreibt man in die Headerdatei:

extern int      i
#ifdef extern
                   = 10
#endif
                       ;

Dann kann man (wie oben beschrieben) mit

    #define extern
    #include "..."
    #undef extern
die Datei zur Definition (und Initialisierung) einfügen, dagegen nur mit
    #include "..."
zur Deklaration.

Durch diese Vorgehensweise kommen alle Vereinbarungen nur noch einfach (Variablen, Praeprozessorkonstanten) oder zweifach (Funktionsprototypen) vor.

Dateilokale Objekte: Soll eine Funktion oder eine globale Variable nur von der Datei aus verwendet werden können, in der sie auch definiert ist (also eben nicht global bezüglich aller Quelltexte), dann muß man sie als static vereinbaren (siehe Attributangaben). Damit wird der Name des Objekts nicht dem Linker bekannt gemacht wie sonst alle globalen Namen eines Quelltextes, sondern ist nur in der aktuellen Datei ansprechbar (information hiding).

Mehrfaches #includen: Manchmal kommt es vor, daß eine Includedatei versehentlich mehrfach eingefügt wird, beispielsweise durch geschachtelte #include-Anweisungen. Deshalb wird üblicherweise jede Includedatei von einer #ifndef-Anweisung eingeschlossen und das zugehörige Makro dazwischen definiert.

Beispiel: Die Datei meine.h soll so aussehen:

/* include-Datei meine.h vom 16. 8.91                           */
#define PI    3.141592654           /* Kreiszahl                */
#define EU    2.718281828           /* Eulersche Zahl           */
int wieoft;                         /* eine globale Variable    */

Wird diese Datei versehentlich mehrfach eingefügt, dann sind die Definitionen für den Praeprozessor und den Compiler entsprechend mehrfach vorhanden. Die Lösung:

/* include-Datei meine.h vom 16. 8.91                           */
#ifndef _MEINE_H

#define _MEINE_H

#define PI    3.141592654           /* Kreiszahl                */
#define EU    2.718281828           /* Eulersche Zahl           */
int wieoft;                         /* eine globale Variable    */

#endif
Beim ersten vorkommenden #include ¨meine.h¨ ist das Makro _MEINE_H noch nicht definiert; dadurch ist die #ifndef-Bedingung erfüllt und der Rest der Datei bis zum #endif wird verwertet; unter anderem wird dabei das Makro _MEINE_H definiert (ohne Wert).

Bei einem eventuell nachfolgenden #include ¨meine.h¨ im selben Quelltext ist die #ifndef-Bedingung nicht mehr erfüllt, und die Datei meine.h wird bis zum #endif ignoriert.

AnyWare@Wachtler.de