Menu

Pliki nagłówkowe w C: .h a .c, include guard i programy wieloplikowe

Jak podzielić program w C na pliki: co należy do .h, co do .c, include guardy chroniące przed podwójnym dołączeniem, kompilowanie kilku plików razem i współdzielenie zmiennych globalnych przez extern.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Pojedynczy plik .c jest w porządku, dopóki nie ma 2000 linii. Podział programu na pliki pozwala kompilować każdą część osobno, używać jej w innych programach i czytać ją samodzielnie, ale C nie ma systemu importów. Ma za to preprocesor, który wkleja tekst, i linker, który na końcu łączy kawałki.

Plik nagłówkowy (.h) to wspólny kontrakt między tymi kawałkami: mówi każdemu plikowi źródłowemu, co istnieje gdzie indziej, nie zawierając implementacji.

Deklaracje a definicje

Cała konstrukcja opiera się na jednym rozróżnieniu.

Deklaracja mówi: to gdzieś istnieje i tak wygląda. Nie generuje kodu i może się pojawić dowolną liczbę razy:

int add(int a, int b);        /* deklaracja funkcji (prototyp) */
extern int error_count;       /* deklaracja zmiennej */
struct Point { int x, y; };   /* definicja typu: można powtórzyć w każdym pliku */

Definicja tworzy daną rzecz. Musi wystąpić dokładnie raz w całym programie:

int add(int a, int b) { return a + b; }   /* definicja funkcji */
int error_count = 0;                      /* definicja zmiennej */

Nagłówki zawierają deklaracje. Pliki źródłowe zawierają definicje. Odwróć to, a linker zgłosi "multiple definition of ...": ten komunikat niezawodnie oznacza, że definicja zawędrowała do nagłówka.

Program z dwóch plików

Oto najmniejszy sensowny podział. Nagłówek deklarujący dwie funkcje:

/* 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

Plik źródłowy, który je implementuje (zwróć uwagę, że dołącza własny nagłówek):

/* 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;
}

I program, który z nich korzysta:

/* 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;
}

Skompiluj oba pliki źródłowe razem:

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

Warto zauważyć dwa szczegóły. Po pierwsze, math_utils.c dołącza własny nagłówek i nie jest to zbędne. Dzięki temu kompilator sprawdza, czy każda definicja pasuje do swojej deklaracji, więc jeśli zmienisz prototyp w nagłówku i zapomnisz o pliku .c, od razu dostaniesz błąd, a nie niezgodność przy linkowaniu.

Po drugie, math_utils.h nie stoi w poleceniu gcc. Nagłówków nigdy się nie kompiluje; #include wkleja je do plików .c. Przekazanie .h kompilatorowi tworzy zbędny plik prekompilowanego nagłówka i żadnego kodu do linkowania.

Ten sam program ściśnięty do jednego pliku, żeby dało się go tu uruchomić:

Deklaracje przed main to w prawdziwej wersji zawartość nagłówka i właśnie dlatego prototypy funkcji i nagłówki to ta sama idea w dwóch skalach.

Include guardy

#include wkleja tekst, a tekst wklejony dwa razy jest zdublowany. Dla prototypu funkcji to nieszkodliwe, ale dla struct zabójcze:

/* shapes.h BEZ guarda */
struct Point { int x, y; };

Jeśli main.c dołącza zarówno shapes.h, jak i canvas.h, a canvas.h też dołącza shapes.h, kompilator widzi struct Point zdefiniowane dwa razy w jednej jednostce translacji i zatrzymuje się z komunikatem "redefinition of 'struct Point'". W prawdziwym projekcie takie łańcuchy robią się na tyle głębokie, że nie da się ich śledzić ręcznie.

Rozwiązaniem jest include guard: makro, które zapamiętuje, że "ten nagłówek został już wklejony".

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

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

#endif /* SHAPES_H */

Przy pierwszym dołączeniu SHAPES_H nie jest zdefiniowane, więc treść zostaje zachowana, a po drodze SHAPES_H zostaje zdefiniowane. Każde kolejne dołączenie w tym samym pliku zastaje je zdefiniowane i przeskakuje prosto do #endif. Nazwa makra musi być unikalna w całym projekcie; zwykle stosuje się konwencję FILENAME_H utworzoną ze ścieżki.

Jednolinijkową alternatywę obsługuje każdy popularny kompilator:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once nie może spowodować kolizji nazw i nie zepsuje go literówka w #endif. Jej jedyną wadą jest to, że nie ma jej w standardzie C, więc projekt, który musi się budować na nietypowych kompilatorach, powinien wybrać formę #ifndef. Tak czy inaczej, każdy nagłówek dostaje guarda, bez wyjątków, także nagłówki, których według ciebie nic innego nie dołączy.

Co należy do nagłówka

Do .h wstaw:

  • prototypy funkcji,
  • definicje struct, union i enum,
  • deklaracje typedef,
  • makra przeznaczone do współdzielenia,
  • deklaracje extern współdzielonych zmiennych globalnych,
  • dyrektywy #include, których sam nagłówek potrzebuje, żeby był samowystarczalny.

Poza .h trzymaj:

  • ciała funkcji (chyba że celowo static inline),
  • definicje zmiennych: int counter; w nagłówku definiuje osobną zmienną w każdym pliku, który go dołącza, albo daje błąd linkowania, zależnie od kompilatora,
  • #include nagłówków, których sam nagłówek nie potrzebuje, bo przerzuca to zależność na wszystkich dalej.

