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





Это препроцессорный способ объявления интерфейсов. Вы помещаете интерфейс (объявления методов) в файл заголовка, а реализацию - в файл cpp. Приложениям, использующим вашу библиотеку, необходимо знать только интерфейс, к которому они могут получить доступ через #include.
Поскольку в C++ окончательный исполняемый код не несет никакой символьной информации, это более или менее чистый машинный код.
Таким образом, вам нужен способ описания интерфейса фрагмента кода, который отделен от самого кода. Это описание находится в заголовочном файле.
Ну, основная причина была бы в том, чтобы отделить интерфейс от реализации. Заголовок объявляет, «что» будет делать класс (или что-то еще, что реализуется), в то время как файл cpp определяет, «как» он будет выполнять эти функции.
Это уменьшает зависимости, так что код, использующий заголовок, не обязательно должен знать все детали реализации и любых других классов / заголовков, необходимых только для этого. Это сократит время компиляции, а также количество перекомпиляций, необходимых при изменении чего-либо в реализации.
Это не идеально, и вы обычно прибегаете к таким методам, как Pimpl Idiom, чтобы должным образом разделить интерфейс и реализацию, но это хорошее начало.
Не совсем так. Заголовок по-прежнему содержит основную часть реализации. С каких это пор переменные частного экземпляра стали частью интерфейса класса? Частные функции-члены? Тогда что, черт возьми, они делают в общедоступном заголовке? А с шаблонами он еще больше разваливается.
Вот почему я сказал, что это не идеально, и идиома Pimpl нужна для большего разделения. Шаблоны - это совершенно другая баня червей - даже если бы ключевое слово "exports" полностью поддерживалось в большинстве компиляторов, это все равно было бы синтаксическим сахаром, а не реальным разделением.
Как с этим справляются другие языки? например - Ява? В Java нет концепции файла заголовка.
В Java более сильно поддерживается концепция «интерфейсов». На мой взгляд, эти интерфейсы являются дальнейшим развитием первоначальной идеи «заголовочного» файла, содержащего Только «что».
@Lazer: Java проще разбирать. Компилятор Java может анализировать файл, не зная всех классов в других файлах, и проверять типы позже. В C++ многие конструкции неоднозначны без информации о типе, поэтому компилятору C++ требуется информация о ссылочных типах для анализа файла. Вот почему ему нужны заголовки.
@nikie: При чем тут "легкость" парсинга? Если бы у Java была грамматика, которая была бы хотя бы такой же сложной, как C++, она все равно могла бы просто использовать java-файлы. В любом случае, что насчет C? C легко разбирается, но использует как заголовки, так и файлы c.
@Lazer: Java не только проще в синтаксическом анализе, но и не имеет шаблонов (да, у него есть дженерики, но их даже нет в работе).
Нет ли возможности C++ поддерживать объединенную реализацию с интерфейсом? Подавляющее большинство кода, использующего C++, такого как доказательство концепции, тесты, простые проекты, не получают выгоды от разделения и только усложняют управление вашим кодом из-за перебоев между двумя файлами.
Можем ли мы также добиться абстракции, отделяя файлы заголовков от файлов реализации? означает, что мы хотим предоставить наш код третьей стороне, тогда мы также можем отправлять только файлы заголовков и скрывать нашу бизнес-логику.
@jalf они по-прежнему являются частью интерфейса, потому что класс определяет конкретную структуру модели памяти размером определенный. И виртуальное наследование там, где это имеет значение. C++ получил модель памяти на основе хранилища, а не на основе типов, как виртуальная машина Java. Java не выполняется ни на каком реальном оборудовании, поэтому она стала более абстрактной.
Потому что люди, разработавшие формат библиотеки, не хотели «тратить» место на редко используемую информацию, такую как макросы препроцессора C и объявления функций.
Поскольку вам нужна эта информация, чтобы сообщить вашему компилятору, что «эта функция будет доступна позже, когда компоновщик будет выполнять свою работу», им пришлось придумать второй файл, в котором эта общая информация могла бы храниться.
Большинство языков после C / C++ хранят эту информацию в выходных данных (например, байт-код Java) или вообще не используют предварительно скомпилированный формат, всегда распространяются в исходной форме и компилируются на лету (Python, Perl).
Не получится, циклические ссылки. То есть вы не можете построить a.lib из a.cpp до сборки b.lib из b.cpp, но вы также не можете построить b.lib перед a.lib.
Java решила это, Python может это сделать, любой современный язык может это сделать. Но в то время, когда был изобретен C, ОЗУ была настолько дорогой и дефицитной, что это просто не было вариантом.
Часто вам может потребоваться определение интерфейса без необходимости отправлять весь код. Например, если у вас есть общая библиотека, вы должны отправить с ней файл заголовка, который определяет все функции и символы, используемые в общей библиотеке. Без файлов заголовков вам нужно будет отправить исходный код.
В рамках одного проекта файлы заголовков используются, IMHO, как минимум для двух целей:
Почему поставщики библиотек не могли просто отправить сгенерированный «заголовочный» файл? "Заголовочный" файл без препроцессора должен давать гораздо лучшую производительность (если только реализация не была действительно нарушена).
Я думаю, это не имеет значения, если файл заголовка сгенерирован или написан вручную, вопрос был не в том, «почему люди сами пишут файлы заголовков?», А в том, «почему у нас есть файлы заголовков». То же самое касается заголовков, свободных от препроцессора. Конечно, это было бы быстрее.
Компиляция на 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
Вам все равно нужно скопировать и вставить подпись из файла заголовка в файл cpp, не так ли?
@nimcap: You still have to copy paste the signature from header file to cpp file, don't you?: Нет необходимости. Пока CPP "включает" HPP, прекомпилятор автоматически копирует и вставляет содержимое файла HPP в файл CPP. Я обновил ответ, чтобы прояснить это.
Спасибо, ваша идея копирования / вставки оказалась полезной. Но ваша точка зрения «Он не будет компилироваться, потому что A.cpp не знает, что« doSomethingElse »существует», мне кажется неправильным. При компиляции A.cpp компилятор знает типы аргументов и возвращаемое значение doSomethingElse из самого вызова; он может предполагать, что doSomethingElse определен в другом модуле и полагаться на компоновщик для заполнения зависимости (или возвращать ошибку, если он не может найти ее определение или типы аргументов / возвращаемого значения несовместимы в A.cpp и B.cpp). Я до сих пор не понимаю необходимости заголовков. Кажется, это просто довольно уродливый произвольный дизайн.
@ Боб: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Нет, это не так. Он знает только типы, предоставленные пользователем, который в половине случаев даже не потрудится прочитать возвращаемое значение. Затем происходят неявные преобразования. А когда у вас есть код: foo(bar), вы даже не можете быть уверены, что foo - это функция. Таким образом, компилятор должен иметь доступ к информации в заголовках, чтобы решить, правильно ли компилируется исходный код или нет ... Затем, как только код будет скомпилирован, компоновщик просто свяжет вместе вызовы функций.
@Bob: [продолжение] ... Теперь, я думаю, компоновщик мог бы выполнять работу, проделанную компилятором, что тогда сделало бы ваш вариант возможным. (Я предполагаю, что это предмет предложения о «модулях» для следующего стандарта). Seems, they're just a pretty ugly arbitrary design.: Если бы C++ действительно был создан в 2012 году. Но помните, что C++ был построен на C в 1980-х годах, и в то время ограничения были совершенно другими (IIRC, в целях принятия было решено сохранить те же компоновщики, что и C).
@paercebal Спасибо за объяснения и примечания, paercebal! Почему я не могу быть уверен, что foo(bar) - это функция, если она получена как указатель? На самом деле, говоря о плохом дизайне, я виню C, а не C++. Мне действительно не нравятся некоторые ограничения чистого C, такие как наличие файлов заголовков или наличие функций, возвращающих одно и только одно значение, при одновременном приеме нескольких аргументов на входе (не кажется ли естественным, что ввод и вывод ведут себя аналогичным образом ; почему несколько аргументов, а один выход?) :)
@Bobo: Why can't I be sure, that foo(bar) is a function: foo может быть типом, поэтому у вас должен быть вызван конструктор класса. In fact, speaking of bad design, I blame C, not C++: Я могу винить C во многих вещах, но то, что он был разработан в 70-х, не будет одним из них. Опять же, ограничения того времени ... such as having header files or having functions return one and only one value: Кортежи могут помочь смягчить это, а также передача аргументов по ссылке. Теперь, каков будет синтаксис для получения нескольких возвращаемых значений и стоит ли менять язык?
Ответ должен был быть выбран в качестве ответа на этот пост. Оригинал тоже хорош, но этот ответ охватывает в основном все.
Почему нельзя было просто включить B.CPP в A.CPP?
Ссылка @Lazer wayback machine на "Организация файлов кода на C и C++" web.archive.org/web/20100124195524/http://www.gamedev.net/… / текущая ссылка: gamedev.net/articles/programming/…
Поскольку C, где зародилась эта концепция, существует 30 лет, и тогда это был единственный жизнеспособный способ связать код из нескольких файлов.
Сегодня это ужасный взлом, который полностью уничтожает время компиляции в C++, вызывает бесчисленные ненужные зависимости (потому что определения классов в файле заголовка предоставляют слишком много информации о реализации) и так далее.
Интересно, почему файлы заголовков (или все, что действительно было необходимо для компиляции / компоновки) не были просто «автоматически сгенерированы»?
он предшествовал K&R C. Почти все языки до этого использовали те же парадигмы, исключением были бы языки, подобные Паскалю, у которых был специальный модуль компиляции, называемый «unit», который был и заголовком, и реализацией в одном и основном, называемом «программа». Все дело в том, чтобы разделить программу на части кода, которыми может управлять компилятор, и сократить время компиляции \ разрешить инкрементную компиляцию.
Потому что C++ унаследовал их от C. К сожалению.
Почему наследование C++ от C неудачно?
@Lokesh Из-за своего багажа :(
Как это может быть ответом?
@ShuvoSarker, потому что, как показали тысячи языков, нет технического объяснения того, как C++ заставляет программистов писать сигнатуры функций дважды. Ответ на вопрос "почему?" это «история».
@Boris забавно, что C на самом деле не требовал писать их дважды. ANd C изначально вообще не нуждался в прототипах, потому что он работал на платформах, допускающих такую реализацию. У них даже не было регистров стека, «стек» был просто областью памяти, управляемой созданным кодом. Это C++, и современные платформы перешли на регистровый или смешанный способ вызова функций, поэтому требуется отдельный прототип, если мы скрываем реализацию и можем ли мы перегрузить. Довольно много классических (Фортран, Паскаль) и современных языков тоже. Отсутствие такового обычно является подписью переводчика.
Отвечая на Ответ MadKeithV,
This reduces dependencies so that code that uses the header doesn't necessarily need to know all the details of the implementation and any other classes/headers needed only for that. This will reduce compilation times, and also the amount of recompilation needed when something in the implementation changes.
Другая причина в том, что заголовок дает каждому классу уникальный идентификатор.
Итак, если у нас есть что-то вроде
class A {..};
class B : public A {...};
class C {
include A.cpp;
include B.cpp;
.....
};
У нас будут ошибки, когда мы попытаемся построить проект, поскольку A является частью B, с заголовками мы бы избежали такой головной боли ...
Это конкретно называется абстракцией, я прав?
Связанный вопрос: stackoverflow.com/questions/1945846/…