Подтвердить что ты не робот

Файлы заголовков и файлов 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 );

-

Я понимаю, как препроцессор включает код заголовка, но как заголовок и основная функция даже знают определение функции, а тем более используют его?

Извиняюсь, если это не ясно или я очень ошибаюсь в чем-то, но здесь здесь

4b9b3361

Ответ 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. Это как оно находит.