C — компилируемый язык: ничего не запустится, пока компилятор не переведёт ваш исходник в машинный код для конкретного процессора и операционной системы. Этот шаг — одна команда, но понимание того, что она делает, превращает большинство сообщений об ошибках из загадочных в очевидные.
Две команды
Сохраните это как hello.c:
#include <stdio.h>
int main(void) {
printf("Привет, C!\n");
return 0;
}
Затем в той же папке:
gcc hello.c -o hello
./hello
Первая команда компилирует. Вторая запускает результат. В Windows команда запуска — hello в Command Prompt или .\hello в PowerShell: префикса ./ там нет, потому что Windows по умолчанию ищет в текущем каталоге, а Unix-оболочки намеренно этого не делают.
Если опустить -o hello, gcc назовёт результат a.out (или a.exe) — поэтому старые руководства заканчиваются на ./a.out. Всегда задавайте имя выходного файла: это стоит четырёх символов и избавляет от путаницы.
Вот та же программа в браузерном редакторе, который делает оба шага за вас:
Что на самом деле делает «компиляция»
gcc hello.c -o hello выглядит как один шаг. На деле их четыре, и каждый может провалиться со своим особым видом ошибки.
1. Препроцессирование. Прежде чем будет скомпилирован хоть один символ C, препроцессор обрабатывает каждую строку, начинающуюся с #. #include <stdio.h> буквально заменяется содержимым этого заголовочного файла; макросы #define раскрываются; блоки #ifdef сохраняются или удаляются. На выходе — один большой файл C, в котором не осталось строк с #. Его можно посмотреть:
gcc -E hello.c
Это выведет сотни строк — почти всё это вставленный stdio.h.
2. Компиляция. Препроцессированный C разбирается, проверяется по типам и переводится в ассемблер для вашего процессора. Отсюда берутся синтаксические ошибки, ошибки типов и предупреждения.
3. Ассемблирование. Ассемблер превращается в объектный файл — машинный код с заглушками там, где находятся вызовы функций из других файлов.
gcc -c hello.c # создаёт hello.o и останавливается перед компоновкой
4. Компоновка. Объектные файлы сшиваются вместе со стандартной библиотекой C, каждая заглушка заполняется настоящим адресом, и получается исполняемый файл. Отсюда берутся ошибки «undefined reference».
Практический вывод: ошибка компиляции указывает на строку в вашем исходнике, а ошибка компоновки — нет, потому что компоновка происходит после того, как все строки уже приняты.
Флаги, которые стоит использовать с первого дня
Голая команда gcc file.c -o prog молча принимает немало опасного кода. Четыре флага это меняют.
gcc -std=c17 -Wall -Wextra -g hello.c -o hello
-Wallвключает распространённые предупреждения. Вопреки названию, это не «все» предупреждения, а разумный набор.-Wextraдобавляет ещё, в том числе про неиспользуемые параметры и некоторые ошибки в сравнениях.-std=c17фиксирует стандарт языка, чтобы ваш код означал одно и то же на любой машине. Используйте-std=c99, если следуете более старым материалам.-gсохраняет отладочную информацию, чтобыgdbилиlldbмогли показать реальные строки вашего исходника, когда что-то падает.
Ещё два, которые понадобятся со временем:
-O2включает оптимизацию для релизных сборок. Во время учёбы держите его выключенным: оптимизированный код труднее отлаживать, а предупреждения иногда смещаются.-fsanitize=address,undefined(gcc и clang) заставляет программу аварийно завершиться с внятным сообщением в тот самый момент, когда она читает за границами или попадает в неопределённое поведение. Это самый полезный учебный флаг в C.
Проверьте предупреждения сами. Эта программа компилируется и запускается, но в ней два настоящих бага:
Переменной count никогда не присваивается значение, поэтому программа печатает те байты, что случайно там оказались. С -Wall компилятор так и говорит: 'count' is used uninitialized. Предупреждения в C почти никогда не бывают шумом — относитесь к ним как к ошибкам, до которых вы ещё не дошли.
Компиляция нескольких файлов
Настоящие программы разбиты на файлы. Передайте их gcc все сразу:
gcc -std=c17 -Wall main.c utils.c -o myprog
Или скомпилируйте каждый по отдельности и скомпонуйте в конце — именно так поступают системы сборки, чтобы изменение одного файла не пересобирало всё:
gcc -c main.c # -> main.o
gcc -c utils.c # -> utils.o
gcc main.o utils.o -o myprog
Некоторым библиотекам нужен явный флаг компоновки. Математическая библиотека — та, с которой сталкивается каждый новичок:
gcc calc.c -o calc -lm
Без -lm использование sqrt из math.h прекрасно компилируется, а затем падает на этапе компоновки с undefined reference to sqrt: объявление было в заголовочном файле, но код лежал в библиотеке, которую никто не попросил.
Читаем ошибки
Ошибки C лаконичны, но последовательны. Три примера покрывают большую часть того, что вы увидите на первых порах.
Пропущенная точка с запятой сообщается на следующей строке, потому что компилятор продолжил чтение:
hello.c:5:5: error: expected ';' before 'return'
Исправлять нужно строку 4, а не 5. Всякий раз, когда ошибка указывает на строку, которая выглядит нормально, проверьте строку выше.
Отсутствующий заголовочный файл выглядит как загадка про функцию, имя которой вы явно написали правильно:
hello.c:4:5: warning: implicit declaration of function 'printf'
Это значит, что компилятор не видел объявления printf, то есть вы забыли #include <stdio.h>. В C99 и позже это ошибка, а не просто предупреждение.
Сбой компоновки вообще не содержит номера строки:
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x1a): undefined reference to `helper'
Компилятор поверил вам, что helper где-то существует; компоновщик поискал — и не нашёл. Либо вы так и не написали определение, либо написали имя иначе, либо забыли передать gcc файл, в котором оно лежит.
Всегда исправляйте первую ошибку. Ошибки C идут лавиной: одна плохая строка может породить двадцать сообщений, и девятнадцать из них исчезнут, как только будет исправлена первая.
Коды возврата
return 0 из main — не украшение. Это код завершения программы, и оболочка может его прочитать:
После запуска echo $? на macOS/Linux (или echo %errorlevel% в Windows) выведет это число. Скрипты и инструменты сборки по нему решают, продолжать ли. Соглашение абсолютно: 0 — успех, ненулевое значение — код ошибки на ваш выбор. В stdlib.h определены EXIT_SUCCESS и EXIT_FAILURE, если вам приятнее имена.
Начиная с C99 достижение конца main без return трактуется как return 0, но писать его явно понятнее, а для всех остальных функций это обязательно.
Часто задаваемые вопросы
Как скомпилировать и запустить программу на C?
Сохраните код как hello.c, затем выполните gcc hello.c -o hello, чтобы собрать программу, и ./hello, чтобы её запустить (в Windows просто hello). Если gcc не найден, сначала нужно установить компилятор.
Что делает gcc -o?
-o задаёт имя выходного файла. gcc hello.c -o hello создаёт исполняемый файл с именем hello. Без -o gcc пишет в a.out (или a.exe в Windows) — именно поэтому во множестве старых руководств запускают ./a.out.
Какие флаги gcc стоит использовать всегда?
gcc -std=c17 -Wall -Wextra -g yourfile.c -o yourprog. -Wall -Wextra включают предупреждения, которые ловят настоящие баги, -std=c17 фиксирует версию языка, а -g сохраняет отладочные символы, чтобы отладчик мог показать ваш исходник. Добавьте -O2, когда нужна скорость в релизной сборке.
Что означает «undefined reference to» в C?
Это ошибка компоновщика: компилятор принял вызов функции, но определения этой функции нигде не нашлось. Обычные причины — опечатка в имени, забытая компиляция второго файла .c или использование математической функции без подключения математической библиотеки (-lm).