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

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

9 ответов

Решение

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

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

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

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

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

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

  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, который использует тот же символ, вы затем копируете / вставляете объявление...

КОПИЯ / ВСТАВИТЬ ПРЕДУПРЕЖДЕНИЕ!

Да, есть проблема. Копирование / вставка опасны и сложны в обслуживании. Что означает, что было бы здорово, если бы у нас был какой-то способ НЕ копировать / вставлять, и все же объявить символ... Как мы можем это сделать? Включением некоторого текстового файла, к которому обычно добавляются суффиксы.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 объявление функции, и 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++, вызывает бесчисленные ненужные зависимости (потому что определения классов в заголовочном файле предоставляют слишком много информации о реализации) и так далее.

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

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

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

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

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

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

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

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

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

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

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

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

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

Так что, если у нас есть что-то вроде

class A {..};
class B : public A {...};

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

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

Другие вопросы по тегам