Один файл .c хорош, пока в нём не стало 2000 строк. Разбиение программы по файлам позволяет компилировать каждую часть отдельно, переиспользовать её в других программах и читать саму по себе — но в C нет системы импорта. Есть препроцессор, вставляющий текст, и компоновщик, который в конце соединяет куски.
Заголовочный файл (.h) — это общий контракт между этими кусками: он сообщает каждому исходному файлу, что существует в других местах, не содержа реализации.
Объявления против определений
Вся конструкция держится на одном различии.
Объявление говорит: это где-то существует, и вот его форма. Оно не порождает кода и может встречаться сколько угодно раз:
int add(int a, int b); /* объявление функции (прототип) */
extern int error_count; /* объявление переменной */
struct Point { int x, y; }; /* определение типа - можно повторять в каждом файле */
Определение создаёт саму сущность. Оно должно встречаться ровно один раз во всей программе:
int add(int a, int b) { return a + b; } /* определение функции */
int error_count = 0; /* определение переменной */
В заголовках — объявления. В исходных файлах — определения. Перепутайте это, и компоновщик пожалуется на «multiple definition of ...» — то самое сообщение, которое надёжно означает, что определение забрело в заголовок.
Программа из двух файлов
Вот минимальное полезное разбиение. Заголовок, объявляющий две функции:
/* 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
Исходный файл, который их реализует, — обратите внимание, он подключает собственный заголовок:
/* 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;
}
И программа, которая их использует:
/* 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;
}
Скомпилируйте оба исходных файла вместе:
gcc main.c math_utils.c -o app
./app
Стоит заметить две детали. Во-первых, math_utils.c подключает собственный заголовок — это не избыточность. Так компилятор проверяет, что каждое определение соответствует своему объявлению, и если вы измените прототип в заголовке и забудете файл .c, ошибка возникнет сразу, а не в виде несовпадения на этапе компоновки.
Во-вторых, math_utils.h не указан в командной строке gcc. Заголовки никогда не компилируются; они вставляются в файлы .c директивой #include. Передача .h компилятору порождает посторонний файл предкомпилированного заголовка и никакого пригодного для компоновки кода.
Та же программа, ужатая в один файл, чтобы её можно было запустить прямо здесь:
Объявления перед main — это то, что в настоящей версии даёт заголовок; именно поэтому прототипы функций и заголовки — одна и та же идея в двух масштабах.
Стражи включения
#include вставляет текст, а текст, вставленный дважды, — это продублированный текст. Для прототипа функции это безвредно, а для struct — фатально:
/* shapes.h БЕЗ стража */
struct Point { int x, y; };
Если main.c подключает и shapes.h, и canvas.h, а canvas.h тоже подключает shapes.h, компилятор видит struct Point, определённую дважды в одной единице трансляции, и останавливается с «redefinition of 'struct Point'». В настоящем проекте такие цепочки становятся достаточно длинными, чтобы их нельзя было отследить вручную.
Решение — страж включения: макрос, фиксирующий, что «этот заголовок уже вставлен».
/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H
struct Point { int x, y; };
struct Point origin_point(void);
#endif /* SHAPES_H */
Первое подключение обнаруживает, что SHAPES_H не определён, поэтому тело сохраняется — и по пути определяет SHAPES_H. Каждое последующее подключение в том же файле находит его определённым и сразу перескакивает к #endif. Имя макроса должно быть уникальным в проекте; обычное соглашение — FILENAME_H, выведенное из пути.
Однострочная альтернатива поддерживается всеми основными компиляторами:
/* shapes.h */
#pragma once
struct Point { int x, y; };
#pragma once не может пострадать от коллизии имён и не ломается от опечатки в #endif. Его единственный недостаток — он не входит в стандарт C, поэтому проекту, который должен собираться на необычных компиляторах, стоит предпочесть форму #ifndef. В любом случае страж получает каждый заголовок — без исключений, включая те, которые, как вам кажется, больше никто не подключит.
Что относится к заголовку
Помещайте в .h:
- Прототипы функций
- Определения
struct,unionиenum - Объявления
typedef - Макросы, предназначенные для совместного использования
extern-объявления разделяемых глобальных переменных- Те
#include, которые нужны самому заголовку, чтобы быть самодостаточным
Не помещайте в .h:
- Тела функций (кроме намеренных
static inline) - Определения переменных —
int counter;в заголовке определяет отдельную переменную в каждом подключающем файле либо даёт ошибку компоновки, в зависимости от компилятора #includeзаголовков, которые самому заголовку не нужны, — это перекладывает зависимость на всех, кто идёт дальше по цепочке
«Самодостаточность» стоит сделать правилом: заголовок должен компилироваться, если подключён первым, до всего остального. Если shapes.h использует size_t, он сам подключает <stddef.h>, а не надеется, что это сделал подключающий файл.
Полный, правильно оформленный заголовок:
/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H
#include <stddef.h> /* ради size_t, используется ниже */
#define MAX_NAME 64
typedef struct {
char name[MAX_NAME];
int quantity;
double price;
} Item;
/* разделяется всей программой, определяется один раз в inventory.c */
extern int item_count;
void inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);
#endif /* INVENTORY_H */
Разделяемая глобальная переменная через extern
Глобальная переменная должна быть определена ровно в одном файле .c и объявлена во всех остальных. Объявление делает extern:
/* inventory.h - объявление, без выделения памяти */
extern int item_count;
/* inventory.c - единственное определение */
#include "inventory.h"
int item_count = 0;
/* main.c - использует её через заголовок */
#include <stdio.h>
#include "inventory.h"
int main(void) {
printf("%d items\n", item_count);
return 0;
}
Уберите extern из заголовка — и каждый подключающий файл определит собственный item_count, что в лучшем случае даёт ошибку компоновки «multiple definition», а в худшем — два независимых счётчика.
Противоположная потребность встречается не реже: переменная или вспомогательная функция, которая должна остаться закрытой внутри одного файла .c. Это делает static на уровне файла — он даёт имени внутреннее связывание, невидимое компоновщику, а значит и всем остальным файлам:
/* inventory.c */
static Item storage[256]; /* приватно для этого файла */
static int find_slot(const char *name); /* приватная вспомогательная функция */
В двух файлах может быть по своему static int counter; без всякой коллизии. Это версия приватного члена в C, и именно к ней стоит тянуться по умолчанию — в заголовок попадает только то, что другим файлам действительно нужно.
Компиляция программ побольше
Перечислять все файлы работает, но медленно, потому что каждый файл пересобирается каждый раз:
gcc main.c inventory.c report.c -o app
Масштабируемая форма компилирует каждый исходник в объектный файл и компонует их:
gcc -c main.c # даёт main.o
gcc -c inventory.c # даёт inventory.o
gcc -c report.c # даёт report.o
gcc main.o inventory.o report.o -o app
Теперь изменение report.c требует только gcc -c report.c и повторной компоновки. Именно эту бухгалтерию автоматизирует make:
app: main.o inventory.o report.o
gcc main.o inventory.o report.o -o app
%.o: %.c
gcc -Wall -Wextra -c $< -o $@
Если ваши заголовки лежат в подкаталоге, -Iinclude добавляет его к путям поиска для угловых скобок.
Ошибки и что они значат
Два режима отказа легко различить, как только вы знаете, какая стадия их породила.
«undefined reference to 'add'» — ошибка компоновщика. Объявление нашлось, определение — нет. Либо вы забыли указать файл .c в командной строке, либо функция объявлена static, либо имя написано с ошибкой в одном из двух мест.
«multiple definition of 'item_count'» — тоже ошибка компоновщика, зеркальная: определение оказалось в заголовке или в двух исходных файлах. Перенесите его в один .c, а в заголовке оставьте extern-объявление.
«redefinition of 'struct Item'» — ошибка компилятора: заголовок был вставлен в один файл дважды. Добавьте страж включения.
«implicit declaration of function 'add'» — предупреждение компилятора (ошибка в режимах C99 и позже), означающее, что прототип так и не был увиден. Вы забыли #include либо заголовок его не объявляет.
Когда программа занимает несколько файлов, следующее, чего обычно хочется, — менять состав компилируемого кода в зависимости от платформы или типа сборки; это условная компиляция.
Часто задаваемые вопросы
Что писать в файле .h, а что в файле .c?
В заголовке — объявления: прототипы функций, определения struct и typedef, enum, макросы и extern-объявления разделяемых глобальных переменных. В файле .c — определения: тела функций и сами переменные. Правило: заголовок говорит, что существует, исходный файл — что оно делает.
Что такое страж включения и зачем он нужен?
#include вставляет текст, поэтому подключение заголовка дважды вставляет его содержимое дважды — а это переопределяет каждую структуру и каждый typedef в нём и не компилируется. Страж включения оборачивает заголовок в #ifndef MYHEADER_H / #define MYHEADER_H / #endif, так что второе подключение видит уже определённый макрос и пропускает тело.
Как скомпилировать программу на C из нескольких файлов?
Перечислите все файлы .c в командной строке: gcc main.c math_utils.c -o app. Никогда не указывайте там файл .h — заголовки вставляются директивой #include, а не компилируются сами по себе. Для больших проектов компилируйте в объектные файлы (gcc -c main.c) и компонуйте их — именно это автоматизирует Makefile.
Что использовать: #pragma once или стражи #ifndef?
Работает и то, и другое. #pragma once — одна строка, в которой не может быть коллизии имён, и его поддерживают все основные компиляторы, но в стандарт C он не входит. Форма #ifndef/#define/#endif стандартна и работает везде. Выберите одно и применяйте последовательно в проекте; ради максимальной переносимости берите форму #ifndef.