Menu

Заголовочные файлы в C: .h против .c, стражи включения и многофайловые программы

Как разбить программу на C по файлам: что относится к .h, что к .c, стражи включения против двойного подключения, компиляция нескольких файлов вместе и разделяемые глобальные переменные через extern.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Один файл .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.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