Программа из нескольких файлов: заголовки, раздельная компиляция, make и библиотека
.h, реализация в .cstatic и externnmundefined reference и multiple definition и находить причину-L и -lmake. Тест по разделу — на портале.Программы прошлых разделов помещались в один файл. Стек из раздела 17 повторялся в трёх практических работах — его копировали. Так нельзя работать дальше:
Модуль в C — пара файлов: заголовок stack.h описывает, что модуль умеет (типы, прототипы функций, комментарии к ним), а реализация stack.c — как. Кто пользуется стеком, включает только заголовок и о реализации не знает. Стандартная библиотека устроена так же: stdio.h — интерфейс, реализация printf лежит в уже скомпилированной библиотеке.
Пример раздела — калькулятор обратной польской записи из раздела 17, разделённый на три файла и два модуля (examples/18/calc/): stack — стек, rpn — вычисление выражения, main.c — ввод и вывод.
/* stack.h — интерфейс модуля «стек целых» */
#ifndef STACK_H
#define STACK_H
#include <stddef.h>
typedef enum { STACK_OK, STACK_EMPTY, STACK_NO_MEMORY } StackStatus;
typedef struct {
long *data;
size_t size, capacity;
} Stack;
/* Сколько раз модуль перевыделял память — определена в stack.c */
extern long stack_reallocs;
void stack_init(Stack *s);
void stack_free(Stack *s);
StackStatus stack_push(Stack *s, long value);
StackStatus stack_pop(Stack *s, long *value);
size_t stack_size(const Stack *s);
#endif
/* stack.c — реализация */
#include <stdlib.h>
#include "stack.h"
long stack_reallocs = 0; /* определение: память под переменную — здесь */
/* static: функция видна только в этом файле, в интерфейс не входит */
static StackStatus grow(Stack *s)
{
…
}
StackStatus stack_push(Stack *s, long value)
{
if (s->size == s->capacity && grow(s) != STACK_OK) {
return STACK_NO_MEMORY;
}
s->data[s->size++] = value;
return STACK_OK;
}
…
extern-переменные. Определения — тела функций и переменные с выделением памяти — в .c. Заголовок включают многие файлы; всё, что в нём есть, появится в каждом из них.#include "stack.h" в кавычках ищет файл сначала рядом с исходным, #include <stdlib.h> — только в системных каталогах. Свои заголовки — в кавычках.stddef.h ради size_t), — чтобы его можно было включить в любой файл в любом порядке.stack_push, STACK_OK, rpn_eval. В C нет пространств имён, и так модули не конфликтуют.Сборка из раздела 1 — препроцессор, компилятор, компоновщик — для нескольких файлов идёт так: каждый .c отдельно превращается в объектный файл .o (машинный код с «дырами» на месте вызовов чужих функций), затем компоновщик (ld) склеивает объектные файлы и библиотеки в программу и заполняет дыры адресами.
gcc -std=c11 -Wall -Wextra -pedantic -g -c main.c → main.o (-c: только компиляция)
gcc -std=c11 -Wall -Wextra -pedantic -g -c rpn.c → rpn.o
gcc -std=c11 -Wall -Wextra -pedantic -g -c stack.c → stack.o
gcc -std=c11 -Wall -Wextra -pedantic -g -o calc main.o rpn.o stack.o → компоновка
./calc < in.txt
3 4 + 2 * = 14
5 1 2 + 4 * + 3 - = 14
1 2 3 4 5 6 7 8 9 + + + + + + + + = 45
4 0 / error: division by zero
stack reallocs: 5
Каждый файл компилируется в одиночку: компилируя rpn.c, gcc видит только его и включённые заголовки. Поэтому для вызова stack_push нужен прототип из stack.h, а где лежит тело функции — дело компоновщика. Что модуль даёт и что ему нужно, показывает программа nm:
$ nm stack.o $ nm rpn.o
U free 000000000000001f t apply
0000000000000000 t grow 0000000000000000 d messages
U realloc 00000000000000e3 T rpn_eval
00000000000000c1 T stack_free 0000000000000000 T rpn_message
000000000000008f T stack_init U stack_free
0000000000000158 T stack_pop U stack_init
00000000000000eb T stack_push U stack_pop
0000000000000000 B stack_reallocs U stack_push
00000000000001b3 T stack_size U stack_size
U strtol …
T — функция определена здесь и видна другим файлам; t — определена, но static, снаружи не видна (grow, apply).U — используется, но не определена (undefined): компоновщик должен найти её в другом файле или библиотеке. rpn.o ждёт пять функций стека, stack.o — realloc и free из стандартной библиотеки.B и d — переменные (B — обнулённые, d — с начальным значением; строчная буква — static).Препроцессор для каждого файла сначала вставляет все заголовки: 23 строки main.c после gcc -E main.c превращаются в 1221 — почти всё из stdio.h.
Заголовки включают друг друга, и один файл легко попадает в программу дважды: main.c включает point.h и geometry.h, а geometry.h тоже включает point.h (examples/18/errors/). Без защиты структура объявится дважды:
$ gcc -std=c11 -Wall main_guard.c
In file included from geometry.h:2,
from main_guard.c:4:
point.h:2:16: error: redefinition of 'struct Point'
point.h:2:16: note: originally defined here
Защита — include guard: весь заголовок внутри условия препроцессора с уникальным именем. При первом включении имя ещё не определено — текст вставляется и имя определяется; при втором условие ложно, и текст пропускается.
#ifndef POINT_H /* имя — от имени файла, заглавными */
#define POINT_H
typedef struct Point {
double x, y;
} Point;
#endif
Короче — #pragma once первой строкой. Это не стандарт C, но её понимают gcc, clang и MSVC. Защита нужна каждому заголовку, даже если сейчас его включает один файл.
Имя функции или глобальной переменной по умолчанию имеет внешнее связывание: компоновщик видит его во всех файлах программы, и во всей программе оно должно быть определено ровно один раз. Два слова меняют это:
static перед функцией или глобальной переменной — внутреннее связывание: имя видно только в своём файле. Это «приватная» часть модуля: grow в stack.c никто снаружи не вызовет, а в другом модуле может быть своя grow — конфликта не будет. (Внутри функции static значит другое — переменная живёт всё время работы программы, раздел 15.)extern — объявление без определения: «переменная такого типа есть, память под неё выделена в другом файле». extern long stack_reallocs; пишут в заголовке, а определение long stack_reallocs = 0; — ровно в одном .c. У прототипов функций extern подразумевается сам.Частая ошибка — определить переменную прямо в заголовке: int counter = 0; в counter.h. Заголовок включён в main_counter.c и tick.c — переменная определена в обоих, и компоновщик это видит:
$ gcc -std=c11 -Wall main_counter.c tick.c
/usr/bin/ld: /tmp/ccp3lVMT.o:(.bss+0x0): multiple definition of `counter';
/tmp/cc6SQ9Yx.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
Исправление: в counter.h — extern int counter;, в tick.c — int counter = 0;. Но лучше вовсе обходиться без глобальных переменных, разделяемых между модулями: функция tick_count(), возвращающая static-счётчик, даёт тот же результат и не позволяет испортить его снаружи.
Ошибки компоновки приходят от ld, а не от компилятора: каждый файл по отдельности верен, не сходится целое. Их узнают по словам undefined reference или multiple definition и по строке collect2: error: ld returned 1 exit status. Номера строк в них часто нет, а имена файлов бывают временными (/tmp/cc….o).
| Сообщение | Причина | Исправление |
|---|---|---|
undefined reference to `stack_pop' | в сборку не включён stack.c (gcc main.c rpn.c) | добавить файл или его .o в команду, в Makefile |
undefined reference to `square' | функция объявлена в другом файле static — снаружи её нет | убрать static и объявить в заголовке, если функция нужна снаружи; иначе не вызывать |
undefined reference to `sqrt' | математическая библиотека в Linux подключается отдельно | ключ -lm в конце команды |
undefined reference при библиотеке в команде | -lds стоит раньше объектных файлов, которым она нужна | библиотеки — после файлов: gcc main.o rpn.o -L. -lds |
multiple definition of `counter' | переменная или функция определена в заголовке или в двух .c | в заголовке — extern или прототип, определение — в одном .c |
undefined reference to `main' | собирается программа без файла с main (или опечатка mian) | добавить файл с main; модуль без main компилировать с -c |
Отличайте их от ошибок компилятора: implicit declaration of function — забыт #include с прототипом, компилятор не знает функцию (в gcc 14 это ошибка, в старых версиях — лишь предупреждение, и программа собиралась с неверными типами аргументов), undefined reference — прототип был, а тела функции компоновщик не нашёл.
Набирать команды для каждого файла долго, а пересобирать всё после каждой правки — медленно в большой программе. Утилита make читает Makefile — список правил «цель: зависимости» и команд — и выполняет только те команды, цели которых старше своих зависимостей (сравнивает время изменения файлов).
# Калькулятор ОПН из трёх модулей. make — собрать, make lib — через статическую библиотеку, make clean — убрать
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic -g
OBJS = main.o rpn.o stack.o
calc: $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
# Объектный файл зависит от своего .c и от всех заголовков, которые он включает
main.o: main.c rpn.h stack.h
$(CC) $(CFLAGS) -c main.c
rpn.o: rpn.c rpn.h stack.h
$(CC) $(CFLAGS) -c rpn.c
stack.o: stack.c stack.h
$(CC) $(CFLAGS) -c stack.c
clean:
rm -f calc calc-lib *.o libds.a
.PHONY: lib clean
make ответит missing separator.$(CC), $(CFLAGS) — переменные: ключи сборки меняются в одном месте. $@ — имя цели.make без аргумента строит первую цель (calc), make clean — указанную. .PHONY — цели, которые не являются файлами.$ make — после правки ничего не изменилось
make: 'calc' is up to date.
$ touch rpn.c && make — «изменили» rpn.c: один файл и компоновка
gcc -std=c11 -Wall -Wextra -pedantic -g -c rpn.c
gcc -std=c11 -Wall -Wextra -pedantic -g -o calc main.o rpn.o stack.o
$ touch stack.h && make — изменили заголовок: все, кто его включает
gcc -std=c11 -Wall -Wextra -pedantic -g -c main.c
gcc -std=c11 -Wall -Wextra -pedantic -g -c rpn.c
gcc -std=c11 -Wall -Wextra -pedantic -g -c stack.c
gcc -std=c11 -Wall -Wextra -pedantic -g -o calc main.o rpn.o stack.o
Если забыть заголовок в зависимостях, после правки stack.h часть файлов не пересоберётся и будет работать со старым описанием структуры — ошибка, которую трудно найти. В больших проектах зависимости от заголовков gcc пишет сам (ключи -MMD -MP), а сами Makefile генерирует CMake — с ним вы встретитесь в разделе 25 (Qt). В Windows make ставится из MSYS2: pacman -S make.
Когда модулей много и они нужны разным программам, их объектные файлы собирают в статическую библиотеку — архив libИМЯ.a. Компоновщик берёт из неё только те объектные файлы, в которых есть нужные программе символы, и копирует их в программу.
$ ar rcs libds.a stack.o — создать архив (r — добавить/заменить, c — создать, s — оглавление)
$ ar t libds.a — что в архиве
stack.o
$ gcc main.o rpn.o -L. -lds -o calc-lib — -L. — искать библиотеки здесь, -lds — файл libds.a
$ ./calc-lib < in.txt
…
stack reallocs: 5
-lИМЯ ищет файл libИМЯ.a (или .so) в каталогах -L и в системных. -lm — это libm, математическая библиотека.gcc -L. -lds main.o rpn.o — к моменту чтения libds.a ещё никто не просил stack_pop, и сборка падает с undefined reference..so в Linux, .dll в Windows): код не копируется в программу, а подгружается при запуске. Стандартная библиотека C (libc) по умолчанию подключается именно так — поэтому программа из пяти строк весит килобайты, а не мегабайты.extern), определения — в одном .c#ifndef X_H / #define X_H / #endif) или #pragma oncestatic вне функции — имя видно только в своём файле; extern — объявление переменной из другого файла.c компилируется отдельно в .o; компоновщик сводит U одних файлов с T другихundefined reference — тела не нашлось (не тот набор файлов, static, нет -lm, порядок библиотек); multiple definition — определено дваждыmake пересобирает только устаревшееar rcs libX.a *.o, подключение -L. -lX — после объектных файловstatic, прототипы, область видимости; раздел 17 — стек и очередь как готовые модули. Нужен для: раздела 19 — ПЗ №19, модуль «словарь» на хеш-таблице с Makefile; раздела 20 — заголовки C++ и пространства имён вместо префиксов; разделов 25–28 — сборка оконного приложения из многих файлов (CMake в Qt Creator).examples/18/: соберите calc через make, измените заголовок и посмотрите, что пересоберётся; повторите ошибки из errors/ и исправьте их.