Компиляция и компоновка
Вспомните переводчика и переплётчика из первого урока. Толстую книгу переводят по главам, и переводчик каждой главы видит только её. Если в первой главе написано «см. главу 3», переводчик верит ссылке на слово. Найдётся ли третья глава, проверит переплётчик, когда будет сшивать книгу целиком.
Программа на C++ часто состоит из нескольких .cpp-файлов, как книга из глав. Компилятор переводит каждый файл отдельно и, читая один, не видит, что написано в соседнем. Но вызов функции компилируется, только если компилятор уже знает, что такая функция есть. Вот программа из двух файлов: main.cpp вызывает функцию triple, а её тело лежит в triple.cpp.
Файл main.cpp:
#include <iostream>
int triple(int x);
int main() {
std::cout << triple(14) << '\n'; // 42
}Файл triple.cpp:
int triple(int x) {
return x * 3;
}Компиляцию обоих файлов и их компоновку в одну программу вместе называют сборкой.
Объявление и определение
Строка int triple(int x); в main.cpp — это объявление, та самая ссылка «см. главу 3». Оно повторяет первую строку функции:
- тип результата;
- имя;
- то, что функция принимает, в круглых скобках.
Вместо тела стоит точка с запятой. Объявления компилятору хватает, чтобы проверить вызов triple(14): функция принимает int и возвращает int.
В triple.cpp записано определение — та же первая строка вместе с телом в фигурных скобках. Объявить функцию можно в каждом файле, который её вызывает, а определить — только в одном.
Как работает вызов
Давайте разберём, что происходит при вызове triple(14):
xвint triple(int x)— это параметр, переменная функции. При вызове в неё попадает значение из скобок, здесь14.return x * 3;вычисляет14 * 3и завершает функцию.- Результат
42возвращается на место вызова, иstd::coutпечатает его.
Если параметров несколько, их перечисляют через запятую: int add(int a, int b). Операторы +, - и * работают с целыми числами как в арифметике.
Кто сообщает об ошибке
Компилятор находит ошибки в тексте одного файла — например, пропущенную точку с запятой, как в программе с Hello, world. По объявлению он заметит и вызов triple(14, 1) с лишним числом. Объектный файл в таком случае не появляется.
Компоновщик видит программу целиком и ищет определение для каждой вызванной функции. Если triple.cpp не попал в сборку, main.cpp скомпилируется без ошибок, а компоновщик остановится — как переплётчик, у которого нет третьей главы:
undefined reference to `triple(int)'Так пишет GNU ld — стандартный компоновщик в Linux. В задачах курса то же сообщение выглядит иначе: undefined symbol: triple(int).
Соберите программу ниже, потом уберите triple.cpp из сборки и следите, какой объектный файл определяет нужную main.o функцию triple(int) и что пишет компоновщик:
1#include <iostream>2 3int triple(int x);4 5int main() {6 std::cout << triple(14) << '\n';7}
1int triple(int x) {2 return x * 3;3}
Компилятор переведёт каждый .cpp-файл отдельно, а компоновщик соберёт объектные файлы в программу. Нажмите кнопку сборки.
Сообщение компилятора указывает файл и строку, например main.cpp:6, а компоновщик называет функцию, у которой нет определения.
Частая ошибка
Имя в определении должно в точности совпадать с объявленным. Здесь в triple.cpp оно начинается с заглавной буквы:
int Triple(int x) { // ✗
return x * 3;
}Оба файла компилируются без ошибок, но компоновщик снова не находит triple(int). C++ различает заглавные и строчные буквы, поэтому Triple — другая функция, а у объявленной triple определения по-прежнему нет.
Главное
- Компилятор переводит каждый
.cpp-файл отдельно, и для вызова ему хватает объявления. - Компоновщик ищет определение каждой вызванной функции во всей программе.
- Нет определения — ошибка компоновщика
undefined reference, хотя каждый файл скомпилировался. - Параметр получает значение из вызова, а
returnотдаёт результат на место вызова.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.