Зачем нужны файлы заголовков и .cpp-файлы?

Почему у C ++ есть файлы заголовков и .cpp-файлы?

Ну, основная причина заключалась бы в том, чтобы отделить интерфейс от реализации. Заголовок объявляет «что» class (или что-то, что выполняется) будет делать, а файл cpp определяет «как» он будет выполнять эти функции.

Это уменьшает зависимости, поэтому код, который использует заголовок, не обязательно должен знать все детали реализации и любые другие classы / заголовки, необходимые только для этого. Это сократит время компиляции, а также объем перекомпиляции, необходимый для изменения ситуации.

Это не идеально, и вы обычно прибегаете к таким методам, как Идиома Pimpl, чтобы правильно разделить интерфейс и реализацию, но это хороший старт.

Компиляция C ++

Компиляция в C ++ выполняется в 2 основных этапа:

  1. Во-первых, это компиляция «исходных» текстовых файлов в двоичные «объектные» файлы: CPP-файл является скомпилированным файлом и скомпилирован без каких-либо знаний о других файлах CPP (или даже в библиотеках), если только их не доставлять через raw-объявление или заголовок. Файл CPP обычно скомпилируется в файл .OBJ или .O “object”.

  2. Во-вторых, это связывание всех «объектных» файлов и, следовательно, создание финального двоичного файла (либо библиотеки, либо исполняемого файла).

Где ГЭС подходит во всем этом процессе?

Бедный одиночный файл CPP …

Компиляция каждого файла CPP не зависит от всех других файлов CPP, а это означает, что если A.CPP требуется символ, определенный в B.CPP, например:

// A.CPP void doSomething() { doSomethingElse(); // Defined in B.CPP } // B.CPP void doSomethingElse() { // Etc. } 

Он не будет компилироваться, потому что A.CPP не имеет никакого способа узнать, что «doSomethingElse» существует … Если в A.CPP нет объявления, например:

 // A.CPP void doSomethingElse() ; // From B.CPP void doSomething() { doSomethingElse() ; // Defined in B.CPP } 

Затем, если у вас есть C.CPP, который использует тот же символ, вы затем копируете / вставляете декларацию …

COPY / PASTE ALERT!

Да, есть проблема. Копирование / пасты опасны и трудно поддерживать. Это означает, что было бы здорово, если бы у нас был способ НЕ копировать / вставлять и все еще объявлять символ … Как мы можем это сделать? К включению некоторого текстового файла, который обычно помещается как .h, .hxx, .h ++ или, мой предпочтительный для файлов C ++, .hpp:

 // B.HPP (here, we decided to declare every symbol defined in B.CPP) void doSomethingElse() ; // A.CPP #include "B.HPP" void doSomething() { doSomethingElse() ; // Defined in B.CPP } // B.CPP #include "B.HPP" void doSomethingElse() { // Etc. } // C.CPP #include "B.HPP" void doSomethingAgain() { doSomethingElse() ; // Defined in B.CPP } 

Как include работу?

Включение файла будет, по сути, анализировать, а затем копировать-вставлять его содержимое в файл CPP.

Например, в следующем коде с заголовком A.HPP:

 // A.HPP void someFunction(); void someOtherFunction(); 

… источник B.CPP:

 // B.CPP #include "A.HPP" void doSomething() { // Etc. } 

… станет после включения:

 // B.CPP void someFunction(); void someOtherFunction(); void doSomething() { // Etc. } 

Одна маленькая вещь – зачем включать B.HPP в B.CPP?

В текущем случае это не требуется, а B.HPP имеет doSomethingElse функции doSomethingElse , а B.CPP имеет определение функции doSomethingElse (которое само по себе является объявлением). Но в более общем случае, когда B.HPP используется для объявлений (и встроенного кода), не может быть никакого соответствующего определения (например, перечислений, простых структур и т. Д.), Поэтому включение может потребоваться, если B.CPP использует это заявление от B.HPP. В целом, это «хороший вкус» для источника, который по умолчанию включает заголовок.

Вывод

Таким образом, файл заголовка необходим, поскольку компилятор C ++ не может самостоятельно искать объявления символов, и поэтому вы должны помочь ему, включив эти объявления.

Последнее слово: вы должны ставить защитники заголовков вокруг содержимого ваших файлов HPP, чтобы убедиться, что несколько включений ничего не сломают, но в целом я считаю, что основная причина существования файлов HPP объясняется выше.

 #ifndef B_HPP_ #define B_HPP_ // The declarations in the B.hpp file #endif // B_HPP_ 

Потому что C, откуда возникла концепция, 30 лет, и тогда это был единственный жизнеспособный способ связать код из нескольких файлов.

Сегодня это ужасный хак, который полностью уничтожает время компиляции на C ++, вызывает бесчисленные ненужные зависимости (потому что определения classов в файле заголовка содержат слишком много информации об этой реализации) и т. Д.

Поскольку в C ++ конечный исполняемый код не несет никакой информации о символах, это более или менее чистый машинный код.

Таким образом, вам нужен способ описания интерфейса fragmentа кода, который отделен от самого кода. Это описание находится в файле заголовка.

Поскольку люди, которые разработали библиотечный формат, не хотели «тратить» пространство на редко используемую информацию, такую ​​как macros препроцессора C и декларации функций.

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

Большинство языков после C / C ++ хранят эту информацию в выводе (например, байт-код Java), или они вообще не используют предварительно скомпилированный формат, всегда распространяются в исходной форме и компилируются «на лету» (Python, Perl).

Потому что C ++ унаследовал их от C. К сожалению.

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

Часто вам нужно иметь определение интерфейса без отправки всего кода. Например, если у вас есть разделяемая библиотека, вы отправляете с ней заголовочный файл, который определяет все функции и символы, используемые в общей библиотеке. Без файлов заголовков вам необходимо отправить исходный код.

В рамках одного проекта используются файлы заголовков, IMHO, по крайней мере для двух целей:

  • Ясность, то есть, сохраняя интерфейсы отдельно от реализации, легче читать код
  • Время компиляции. Используя только интерфейс, где это возможно, вместо полной реализации время компиляции может быть уменьшено, потому что компилятор может просто сделать ссылку на интерфейс вместо того, чтобы анализировать фактический код (что, идеально, было бы нужно сделать только один раз).

Отвечая на ответ MadKeithV ,

Это уменьшает зависимости, поэтому код, который использует заголовок, не обязательно должен знать все детали реализации и любые другие classы / заголовки, необходимые только для этого. Это сократит время компиляции, а также объем перекомпиляции, необходимый для изменения ситуации.

Другая причина заключается в том, что заголовок дает уникальный идентификатор каждому classу.

Поэтому, если у нас есть что-то вроде

 class A {..}; class B : public A {...}; class C { include A.cpp; include B.cpp; ..... }; 

У нас будут ошибки, когда мы попытаемся построить проект, поскольку A является частью B, с заголовками мы бы избегали этой головной боли …

Давайте будем гением компьютера.