Заголовок C / C ++ и файлы реализации: как они работают? - PullRequest
39 голосов
/ 10 февраля 2012

Это , вероятно, глупый вопрос, но я уже довольно долго искал здесь и в Интернете и не мог придумать четкого ответа (сделал мой поиск в Google из-за должной осмотрительности).

Итак, я новичок в программировании ... Мой вопрос: откуда основная функция знает об определениях функций (реализациях) в другом файле?

ех. Скажем, у меня есть 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 );

-

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

Я прошу прощения, если это не ясно, или я сильно ошибаюсь, что-то новое

Ответы [ 7 ]

61 голосов
/ 10 февраля 2012

Заголовочный файл объявляет функции / классы - т.е. сообщает компилятору при компиляции файла .cpp, какие функции / классы доступны.

Файл .cpp определяет этифункции - т.е. компилятор компилирует код и, следовательно, создает фактический машинный код для выполнения тех действий, которые объявлены в соответствующем файле .hpp.

В вашем примере main.cpp включает файл .hpp.Препроцессор заменяет #include содержимым файла .hpp.Этот файл сообщает компилятору, что функция myfunction определена в другом месте, и она принимает один параметр (int) и возвращает int.

Таким образом, когда вы компилируете main.cpp в объектный файл (.o extension) в этом файле записывается, что для него требуется функция myfunction.Когда вы компилируете myfunction.cpp в объектный файл, в объектном файле есть примечание, что у него есть определение для myfunction.

Затем, когда вы пришли к соединению двух объектных файлов вместе в исполняемый файл,компоновщик связывает концы - т.е. main.o использует myfunction, как определено в myfunction.o.

Надеюсь, это поможет

15 голосов
/ 10 февраля 2012

Вы должны понимать, что компиляция - это двухэтапная операция, с точки зрения пользователя.


1-й шаг: компиляция объекта

Во время этого шага ваша *.c файлы индивидуально скомпилированы в отдельные объектные файлы.Это означает, что когда main.cpp скомпилирован, он ничего не знает о вашем myfunction.cpp .Единственное, что он знает, это то, что вы объявляете , что функция с такой сигнатурой: int myfunction( int x ) существует в другом объектном файле.

Компилятор сохранит ссылку на этот вызов и включит еепрямо в объектном файле.Объектный файл будет содержать «Я должен вызвать myfunction с int , и он вернется ко мне с int . Он хранит индекс всех extern звонки, чтобы потом иметь возможность связаться с другими.


2-й шаг: связывание

Во время этого шага компоновщик приметПосмотрите на все эти индексы ваших объектных файлов и постарайтесь решить зависимости в этих файлах. Если его там нет, вы получите из него знаменитый undefined symbol XXX. Затем он переведет эти ссылки в реальный адрес памяти вфайл результатов: либо бинарный файл, либо библиотека.


И затем вы можете начать спрашивать, как это можно сделать с помощью гигантской программы, такой как Office Suite, в которой есть множество методов и объектов?Ну, они используют механизм совместно используемой библиотеки . Вы знаете их по файлам '.dll' и / или '.so', которые есть на рабочей станции Unix / Windows. Это позволяет отложить решение неопределенного символа допрограммам бежит.

Он даже позволяет решать неопределенный символ по требованию , с функциями dl *.

5 голосов
/ 10 февраля 2012

1. Принцип

Когда вы пишете:

int A = myfunction(12);

Это переведено на:

int A = @call(myfunction, 12);

, где @call можно рассматривать как поиск по словарю. И если вы подумаете об аналогии со словарем, вы наверняка сможете узнать о слове ( смог ?), Прежде чем узнаете его определение. Все, что вам нужно, это чтобы во время выполнения определение находилось в словаре.

2. Точка на ABI

Как это @ call работает? Из-за ABI. ABI - это способ, который описывает многие вещи, и среди них, как выполнить вызов данной функции (в зависимости от ее параметров). Контракт на вызов прост: он просто говорит, где можно найти каждый из аргументов функции (некоторые будут в регистрах процессора, другие - в стеке).

