Файлы заголовков и файлов C/С++: как они работают?
Это, наверное, глупый вопрос, но я искал довольно долгое время здесь и в Интернете, и я не мог найти четкого ответа (выполнил мою тщательную проверку).
Итак, я новичок в программировании... Мой вопрос в том, как основная функция знает о определениях функций (реализациях) в другом файле?
ех. Скажем, у меня есть 3 файла
- main.cpp
- myfunction.cpp
- myfunction.hpp
//main.cpp
#include "myfunction.hpp"
int main() {
int A = myfunction( 12 );
...
}
-
//myfunction.cpp
#include "myfunction.hpp"
int myfunction( int x ) {
return x * x;
}
-
//myfunction.hpp
int myfunction( int x );
-
Я понимаю, как препроцессор включает код заголовка, но как заголовок и основная функция даже знают определение функции, а тем более используют его?
Извиняюсь, если это не ясно или я очень ошибаюсь в чем-то, но здесь здесь
Ответы
Ответ 1
Заголовочный файл объявляет функции/классы - то есть сообщает компилятору, когда он компилирует файл .cpp
, какие функции/классы доступны.
Файл .cpp
определяет эти функции - то есть компилятор компилирует код и, следовательно, создает фактический машинный код для выполнения тех действий, которые объявлены в соответствующем файле .hpp
.
В вашем примере main.cpp
содержит файл .hpp
. Препроцессор заменяет #include
содержимым файла .hpp
. Этот файл сообщает компилятору, что функция myfunction
определена в другом месте и принимает один параметр (a int
) и возвращает int
.
Поэтому, когда вы компилируете main.cpp
в файл объекта (расширение .o), он делает заметку в этом файле, для которой требуется функция myfunction
. Когда вы компилируете myfunction.cpp
в объектный файл, в объектном файле есть примечание, в котором оно имеет определение для myfunction
.
Затем, когда вы подключаетесь к двум объектным файлам вместе в исполняемый файл, компоновщик связывает концы вверх - т.е. main.o
использует myfunction
, как определено в myfunction.o
.
Я надеюсь, что это поможет
Ответ 2
Вы должны понимать, что компиляция представляет собой двухэтапные операции с точки зрения пользователя.
1-й шаг: компиляция объекта
На этом этапе ваши файлы *.c индивидуально скомпилированы в отдельные файлы объектов. Это означает, что когда main.cpp скомпилирован, он ничего не знает о вашей myfunction.cpp. Единственное, что он знает, это то, что вы заявляете, что функция с этой сигнатурой: int myfunction( int x )
существует в другом объектном файле.
Компилятор сохранит ссылку на этот вызов и включит его непосредственно в объектный файл. Объектный файл будет содержать "Мне нужно вызвать myfunction с int, и он вернется ко мне с int. Он хранит индекс всех extern, чтобы впоследствии иметь возможность связываться с другими.
Второй шаг: привязка
Во время этого шага linker рассмотрит все эти индексы ваших объектных файлов и попытается решить зависимости в пределах этих файлы. Если его там нет, вы получите от него знаменитый undefined symbol XXX
. Затем он переведёт эти ссылки в реальный адрес памяти в файл результатов: либо двоичный, либо библиотечный.
И тогда вы можете начать спрашивать, как это можно сделать с помощью гигантской программы, такой как Office Suite, у которой есть множество методов и объектов? Ну, они используют механизм shared library. Вы знаете их с вашими файлами ".dll" и/или ".so", которые у вас есть на вашей рабочей станции Unix/Windows. Это позволяет отложить решение символа undefined до запуска программы.
Он даже позволяет разрешать символ undefined по запросу, dl *.
Ответ 3
1. Принцип
Когда вы пишете:
int A = myfunction(12);
Это переведено на:
int A = @call(myfunction, 12);
где @call
можно рассматривать как поиск в словаре. И если вы думаете о аналогии с словарем, вы наверняка знаете о слове (smogashboard?), Прежде чем знать его определение. Все, что вам нужно, это то, что во время выполнения определение должно быть в словаре.
2. Точка на ABI
Как работает этот @call? Из-за ABI. ABI - это способ, который описывает многие вещи и среди них, как выполнять вызов данной функции (в зависимости от ее параметров). Контракт вызова прост: он просто говорит, где можно найти каждый из аргументов функции (некоторые из них будут в регистрах процессора, а некоторые - в стеке).
Следовательно, @call фактически делает:
@push 12, reg0
@invoke myfunction
И определение функции знает, что его первый аргумент (x) находится в reg0
.
3. Но хотя словари были для динамических языков?
И вы правы, в какой-то степени. Динамические языки обычно реализуются с хеш-таблицей для поиска по символам, которая динамически заполняется.
Для С++ компилятор преобразует блок перевода (грубо говоря, предварительно обработанный исходный файл) в объект (.o
или .obj
в целом). Каждый объект содержит таблицу символов, которые он ссылается, но для которых определение неизвестно:
.undefined
[0]: myfunction
Затем компоновщик объединяет объекты и согласовывает символы. На данный момент есть два вида символов:
- те, которые находятся в библиотеке, и могут быть указаны через смещение (окончательный адрес до сих пор неизвестен)
- те, которые находятся за пределами библиотеки, и чей адрес полностью неизвестен до выполнения.
Оба могут обрабатываться одинаково.
.dynamic
[0]: myfunction at <undefined-address>
И тогда код будет ссылаться на поисковую запись:
@invoke .dynamic[0]
Когда библиотека загружается (например, DLL_Open
), среда выполнения, наконец, знает, где символ отображается в памяти, и перезаписывает <undefined-address>
реальным адресом (для этого прогона).
Ответ 4
Как указано в комментарии Matthieu M., это компоновщик, чтобы найти нужную "функцию" в нужном месте. Шаги компиляции примерно:
- Компилятор вызывается для каждого файла cpp и переводит его на
объектный файл (двоичный код) с таблицей символов, которая связывает
имя функции (имена искажены в С++) до их местоположения в
объектный файл.
- Линкером вызывается только один раз: каждый объектный файл в
параметр. Он разрешит местоположение вызова функции из одного объекта
файл в другой благодаря таблицам символов. Одна функция main() ДОЛЖНА
существуют где-то. В итоге создается двоичный исполняемый файл
когда компоновщик нашел все, что ему нужно.
Ответ 5
Препроцессор включает содержимое файлов заголовков в файлы cpp (файлы cpp называются единицей перевода).
Когда вы компилируете код, каждый трансляционный блок отдельно проверяется на семантические и синтаксические ошибки. Наличие определений функций в единицах перевода не рассматривается. После компиляции создаются файлы .obj.
На следующем шаге, когда связаны файлы obj. используется определение функций (функций-членов для классов), которые используются, и происходит связь. Если функция не найдена, возникает ошибка компоновщика.
В вашем примере. Если функция не была определена в myfunction.cpp, компиляция будет продолжаться без проблем. Об ошибке будет сообщено на этапе связывания.
Ответ 6
int myfunction(int);
является прототипом функции. Вы объявляете функцию с ней, чтобы компилятор знал, что вы вызываете эту функцию при написании myfunction(0);
.
И , как заголовок и основная функция даже знают, что определение функции существует?
Ну, это работа Linker.
Ответ 7
При компиляции программы препроцессор добавляет исходный код каждого файла заголовка в файл, который его включал. Компилятор компилирует файл КАЖДЫЙ .cpp
. Результатом является количество файлов .obj
.
После этого появляется компоновщик. Linker принимает все .obj
файлы, начиная с вашего основного файла. Всякий раз, когда он находит ссылку, которая не имеет определения (например, переменная, функция или класс), она пытается найти соответствующее определение в других файлах .obj
, созданных на этапе компиляции или поставляемый компоновщику в начале стадии связывания.
Теперь, чтобы ответить на ваш вопрос: каждый .cpp
файл компилируется в файл .obj
, содержащий инструкции в машинный код. Когда вы включаете файл .hpp
и используете некоторую функцию, определенную в другом файле .cpp
, на этапе компоновки компоновщик ищет это определение функции в соответствующем файле .obj
. Это как оно находит.