Menu

Header-Dateien in C: .h gegen .c, Include-Schutz und Programme aus mehreren Dateien

Wie man ein C-Programm auf Dateien aufteilt: was in eine .h gehört, was in eine .c, Include-Schutz gegen doppeltes Einbinden, mehrere Dateien zusammen kompilieren und globale Variablen mit extern teilen.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Eine einzelne .c-Datei ist in Ordnung, bis sie 2.000 Zeilen lang ist. Ein Programm auf Dateien aufzuteilen lässt jeden Teil getrennt übersetzen, in anderen Programmen wiederverwenden und für sich lesen - aber C hat kein Importsystem. Was es hat, ist der Präprozessor, der Text einfügt, plus ein Linker, der die Teile am Ende zusammenfügt.

Eine Header-Datei (.h) ist der gemeinsame Vertrag zwischen diesen Teilen: Sie sagt jeder Quelldatei, was anderswo existiert, ohne die Implementierung zu enthalten.

Deklarationen gegen Definitionen

Der ganze Entwurf ruht auf einer Unterscheidung.

Eine Deklaration sagt das hier existiert irgendwo, und so sieht es aus. Sie erzeugt keinen Code und darf beliebig oft auftauchen:

int add(int a, int b);        /* Funktionsdeklaration (Prototyp) */
extern int error_count;       /* Variablendeklaration */
struct Point { int x, y; };   /* Typdefinition - pro Datei gefahrlos wiederholbar */

Eine Definition erschafft die Sache. Sie muss im ganzen Programm genau einmal auftauchen:

int add(int a, int b) { return a + b; }   /* Funktionsdefinition */
int error_count = 0;                      /* Variablendefinition */

Header halten Deklarationen. Quelldateien halten Definitionen. Mach es andersherum, und der Linker beschwert sich über „multiple definition of ..." - die eine Fehlermeldung, die zuverlässig bedeutet, dass eine Definition in einen Header gewandert ist.

Ein Programm aus zwei Dateien

Hier ist die kleinste nützliche Aufteilung. Ein Header, der zwei Funktionen deklariert:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

Die Quelldatei, die sie implementiert - beachte, dass sie ihren eigenen Header einbindet:

/* math_utils.c */
#include "math_utils.h"

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

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

Und das Programm, das sie benutzt:

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

Kompiliere beide Quelldateien zusammen:

gcc main.c math_utils.c -o app
./app

Zwei Einzelheiten sind bemerkenswert. Erstens bindet math_utils.c seinen eigenen Header ein - das ist nicht überflüssig. Es lässt den Compiler prüfen, dass jede Definition zu ihrer Deklaration passt; änderst du also den Prototyp im Header und vergisst die .c-Datei, bekommst du sofort einen Fehler statt einer Unstimmigkeit beim Linken.

Zweitens steht math_utils.h nicht auf der gcc-Kommandozeile. Header werden nie kompiliert; sie werden per #include in .c-Dateien eingefügt. Eine .h an den Compiler zu übergeben erzeugt eine verirrte vorkompilierte Header-Datei und keinen linkbaren Code.

Dasselbe Programm in eine Datei gequetscht, damit du es hier ausführen kannst:

Die Deklarationen vor main sind das, was der Header in der echten Fassung liefert - und genau deshalb sind Funktionsprototypen und Header dieselbe Idee auf zwei Größenordnungen.

Include-Schutz

#include fügt Text ein, und zweimal eingefügter Text ist verdoppelter Text. Für einen Funktionsprototyp ist das harmlos und für eine struct tödlich:

/* shapes.h OHNE Schutz */
struct Point { int x, y; };

Bindet main.c sowohl shapes.h als auch canvas.h ein, und bindet canvas.h ebenfalls shapes.h ein, sieht der Compiler struct Point zweimal in einer Übersetzungseinheit definiert und hält mit „redefinition of 'struct Point'" an. In einem echten Projekt werden diese Ketten so tief, dass du sie nicht von Hand verfolgen kannst.

Die Lösung ist ein Include-Schutz: ein Makro, das festhält „dieser Header wurde bereits eingefügt".

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

Das erste Einbinden findet SHAPES_H undefiniert, der Rumpf bleibt also erhalten - und definiert unterwegs SHAPES_H. Jedes spätere Einbinden in derselben Datei findet es definiert und springt direkt zu #endif. Der Makroname muss projektweit eindeutig sein; DATEINAME_H, vom Pfad abgeleitet, ist die übliche Konvention.

Die einzeilige Alternative unterstützt jeder gängige Compiler:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once kann keine Namenskollision erleiden und nicht durch einen Tippfehler im #endif kaputtgehen. Sein einziger Nachteil ist, dass es nicht im C-Standard steht, ein Projekt, das auf ungewöhnlichen Compilern bauen muss, sollte also die #ifndef-Form bevorzugen. So oder so bekommt jeder Header einen - ohne Ausnahme, auch Header, von denen du glaubst, dass sie nichts sonst einbindet.

Was in einen Header gehört

In eine .h gehören:

  • Funktionsprototypen
  • struct-, union- und enum-Definitionen
  • typedef-Deklarationen
  • Makros, die geteilt werden sollen
  • extern-Deklarationen gemeinsamer globaler Variablen
  • Die #includes, die der Header selbst braucht, um in sich geschlossen zu sein

Aus einer .h heraus gehören:

  • Funktionsrümpfe (sofern nicht absichtlich static inline)
  • Variablendefinitionen - int counter; in einem Header definiert je nach Compiler in jeder einbindenden Datei eine eigene Variable oder erzeugt einen Linkfehler
  • #includes von Headern, die der Header selbst nicht braucht - das schiebt diese Abhängigkeit auf alle weiter unten