Следовательно, @call на самом деле:

@push 12, reg0
@invoke myfunction

И определение функции знает, что ее первый аргумент ( x ) находится в reg0.

3. Но я хоть словари были для динамических языков?

И вы правы, в некоторой степени. Динамические языки обычно реализуются с помощью хеш-таблицы для поиска символов, которая заполняется динамически.

Для C ++ компилятор преобразует модуль перевода (грубо говоря, предварительно обработанный исходный файл) в объект (.o или .obj в целом). Каждый объект содержит таблицу символов, на которые он ссылается, но определение которых неизвестно:

.undefined
[0]: myfunction

Тогда компоновщик соберет объекты и примирит символы. На данный момент существует два вида символов:

  • те, которые находятся в библиотеке, и на которые можно ссылаться через смещение (конечный адрес все еще неизвестен)
  • те, которые находятся за пределами библиотеки и чей адрес полностью неизвестен до времени выполнения.

Оба могут рассматриваться одинаково.

.dynamic
[0]: myfunction at <undefined-address>

И тогда код будет ссылаться на запись поиска:

@invoke .dynamic[0]

Когда библиотека загружена (например, DLL_Open), среда выполнения наконец узнает , где символ отображается в памяти, и перезаписывает <undefined-address> реальным адресом (для этого прогона) .

4 голосов
/ 10 февраля 2012

Препроцессор включает содержимое заголовочных файлов в файлы cpp (файлы cpp называются единицами перевода). При компиляции кода каждая трансляционная единица отдельно проверяется на наличие семантических и синтаксических ошибок. Наличие определений функций в разных единицах перевода не рассматривается. Файлы .obj генерируются после компиляции.

На следующем шаге, когда файлы obj связаны. определение используемых функций (функций-членов для классов) ищется и происходит связывание. Если функция не найдена, генерируется ошибка компоновщика.

В вашем примере, если функция не была определена в myfunction.cpp, компиляция продолжалась бы без проблем. Ошибка будет сообщена на этапе связывания.

4 голосов
/ 10 февраля 2012

Как указано в комментарии Матье М., это компоновщик , чтобы найти нужную «функцию» в нужном месте.Шаги компиляции примерно:

  1. Компилятор вызывается для каждого файла cpp и переводит его в объектный файл (двоичный код) с таблицей символов , которая связывает имя функции (именав c ++) их местоположение в объектном файле.
  2. Компоновщик вызывается только один раз: с каждым объектным файлом в параметре.Это разрешит местоположение вызова функции из одного объектного файла в другой благодаря таблицам символов .Одна функция main () ДОЛЖНА существовать где-то.В конечном итоге двоичный исполняемый файл создается, когда компоновщик нашел все, что ему нужно.
2 голосов
/ 10 февраля 2012

int myfunction(int); - прототип функции.Вы объявляете функцию вместе с ней, чтобы компилятор знал, что вы вызываете эту функцию, когда пишете myfunction(0);.

И как заголовок и основная функция даже знают, что определение функции существует?1007 *
Ну, это работа Linker .

1 голос
/ 10 февраля 2012

Когда вы компилируете программу, препроцессор добавляет исходный код каждого заголовочного файла в файл, который его включал. Компилятор компилирует файл EVERY .cpp. Результатом является количество .obj файлов.
После этого идет компоновщик. Линкер принимает все файлы .obj, начиная с основного файла. Всякий раз, когда он находит ссылку, не имеющую определения (например, переменную, функцию или класс), он пытается найти соответствующее определение в других файлах .obj, созданных на этапе компиляции, или передается компоновщику в начале этапа компоновки.
Теперь, чтобы ответить на ваш вопрос: каждый файл .cpp компилируется в файл .obj, содержащий инструкции в машинном коде. Когда вы включаете файл .hpp и используете какую-то функцию, определенную в другом файле .cpp, на этапе компоновки компоновщик ищет это определение функции в соответствующем файле .obj. Вот как это находит.

...