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


484

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


3
Похожий вопрос: stackoverflow.com/questions/1945846/…
Spoike

это общая парадигма ООП, .h - объявление класса, а cpp - определение. Одному не нужно знать, как оно реализовано, он / она должен знать только интерфейс.
Маниш

Это лучшая часть c ++, отделяющая интерфейс от реализации. Это всегда хорошо, вместо того, чтобы хранить весь код в одном файле, у нас есть отдельный интерфейс. Некоторое количество кода всегда там, как встроенная функция, которая является частью заголовочных файлов. Хорошо выглядит, когда виден заголовочный файл, отображающий список объявленных функций и переменных класса.
Мьянк

Иногда заголовочные файлы необходимы для компиляции, а не просто предпочтения организации или способ распространения предварительно скомпилированных библиотек. Допустим, у вас есть структура, в которой game.c зависит от ОБЕО Physics.c и math.c; физика.с также зависит от математики. Если вы включите файлы .c и забудете о файлах .h навсегда, у вас будут дубликаты объявлений из math.c и нет надежды на компиляцию. Это то, что наиболее важно для меня, почему заголовочные файлы важны. Надеюсь, это поможет кому-то еще.
Сэми Бенчериф

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

Ответы:


202

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

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

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


178
Не совсем правда. Заголовок по-прежнему содержит основную часть реализации. С каких это пор частные переменные экземпляра являются частью интерфейса класса? Частный член функции? Тогда что, черт возьми, они делают в публично видимом заголовке? И это разваливается дальше с шаблонами.
jalf

13
Вот почему я сказал, что он не идеален, и для большего разделения нужна идиома Pimpl. Шаблоны - это совершенно иная червь - даже если в большинстве компиляторов ключевое слово "export" полностью поддерживается, оно все равно будет синтаксическим сахаром, а не реальным разделением.
Йорис Тиммерманс

4
Как другие языки справляются с этим? например - Java? В Java нет концепции заголовочных файлов.
Лазер

8
@Lazer: разбирать Java проще. Компилятор Java может анализировать файл, не зная всех классов в других файлах, и проверять типы позже. В C ++ многие конструкции неоднозначны без информации о типе, поэтому компилятору C ++ требуется информация о ссылочных типах для анализа файла. Вот почему для этого нужны заголовки.
Ники

15
@nikie: При чем тут «простота» разбора? Если бы в Java была грамматика, по крайней мере, такая же сложная, как в C ++, она все равно могла бы просто использовать файлы Java. В любом случае, как насчет C? C легко разбирается, но использует как заголовки, так и файлы c.
Томас Эдинг

609

Компиляция 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_

или даже проще

#pragma once

// The declarations in the B.hpp file

2
@nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?Нет необходимости. Пока CPP «включает» HPP, прекомпилятор будет автоматически копировать и вставлять содержимое файла HPP в файл CPP. Я обновил ответ, чтобы уточнить это.
paercebal

7
@Bob: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Нет, это не так. Он знает только типы, предоставленные пользователем, которые половину времени даже не потрудятся прочитать возвращаемое значение. Затем происходят неявные преобразования. И потом, когда у вас есть код:, foo(bar)вы даже не можете быть уверены, что fooэто функция. Таким образом, компилятор должен иметь доступ к информации в заголовках, чтобы решить, правильно ли компилируется источник, или нет ... Затем, когда код скомпилирован, компоновщик просто свяжет вместе вызовы функций.
paercebal

3
@Bob: [продолжение] ... Теперь, я думаю, компоновщик может выполнить работу, выполняемую компилятором, что сделает ваш выбор возможным. (Я думаю, что это предмет предложения «модулей» для следующего стандарта). Seems, they're just a pretty ugly arbitrary design.: Если C ++ был создан в 2012 году, действительно. Но помните, что C ++ был построен на C в 1980-х годах, и в то время ограничения в то время были совсем другими (IIRC было решено в целях принятия сохранить те же линкеры, что и C).
paercebal

1
@paercebal Спасибо за объяснения и заметки, paercebal! Почему я не могу быть уверен, что foo(bar)это функция - если она получена как указатель? На самом деле, говоря о плохом дизайне, я виню C, а не C ++. Мне действительно не нравятся некоторые ограничения чистого C, такие как наличие заголовочных файлов или функции, возвращающие одно и только одно значение, в то же время принимая несколько аргументов на входе (разве не естественно, что ввод и вывод ведут себя одинаково зачем многократные аргументы, а один вывод?) :)
Борис Бурков

1
@Bobo:: Why can't I be sure, that foo(bar) is a functionfoo может быть типом, поэтому у вас должен быть вызван конструктор класса. 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: Кортежи могут помочь смягчить это, а также передать аргументы по ссылке. Теперь, каков будет синтаксис для получения возвращенных множественных значений, и стоит ли менять язык?
paercebal

93

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

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


3
Интересно, почему заголовочные файлы (или что-то еще действительно было нужно для компиляции / компоновки) не были просто "автоматически сгенерированы"?
Матин Улхак,

54

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

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


16

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


4
Почему наследование C ++ от C неудачно?
Lokesh

3
@Lokesh Из-за багажа :(
力 力

1
Как это может быть ответом?
Шуво Саркер

14
@ShuvoSarker, потому что, как продемонстрировали тысячи языков, для C ++ нет технических объяснений, заставляющих программистов дважды писать сигнатуры функций. Ответ на вопрос «почему?» это "история".
Борис

15

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

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

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


Не сработает, циклические ссылки. То есть вы не можете собрать a.lib из a.cpp перед сборкой b.lib из b.cpp, но вы также не можете собрать b.lib до a.lib.
MSalters

20
Ява решила это, Python может сделать это, любой современный язык может это сделать. Но в то время, когда был изобретен С, оперативная память была настолько дорогой и дефицитной, что это просто не выход.
Аарон Дигулла

6

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


4

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

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

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

3
Почему поставщики библиотек не могут просто отправить сгенерированный файл заголовка? Файл «заголовка» без предварительной обработки должен дать гораздо лучшую производительность (если реализация не была действительно нарушена).
Том Хотин - tackline

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

-5

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

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

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

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.