Я не думаю, что вам действительно нужны какие-либо книги для этого. Насколько я понимаю ваш вопрос, вы просто хотите знать, что каждый тип файла для , и как они связаны с процессом компиляции.
Если вы хотите узнать все подробно, или если вы пишете свой собственный компилятор C ++, вам, очевидно, нужно обратиться к книгам.
Но вот версия высокого уровня:
Во-первых, давайте проигнорируем компоновщики. Не каждый язык использует выделенный компоновщик, и даже в стандартах языка C и C ++ даже не упоминаются ссылки. Компоновщик - это деталь реализации, которая обычно используется для объединения всех частей, но технически вовсе не обязана существовать.
Кроме того, это очень специфично для C / C ++. Процесс компиляции отличается для каждого языка, и, в частности, в C / C ++ используется грязный, устаревший и неэффективный механизм, которого избегает большинство современных языков.
Сначала вы пишете код. Этот код сохраняется в нескольких файлах (обычно с расширением .c, .cc или .cpp) и нескольких заголовках (.h, .hh или .hpp). Эти расширения не требуются, хотя. Это просто общее соглашение, но технически вы можете назвать ваши файлы как угодно.
Для примера, давайте предположим, что у нас есть следующие файлы:
foo.h:
void foo();
foo.cpp:
#include "foo.h"
#include "bar.h"
void foo() {
bar();
}
bar.h:
void bar();
bar.cpp:
#include "bar.h"
void bar() {
}
Компилятор берет один .cpp файл и обрабатывает его. Допустим, мы сначала скомпилируем foo.cpp.
Первое, что он делает, - это предварительная обработка: расширение всех макросов, обработка директив #include путем копирования / вставки содержимого включенного файла в место, из которого он # include'd. Когда это будет сделано, у вас будет модуль перевода или модуль компиляции, и он будет выглядеть так:
void foo(); //#include "foo.h"
void bar(); //#include "bar.h"
void foo() {
bar();
}
По сути, все, что произошло в нашем простом примере, состоит в том, что заголовки копируются / вставляются.
Теперь компилятор максимально компилирует это в машинный код. Конечно, учитывая, что он может видеть только этот один кодовый файл, он столкнется с вызовами функций для функций, которые он не может увидеть в определении.
Как он должен реализовать вызов bar()
в нашем случае? Не может, потому что не видит, что bar
делает . Все, что он может видеть (потому что он включал bar.h
, это то, что функция bar
существует , и что она не принимает аргументов и возвращает void. Таким образом, компилятор в основном генерирует небольшую метку «заполните позже», по сути говоря, «перейти к адресу этой функции, как только мы узнаем, что это за адрес».
Теперь мы собрали foo.cpp
.
Результатом этого процесса является объектный файл, обычно с расширением .o или .obj.
Компилятор теперь также вызывается на bar.cpp
, и происходит почти то же самое. Заголовки включаются, а затем код компилируется в машинный код, хотя на этот раз у нас не должно быть проблем с отсутствующими определениями.
Итак, теперь у нас остались foo.o
и bar.o
, содержащие скомпилированный код для каждого из двух модулей компиляции.
Теперь мы находимся в забавной ничейной стране, где стандарт языка C ++ говорит нам, что должна делать программа, но больше ничего не говорит о том, как туда добраться, но на самом деле программа пока этого не делает. , У нас нет программы. Чтобы исправить это, мы вызываем компоновщик.
Мы передаем все наши объектные файлы, и они читают их и, по сути, заполняют пробелы. При чтении foo.o
он заметит, что есть звонок на bar()
, где адрес bar()
был неизвестен. Но компоновщик имеет доступ к функции bar.o``as well, so it is able to look up the definition of
bar () , and determine its address, which it can paste into the call site inside the
foo () `. Это в основном связывает воедино эти автономные объектные файлы. После того, как все эти проблемы были решены, он объединяет весь код в один двоичный файл (с расширением .exe в Windows), который является вашей программой. Фактический код генерируется компилятором, и компоновщик затем подключается и связывает определения из одного файла со ссылками на него в других файлах.