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,unionienum, - deklaracje
typedef, - makra przeznaczone do współdzielenia,
- deklaracje
externwspół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, #includenagłó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.