Когда я объявляю функцию в заголовочном файле и помещаю определение той функции в некотором другом файле, как компилятор/компоновщик находит определение? Это систематически ищет каждый файл в своем пути для него или является там более изящным решением? Это прослушивало меня в течение прошедших нескольких дней, и я не мог найти объяснение его.
Компилятор не делает этого, это делает компоновщик.
В то время как компилятор работает с одним исходным файлом за раз, при вызове компоновщика ему передаются имена всех объектных файлов, созданных компилятором, а также любых библиотек, которые пользователь хочет подключить. Таким образом, компоновщик имеет полное представление о наборе файлов, которые потенциально могут содержать определение, и ему нужно только заглянуть в таблицы символов этих объектных файлов. Ему не нужно делать никакого поиска сверх этого.
Например, допустим, у вас есть foo.h и foo.c, определяющие и реализующие функцию foo()
, и bar.h и bar.c, определяющие и реализующие bar()
. Скажем, bar
вызывает foo
, так что bar.c включает foo.h. В этой компиляции есть три шага:
gcc -c foo.c
gcc -c bar.c
gcc foo.o bar.o -o program
Первая строка компилирует foo.c, создавая foo.o. Вторая компилирует bar.c, создавая bar.o. В этот момент в объектном файле bar.o foo
является внешним символом. Третья строка вызывает компоновщик, который соединяет foo.o и bar.o в исполняемый файл под названием "program". Когда компоновщик обрабатывает bar.o, он видит неразрешенный внешний символ foo
, поэтому он просматривает таблицу символов всех других компонуемых объектных файлов (в данном случае только foo.o) и находит foo
в foo.o, и завершает компоновку.
С библиотеками это немного сложнее, и порядок их появления в командной строке может иметь значение в зависимости от вашего компоновщика, но в целом принцип тот же.
Предположим, у вас есть файл foo.cpp с #include foo.h и, возможно, другими включениями. Заголовки, конечно, могут иметь свои собственные # include-ы.
Препроцессор начнет с foo.cpp, проанализирует #includes и прочитает содержимое заголовка. Результатом будет текст из заголовочных файлов и "сплющенный" foo.cpp. Затем компилятор обработает этот текст. Если переменная / функция / и т. Д. Должны иметь был объявлен где-то в заголовке, не найден, компилятор сообщит об ошибке.
Суть в том, что компилятор должен видеть все свои объявления как результат .cpp и заголовков.
Когда вы компилируете файл .cpp, компилятор выводит две таблицы в файл .obj: список символов, которые, как он ожидает, будут определены извне, а также список символов, определенных в данном конкретном модуле.
Компоновщик берет все файлы .obj, которые были выведены компилятором, и затем (как следует из названия) соединяет их вместе. Поэтому для каждого модуля он просматривает список символов, которые помечены как "определенные извне", и просматривает все другие модули, которые ему были предоставлены для этих символов.
Таким образом, он "ищет" только в тех модулях, в которых вы сказали ему искать.
Если он не может найти символ ни в одном из других модулей, то возникает ошибка "неопределенная ссылка".