Это, вероятно, более подробный ответ, чем вы хотели, но я думаю, что достойное объяснение оправдано.
В C и C ++ один исходный файл определяется как одна единица перевода . По соглашению заголовочные файлы содержат объявления функций, определения типов и определения классов. Реальные реализации функций находятся в единицах перевода, то есть файлах .cpp.
Идея заключается в том, что функции и функции-члены класса / структуры компилируются и собираются один раз, тогда другие функции могут вызывать этот код из одного места, не создавая дубликатов. Ваши функции объявлены как "внешние" неявно.
/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);
/* function body, or function definition. */
int add(int a, int b)
{
return a + b;
}
Если вы хотите, чтобы функция была локальной для единицы перевода, вы определяете ее как «статическую». Что это значит? Это означает, что если вы включите исходные файлы с внешними функциями, вы получите ошибки переопределения, потому что компилятор встречает одну и ту же реализацию более одного раза. Итак, вы хотите, чтобы все ваши блоки перевода видели объявление функции, но не тело функции .
Так как же все это в конце концов смешается? Это работа линкера. Компоновщик читает все объектные файлы, которые генерируются на этапе ассемблера, и разрешает символы. Как я уже говорил ранее, символ - это просто имя. Например, имя переменной или функции. Когда блоки перевода, которые вызывают функции или объявляют типы, не знают реализацию этих функций или типов, эти символы называются неразрешенными. Компоновщик разрешает неразрешенный символ, соединяя модуль перевода, который содержит неопределенный символ, с тем, который содержит реализацию. Уф. Это верно для всех видимых извне символов, независимо от того, реализованы они в вашем коде или предоставлены дополнительной библиотекой. Библиотека - это просто архив с многоразовым кодом.
Есть два заметных исключения. Во-первых, если у вас есть небольшая функция, вы можете сделать ее встроенной. Это означает, что сгенерированный машинный код не генерирует вызов функции extern, а буквально объединяется на месте. Поскольку они обычно небольшие, размер накладных расходов не имеет значения. Вы можете представить их статичными в том, как они работают. Так что безопасно реализовывать встроенные функции в заголовках. Реализации функций внутри определения класса или структуры также часто автоматически вставляются компилятором.
Другое исключение - шаблоны. Поскольку при создании экземпляра компилятору необходимо видеть все определение типа шаблона, невозможно отделить реализацию от определения, как в случае автономных функций или обычных классов. Что ж, возможно, это возможно сейчас, но получение широкой поддержки компилятором для ключевого слова "export" заняло много времени. Таким образом, без поддержки «экспорта» единицы перевода получают свои собственные локальные копии экземпляров шаблонизированных типов и функций, аналогично тому, как работают встроенные функции. С поддержкой «экспорта» это не так.
За этими двумя исключениями, некоторые люди считают, что «лучше» помещать реализации встроенных функций, шаблонных функций и шаблонных типов в файлы .cpp, а затем #include файл .cpp. Является ли это заголовком или исходным файлом, на самом деле не имеет значения; препроцессор не заботится и является просто соглашением.
Краткое описание всего процесса от кода C ++ (несколько файлов) до конечного исполняемого файла:
- Препроцессор запускается, который анализирует все директивы , которая начинается с «#». Например, директива #include объединяет включенный файл с подчиненным. Он также выполняет макро-замену и вставку токена.
- Фактический компилятор запускается в промежуточном текстовом файле после этапа препроцессора и испускает код ассемблера.
- В ассемблере работает на файл сборки и высылает машинный код, обычно это называется объектный файл и следует двоичный исполняемый формат оперативной системы в вопросе. Например, Windows использует PE (переносимый исполняемый формат), в то время как Linux использует формат Unix System V ELF с расширениями GNU. На этом этапе символы все еще помечены как неопределенные.
- Наконец, компоновщик запускается. Все предыдущие этапы выполнялись на каждом блоке перевода по порядку. Однако этап компоновщика работает со всеми сгенерированными объектными файлами, которые были сгенерированы ассемблером. Компоновщик разрешает символы и выполняет много волшебства, например, создает разделы и сегменты, что зависит от целевой платформы и двоичного формата. Программисты не обязаны знать это в целом, но это, безусловно, помогает в некоторых случаях.
Опять же, это было определенно больше, чем вы просили, но я надеюсь, что мелкие детали помогут вам увидеть более широкую картину.