Почему в C ++ есть заголовочные файлы и файлы .cpp?
Почему в C ++ есть заголовочные файлы и файлы .cpp?
Ответы:
Ну, основной причиной было бы отделение интерфейса от реализации. Заголовок объявляет, «что» будет делать класс (или что-либо еще реализуемое), в то время как файл cpp определяет, «как» он будет выполнять эти функции.
Это уменьшает зависимости, так что код, который использует заголовок, не обязательно должен знать все детали реализации и любые другие классы / заголовки, необходимые только для этого. Это сократит время компиляции, а также количество необходимой перекомпиляции, когда что-то в реализации изменится.
Это не идеально, и вы обычно прибегаете к методам, таким как Pimpl Idiom, чтобы правильно разделить интерфейс и реализацию, но это хорошее начало.
Компиляция в C ++ выполняется в 2 основных этапа:
Первый - это компиляция «исходных» текстовых файлов в двоичные «объектные» файлы: файл CPP является скомпилированным файлом и компилируется без каких-либо знаний о других файлах CPP (или даже библиотеках), если только он не передан в него через необработанное объявление или заголовок включения. Файл CPP обычно компилируется в .OBJ или .O «объектный» файл.
Второй - это объединение всех «объектных» файлов и, следовательно, создание окончательного двоичного файла (библиотеки или исполняемого файла).
Где ГЭС вписывается во весь этот процесс?
Компиляция каждого файла 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 имеет 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_
или даже проще
#pragma once
// The declarations in the B.hpp file
You still have to copy paste the signature from header file to cpp file, don't you?
Нет необходимости. Пока CPP «включает» HPP, прекомпилятор будет автоматически копировать и вставлять содержимое файла HPP в файл CPP. Я обновил ответ, чтобы уточнить это.
While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself
. Нет, это не так. Он знает только типы, предоставленные пользователем, которые половину времени даже не потрудятся прочитать возвращаемое значение. Затем происходят неявные преобразования. И потом, когда у вас есть код:, foo(bar)
вы даже не можете быть уверены, что foo
это функция. Таким образом, компилятор должен иметь доступ к информации в заголовках, чтобы решить, правильно ли компилируется источник, или нет ... Затем, когда код скомпилирован, компоновщик просто свяжет вместе вызовы функций.
Seems, they're just a pretty ugly arbitrary design.
: Если C ++ был создан в 2012 году, действительно. Но помните, что C ++ был построен на C в 1980-х годах, и в то время ограничения в то время были совсем другими (IIRC было решено в целях принятия сохранить те же линкеры, что и C).
foo(bar)
это функция - если она получена как указатель? На самом деле, говоря о плохом дизайне, я виню C, а не C ++. Мне действительно не нравятся некоторые ограничения чистого C, такие как наличие заголовочных файлов или функции, возвращающие одно и только одно значение, в то же время принимая несколько аргументов на входе (разве не естественно, что ввод и вывод ведут себя одинаково зачем многократные аргументы, а один вывод?) :)
Why can't I be sure, that foo(bar) is a function
foo может быть типом, поэтому у вас должен быть вызван конструктор класса. In fact, speaking of bad design, I blame C, not C++
Я могу винить С во многих вещах, но дизайн в 70-х не будет одним из них. Опять же, ограничения того времени ... such as having header files or having functions return one and only one value
: Кортежи могут помочь смягчить это, а также передать аргументы по ссылке. Теперь, каков будет синтаксис для получения возвращенных множественных значений, и стоит ли менять язык?
Потому что C, где возникла эта концепция, 30 лет, и тогда это был единственный жизнеспособный способ связать код из нескольких файлов.
Сегодня это ужасный хак, который полностью уничтожает время компиляции в 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, с заголовками мы бы избежали такого рода головной боли ...