„In sich geschlossen" ist eine Regel wert: Ein Header sollte kompilieren, wenn er als Erstes eingebunden wird, vor allem anderen. Benutzt shapes.h ein size_t, bindet es <stddef.h> selbst ein, statt zu hoffen, dass die einbindende Datei es tat.

Ein vollständiger, wohlgeformter Header:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* für size_t, unten benutzt */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* programmweit geteilt, einmal in inventory.c definiert */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

Eine globale Variable mit extern teilen

Eine globale Variable muss in genau einer .c-Datei definiert und überall sonst deklariert werden. extern ist das, was die Deklaration ausmacht:

/* inventory.h  - Deklaration, kein Speicher */
extern int item_count;
/* inventory.c  - die eine Definition */
#include "inventory.h"
int item_count = 0;
/* main.c - benutzt sie über den Header */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d Artikel\n", item_count);
    return 0;
}

Lass das extern im Header weg, und jede einbindende Datei definiert ihr eigenes item_count, was bestenfalls ein Linkfehler wegen „multiple definition" ist und schlimmstenfalls zwei unabhängige Zähler.

Der umgekehrte Bedarf ist genauso häufig: eine Variable oder Hilfsfunktion, die einer .c-Datei privat bleiben soll. static auf Dateiebene erledigt das - es gibt dem Namen interne Bindung, unsichtbar für den Linker und damit für jede andere Datei:

/* inventory.c */
static Item storage[256];                  /* privat für diese Datei */
static int  find_slot(const char *name);   /* privater Helfer */

Zwei Dateien dürfen je ein static int counter; haben, ohne zu kollidieren. Das ist Cs Fassung eines privaten Mitglieds, und es ist die Voreinstellung, zu der du greifen solltest - nur was andere Dateien wirklich brauchen, kommt in den Header.

Größere Programme kompilieren

Jede Datei aufzuführen funktioniert und ist langsam, weil jede Datei jedes Mal neu übersetzt wird:

gcc main.c inventory.c report.c -o app

Die skalierbare Form übersetzt jede Quelle zu einer Objektdatei und linkt sie:

gcc -c main.c        # erzeugt main.o
gcc -c inventory.c   # erzeugt inventory.o
gcc -c report.c      # erzeugt report.o
gcc main.o inventory.o report.o -o app

Jetzt braucht das Ändern von report.c nur gcc -c report.c und ein erneutes Linken. Genau diese Buchführung automatisiert make:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

Wenn deine Header in einem Unterverzeichnis liegen, fügt -Iinclude es dem Suchpfad für spitze Klammern hinzu.

Fehler und was sie bedeuten

Die beiden Fehlerarten sind leicht auseinanderzuhalten, sobald du weißt, welche Stufe sie erzeugt hat.

„undefined reference to 'add'" - ein Linker-Fehler. Die Deklaration wurde gefunden, die Definition nicht. Entweder hast du vergessen, die .c-Datei auf der Kommandozeile aufzuführen, oder die Funktion ist static, oder der Name ist an einer der beiden Stellen falsch geschrieben.

„multiple definition of 'item_count'" - ebenfalls ein Linkerfehler, das Spiegelbild: Eine Definition ist in einem Header oder in zwei Quelldateien gelandet. Verschiebe sie in eine .c und lass eine extern-Deklaration im Header.

„redefinition of 'struct Item'" - ein Compiler-Fehler, der bedeutet, dass ein Header zweimal in eine Datei eingefügt wurde. Füge den Include-Schutz hinzu.

„implicit declaration of function 'add'" - eine Compilerwarnung (in C99 und späteren Modi ein Fehler), die bedeutet, dass der Prototyp nie gesehen wurde. Du hast das #include vergessen, oder der Header deklariert sie nicht.

Sobald ein Programm über Dateien reicht, willst du als Nächstes meist variieren, was je Plattform oder Build-Art übersetzt wird - das ist die bedingte Übersetzung.

Häufig gestellte Fragen

Was kommt in eine .h-Datei und was in eine .c-Datei?

Der Header hält Deklarationen - Funktionsprototypen, struct- und typedef-Definitionen, enums, Makros und extern-Deklarationen gemeinsamer globaler Variablen. Die .c-Datei hält Definitionen - die Funktionsrümpfe und die tatsächlichen Variablen. Die Faustregel: Ein Header sagt, was existiert, eine Quelldatei sagt, was es tut.

Was ist ein Include-Schutz und warum brauche ich ihn?

#include fügt Text ein, einen Header zweimal einzubinden fügt seinen Inhalt also zweimal ein - was jede Struktur und jedes typedef darin neu definiert und nicht kompiliert. Ein Include-Schutz umhüllt den Header mit #ifndef MEINHEADER_H / #define MEINHEADER_H / #endif, sodass das zweite Einbinden das Makro bereits definiert vorfindet und den Rumpf überspringt.

Wie kompiliere ich ein C-Programm aus mehreren Dateien?

Führe jede .c-Datei auf der Kommandozeile auf: gcc main.c math_utils.c -o app. Setze nie eine .h-Datei dorthin - Header werden per #include eingefügt, nicht für sich kompiliert. Für größere Projekte übersetze zu Objektdateien (gcc -c main.c) und linke sie, was ein Makefile automatisiert.

Sollte ich #pragma once oder #ifndef-Include-Schutz benutzen?

Beides funktioniert. #pragma once ist eine Zeile und kann keine Namenskollision haben, und jeder gängige Compiler unterstützt es - aber es steht nicht im C-Standard. Die Form aus #ifndef/#define/#endif ist Standard und funktioniert überall. Wähle eine und benutze sie im Projekt einheitlich; für größtmögliche Portabilität nimm die #ifndef-Form.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S