Препроцессор, заголовочные файлы и make
Препроцессор
Перед компиляцией текст программы обрабатывает препроцессор. Он понимает строки, начинающиеся с #, и работает с текстом, ничего не зная о C.
#include
#include <sys/mman.h> /* системный заголовок: mmap, PROT_*, MAP_* */
#include "mem.h" /* свой заголовок: ищется рядом с файлом */
Директива просто вставляет содержимое файла. В заголовках лежат объявления: прототипы функций, типы, макросы. Тела функций — в .c-файлах или библиотеках.
«Include files has been skipped»
В задаче 8 заголовки опущены. Чтобы программа скомпилировалась, нужны: <stdio.h> (printf), <stdlib.h> (exit), <unistd.h> (fork, getpid, getppid, _exit), <sys/mman.h> (mmap), <sys/wait.h> (wait), <signal.h> (kill, SIGINT), <sys/types.h> (pid_t).
#define: константы и макросы
#define NUM_PROCS 5
#define BUF_SIZE (4 * 1024)
#define SQR(x) ((x) * (x))
Макрос — текстовая замена. Отсюда ловушки:
| Макрос | Вызов | Что получится |
|---|---|---|
#define SQR(x) x * x |
SQR(2 + 3) |
2 + 3 * 2 + 3 = 11 |
#define SQR(x) ((x) * (x)) |
SQR(2 + 3) |
((2 + 3) * (2 + 3)) = 25 ✓ |
#define SQR(x) ((x) * (x)) |
SQR(i++) |
((i++) * (i++)) — двойной инкремент ✗ |
Многие «константы» системных вызовов — макросы: SIGINT, PROT_READ, MAP_SHARED, MAP_FAILED.
Условная компиляция
#ifdef DEBUG
fprintf(stderr, "counter = %u\n", mem->counter);
#endif
#if defined(__linux__)
/* код для Linux */
#else
/* для других систем */
#endif
gcc -DDEBUG prog.c # определить макрос DEBUG из командной строки
Защита от повторного включения
/* mem.h */
#ifndef MEM_H
#define MEM_H
typedef struct { int state; unsigned counter; pid_t pid, ppid; } Mem;
Mem *mem_create(void);
#endif
При втором #include "mem.h" макрос MEM_H уже определён, и содержимое пропускается. Многие компиляторы понимают и короткую форму #pragma once.
Раздельная компиляция
Большую программу делят на файлы:
gcc -c main.c # → main.o
gcc -c mem.c # → mem.o
gcc main.o mem.o -o prog
Изменился один файл — перекомпилируем только его и заново компонуем.
Make
make автоматизирует сборку: по файлу Makefile он понимает зависимости и пересобирает только то, что устарело (сравнивая время изменения файлов).
CC = gcc
CFLAGS = -Wall -Wextra -g
prog: main.o mem.o
$(CC) main.o mem.o -o prog
main.o: main.c mem.h
$(CC) $(CFLAGS) -c main.c
mem.o: mem.c mem.h
$(CC) $(CFLAGS) -c mem.c
clean:
rm -f *.o prog
.PHONY: clean
Формат правила: цель: зависимости, на следующей строке — табуляция и команда.
Где это встречается
Ядро Linux, coreutils и почти любой исходный пакет собираются через make. При сборке пакета из исходников (./configure && make && make install) работает именно этот механизм.
Итог
- Препроцессор подставляет файлы и макросы до компиляции; макросы — просто текст, ставьте скобки.
- Заголовки содержат объявления и защищаются
#ifndef/#define/#endif. - Раздельная компиляция:
.c→.o→ компоновка. makeпересобирает только изменившееся по правилам изMakefile.
Практика решено 0 из 5
Что напечатает программа?
#define SQR(x) x * x
printf("%d\n", SQR(2 + 3));
Объявите макрос NUM_PROCS со значением 5 (как в задаче 8) и макрос MAX(a, b), возвращающий большее из двух,
с правильной расстановкой скобок.
#define NUM_PROCS 5
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Защитите заголовочный файл mem.h от повторного включения с помощью макроса MEM_H
(#ifndef / #define / #endif).
#ifndef MEM_H
#define MEM_H
typedef struct { int state; } Mem;
#endif
Чем отличаются #include <stdio.h> и #include "mem.h"?
Make пересобирает программу из файлов main.c и mem.c. Изменился только mem.c. Расставьте действия make по порядку:
-
Перекомпоновать:
gcc main.o mem.o -o prog -
Сравнить время изменения
mem.cиmem.o— исходник новее -
Увидеть, что
main.oновееmain.c— его не трогать -
Перекомпилировать:
gcc -c mem.c -o mem.o
Войдите, чтобы отмечать пройденные уроки.