"Samowystarczalność" warto uczynić regułą: nagłówek powinien się kompilować, gdy jest dołączony jako pierwszy, przed czymkolwiek innym. Jeśli shapes.h używa size_t, sam dołącza <stddef.h>, zamiast liczyć na to, że zrobił to plik dołączający.

Kompletny, dobrze zbudowany nagłówek:

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

#include <stddef.h>   /* dla size_t, używanego niżej */

#define MAX_NAME 64

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

/* współdzielona w całym programie, zdefiniowana raz w inventory.c */
extern int item_count;

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

#endif /* INVENTORY_H */

Współdzielenie zmiennej globalnej przez extern

Zmienna globalna musi być zdefiniowana w dokładnie jednym pliku .c i zadeklarowana wszędzie indziej. Deklarację tworzy właśnie extern:

/* inventory.h: deklaracja, bez pamięci */
extern int item_count;
/* inventory.c: jedyna definicja */
#include "inventory.h"
int item_count = 0;
/* main.c: używa jej przez nagłówek */
#include <stdio.h>
#include "inventory.h"

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

Usuń extern z nagłówka, a każdy plik, który go dołącza, zdefiniuje własne item_count. W najlepszym razie skończy się to błędem linkowania "multiple definition", a w najgorszym dwoma niezależnymi licznikami.

Równie częsta jest odwrotna potrzeba: zmienna albo funkcja pomocnicza, która ma pozostać prywatna w jednym pliku .c. Załatwia to static na poziomie pliku: daje nazwie łączność wewnętrzną, niewidoczną dla linkera, a więc dla każdego innego pliku:

/* inventory.c */
static Item storage[256];          /* prywatne dla tego pliku */
static int  find_slot(const char *name);   /* prywatna funkcja pomocnicza */

Dwa pliki mogą mieć każdy własne static int counter; bez kolizji. To wersja prywatnego pola w C i po nią warto sięgać domyślnie: do nagłówka trafia tylko to, czego inne pliki naprawdę potrzebują.

Kompilowanie większych programów

Wypisanie wszystkich plików działa, ale jest wolne, bo za każdym razem każdy plik jest kompilowany od nowa:

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

Skalowalna forma kompiluje każdy plik źródłowy do pliku obiektowego i linkuje je:

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

Teraz zmiana report.c wymaga tylko gcc -c report.c i ponownego linkowania. Dokładnie tę księgowość automatyzuje make:

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

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

Jeśli nagłówki leżą w podkatalogu, -Iinclude dodaje go do ścieżki wyszukiwania dla nawiasów ostrych.

Błędy i ich znaczenie

Dwa rodzaje awarii łatwo rozróżnić, gdy wiesz, który etap je zgłosił.

"undefined reference to 'add'": błąd linkera. Deklaracja została znaleziona, definicja nie. Albo zapomniano wpisać pliku .c w wierszu poleceń, albo funkcja jest static, albo nazwa ma literówkę w jednym z dwóch miejsc.

"multiple definition of 'item_count'": też błąd linkera, lustrzane odbicie poprzedniego: definicja trafiła do nagłówka albo do dwóch plików źródłowych. Przenieś ją do jednego .c i zostaw w nagłówku deklarację extern.

"redefinition of 'struct Item'": błąd kompilatora, który oznacza, że nagłówek został wklejony dwa razy do jednego pliku. Dodaj include guard.

"implicit declaration of function 'add'": ostrzeżenie kompilatora (w trybach C99 i nowszych błąd), które oznacza, że prototyp nigdy nie został zobaczony. Brakuje #include albo nagłówek jej nie deklaruje.

Gdy program obejmuje wiele plików, zwykle chcesz potem zmieniać to, co jest kompilowane, zależnie od platformy albo rodzaju builda, a do tego służy kompilacja warunkowa.

Najczęściej zadawane pytania

Co trafia do pliku .h, a co do pliku .c?

Nagłówek zawiera deklaracje: prototypy funkcji, definicje struct i typedef, enumy, makra i deklaracje extern współdzielonych zmiennych globalnych. Plik .c zawiera definicje: ciała funkcji i same zmienne. Praktyczna zasada: nagłówek mówi, co istnieje, a plik źródłowy mówi, co to robi.

Czym jest include guard i po co mi on?

#include wkleja tekst, więc dwukrotne dołączenie nagłówka wkleja jego zawartość dwa razy, co ponownie definiuje każdą strukturę i typedef i kończy się błędem kompilacji. Include guard otacza nagłówek przez #ifndef MYHEADER_H / #define MYHEADER_H / #endif, więc przy drugim dołączeniu makro jest już zdefiniowane i treść zostaje pominięta.

Jak skompilować program w C złożony z wielu plików?

Wypisz każdy plik .c w wierszu poleceń: gcc main.c math_utils.c -o app. Nigdy nie wpisuj tam pliku .h: nagłówki są wklejane przez #include, a nie kompilowane samodzielnie. W większych projektach kompiluj do plików obiektowych (gcc -c main.c) i linkuj je, a to właśnie automatyzuje Makefile.

Używać #pragma once czy include guardów z #ifndef?

Oba działają. #pragma once to jedna linia, która nie może spowodować kolizji nazw, i obsługuje ją każdy popularny kompilator, ale nie ma jej w standardzie C. Forma #ifndef/#define/#endif jest standardowa i działa wszędzie. Wybierz jedną i stosuj ją konsekwentnie w projekcie; dla maksymalnej przenośności wybierz formę #ifndef.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