Зачем C ++ нужен отдельный заголовочный файл?


138

Я никогда не понимал, почему C ++ нуждается в отдельном заголовочном файле с теми же функциями, что и в файле .cpp. Это делает создание классов и их рефакторинг очень сложным, а также добавляет ненужные файлы в проект. И тогда возникает проблема с необходимостью включения заголовочных файлов, но с явной проверкой, если они уже были включены.

C ++ был ратифицирован в 1998 году, так почему же он разработан таким образом? Какие преимущества имеет наличие отдельного заголовочного файла?


Контрольный вопрос:

Как компилятор находит файл .cpp с кодом в нем, когда я включаю только файл .h? Предполагается ли, что файл .cpp имеет то же имя, что и файл .h, или он просматривает все файлы в дереве каталогов?


2
Если вы хотите редактировать только один файл, оформите заказ lzz (www.lazycplusplus.com).
Ричард Корден

3
Точная копия: stackoverflow.com/questions/333889 . Почти в двух экземплярах: stackoverflow.com/questions/752793
Питер Мортенсен

Ответы:


105

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

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

Причины, которые вы могли бы хотеть отделить:

  1. Чтобы улучшить время сборки.
  2. Для ссылки на код, не имея источника для определений.
  3. Чтобы избежать маркировки всего "встроенного".

Если ваш более общий вопрос «почему C ++ не идентичен Java?», То я должен спросить: «Почему вы пишете C ++ вместо Java?» ;-п

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

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

Преобразование моих комментариев, чтобы ответить на ваш дополнительный вопрос:

Как компилятор находит файл .cpp с кодом в нем

Это не так, по крайней мере, во время компиляции кода, который использовал файл заголовка. Функции, с которыми вы ссылаетесь, даже не нужно писать, не говоря уже о том, что компилятор знает, в каком .cppфайле они будут находиться. Все, что нужно знать вызывающему коду во время компиляции, выражается в объявлении функции. Во время ссылки вы предоставите список .oфайлов, или статических, или динамических библиотек, а действующий заголовок обещает, что определения функций где-то там будут.


3
Чтобы добавить к «Причины, которые вы, возможно, захотите отделить, являются:» и я считаю, что наиболее важная функция заголовочных файлов: отделить проектирование структуры кода от реализации, потому что: A. Когда вы попадаете в действительно сложные структуры, которые включают много объектов, это Гораздо проще просеивать заголовочные файлы и помнить, как они работают вместе, дополненные вашими комментариями к заголовкам. Б. Один человек не позаботился о том, чтобы определить всю структуру объекта, а кто-то другой позаботился о реализации, чтобы все было организовано. В целом, я думаю, что это делает сложный код более читабельным.
Андрес Канелла

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

Я предполагал, что это будет включено в (2), «ссылка на код без определения». Хорошо, так что у вас все еще может быть доступ к git-репо с определениями в, но дело в том, что вы можете скомпилировать свой код отдельно от реализаций его зависимостей и затем ссылаться. Если буквально все, что вам нужно, это разделить интерфейс / реализацию на разные файлы, не заботясь о сборке по отдельности, то вы можете сделать это, просто включив в foo_interface.h файл foo_implementation.h.
Стив Джессоп

4
@ AndresCanella Нет, это не так. Это делает чтение и ведение не-вашего-собственного кода кошмаром. Чтобы полностью понять, что что-то делает в коде, вам нужно перейти через 2n файлов вместо n файлов. Это просто не обозначение Big-Oh, 2n имеет большое значение по сравнению с просто n.
Блажей Михалик

1
Во-вторых, это ложь, что заголовки помогают. например, проверьте исходный код Minix, так трудно понять, где он начинается, где передается управление, где вещи объявляются / определяются ... если бы он был построен с помощью отдельных динамических модулей, он был бы удобочитаемым, если понять смысл одной вещи, а затем перейти к модуль зависимости. вместо этого вам нужно следовать заголовкам, и это делает чтение любого кода, написанного таким образом, чертовски. в отличие от этого, nodejs проясняет, откуда что происходит, без каких-либо ifdef, и вы можете легко определить, откуда это пришло.
Дмитрий

91

C ++ делает это таким образом, потому что C сделал это таким образом, поэтому реальный вопрос в том, почему C сделал это таким образом? Википедия немного говорит об этом.

Более новые скомпилированные языки (такие как Java, C #) не используют предварительные объявления; идентификаторы распознаются автоматически из исходных файлов и считываются непосредственно из символов динамической библиотеки. Это означает, что заголовочные файлы не нужны.


13
+1 Ударяет ноготь по голове. Это действительно не требует подробного объяснения.
MSalters

6
Это не ударило меня по голове :( Мне все еще нужно выяснить, почему C ++ должен использовать предварительные объявления и почему он не может распознавать идентификаторы из исходных файлов и читать непосредственно из символов динамической библиотеки, и почему C ++ сделал это так, просто потому, что С сделал так: р
Александр Тейлор

3
И вы лучший программист, сделав это @AlexanderTaylor :)
Дональд Берд

66

Некоторые люди считают заголовочные файлы преимуществом:

  • Утверждается, что он включает / обеспечивает / разрешает / разделяет интерфейс и реализацию - но обычно это не так. Заголовочные файлы полны деталей реализации (например, переменные-члены класса должны быть указаны в заголовке, даже если они не являются частью общедоступного интерфейса), а функции могут быть и часто определяются внутри строки в объявлении класса в шапке опять уничтожаем это разделение.
  • Иногда говорят, что это улучшает время компиляции, потому что каждая единица перевода может обрабатываться независимо. И все же C ++, вероятно, самый медленный из существующих языков, когда дело доходит до компиляции. Часть причины - много многократных включений того же самого заголовка. Большое количество заголовков включено несколькими модулями перевода, что требует их многократного анализа.

В конечном счете, система заголовков является артефактом 70-х годов, когда был разработан С. Тогда у компьютеров было очень мало памяти, и хранить весь модуль в памяти было просто невозможно. Компилятор должен был начать чтение файла сверху, а затем продолжить линейно через исходный код. Механизм заголовка позволяет это. Компилятору не нужно учитывать другие единицы перевода, он просто должен читать код сверху вниз.

И C ++ сохранил эту систему для обратной совместимости.

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

Однако одно из предложений для C ++ 0x заключалось в том, чтобы добавить правильную систему модулей, позволяющую компилировать код, подобный .NET или Java, в более крупные модули, все за один раз и без заголовков. Это предложение не привело к сокращению C ++ 0x, но я считаю, что оно все еще находится в категории «мы бы хотели сделать это позже». Возможно в TR2 или аналогичном.


Это лучший ответ на странице. Спасибо!
Чак Ле Бат

29

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

Например, следующее должно дать ошибку компилятора:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Ошибка должна заключаться в том, что «SomeOtherFunction не объявлена», потому что вы вызываете ее до ее объявления. Один из способов исправить это - переместить SomeOtherFunction над SomeFunction. Другой подход - сначала объявить сигнатуру функции:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Это позволяет компилятору узнать: где-то в коде есть функция SomeOtherFunction, которая возвращает void и не принимает никаких параметров. Поэтому, если вам нужен код, который пытается вызвать SomeOtherFunction, не паникуйте, а вместо этого отправляйтесь на его поиск.

Теперь представьте, что у вас есть SomeFunction и SomeOtherFunction в двух разных файлах .c. Затем вы должны #include "SomeOther.c" в Some.c. Теперь добавьте некоторые «частные» функции в SomeOther.c. Поскольку C не знает частных функций, эта функция будет доступна и в Some.c.

Вот где приходят файлы .h: они определяют все функции (и переменные), которые вы хотите «экспортировать» из файла .c, к которому можно получить доступ в других файлах .c. Таким образом, вы получаете что-то вроде публичного / частного объема. Кроме того, вы можете передать этот файл .h другим людям без необходимости делиться своим исходным кодом - файлы .h также работают с скомпилированными файлами .lib.

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

Это был С, хотя. C ++ представил классы и частные / публичные модификаторы, поэтому, хотя вы все еще можете спросить, нужны ли они, C ++ AFAIK все еще требует объявления функций перед их использованием. Кроме того, многие разработчики C ++ являются или были разработчиками C и переняли свои концепции и привычки на C ++ - зачем менять то, что не нарушено?


5
Почему компилятор не может выполнить код и найти все определения функций? Кажется, что-то, что было бы довольно легко запрограммировать в компилятор.
Мариус

3
Если у вас есть источник, которого у вас часто нет. Скомпилированный C ++ - это по сути машинный код с достаточным количеством дополнительной информации для загрузки и связывания кода. Затем вы указываете CPU на точку входа и запускаете его. Это принципиально отличается от Java или C #, где код компилируется в промежуточный байт-код, содержащий метаданные своего содержимого.
DevSolar

Я предполагаю, что в 1972 году это могло быть довольно дорогой операцией для компилятора.
Майкл Стум

Именно то, что сказал Майкл Стум. Когда это поведение было определено, единственное, что могло быть практически осуществлено, - это линейное сканирование через единицу перевода.
Джалф

3
Да - компилирование на 16 биттеров с массой ленточных торей нетривиально.
MSalters

11

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

Второе преимущество: это позволяет совместно использовать интерфейсы без разделения кода между разными подразделениями (разными разработчиками, командами, компаниями и т. Д.)


1
Вы намекаете на то, что, например, в C # «вам придется включать исходные файлы в другие исходные файлы»? Потому что, очевидно, нет. Что касается второго преимущества, я думаю, что это слишком зависит от языка: вы не будете использовать файлы .h, например, в Delphi
Vlagged

В любом случае вам придется перекомпилировать весь проект, так что же, на самом деле, учитывается первое преимущество?
Мариус

хорошо, но я не думаю, что это особенность языка. Более практично иметь дело с декларацией C перед определением «проблемы». Это как кто-то известный, говорящий «это не ошибка, это особенность» :)
neuro

@Marius: Да, это действительно важно. Связывание всего проекта отличается от компиляции и связывания всего проекта. По мере того, как количество файлов в проекте увеличивается, их компиляция становится действительно раздражающей. @Vlagged: Вы правы, но я не сравнивал c ++ с другим языком. Я сравнил использование только исходных файлов с использованием исходных и заголовочных файлов.
erelender

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

5

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

Чтобы компенсировать это ограничение, C и C ++ допускают объявления, и эти объявления могут быть включены в модули, которые используют их с помощью директивы препроцессора #include.

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

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


Я согласен с тобой. У меня есть похожая идея здесь: stackoverflow.com/questions/3702132/…
smwikipedia

4

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

Да, в этот момент (через 10 лет после первого стандарта C ++ и через 20 лет после того, как он начал серьезно расти в использовании), легко спросить, почему у него нет надлежащей системы модулей. Очевидно, что любой новый язык, разрабатываемый сегодня, не будет работать как C ++. Но дело не в C ++.

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

Это означает, что это делает некоторые вещи сложнее (особенно для людей, начинающих новый проект), а некоторые вещи легче (особенно для тех, кто поддерживает существующий код), чем другие языки.

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


Как вы интегрируете C # и C ++? Через COM?
Питер Мортенсен

1
Есть три основных способа, «лучший» зависит от вашего существующего кода. Я использовал все три. Больше всего я использую COM, потому что мой существующий код уже спроектирован на нем, поэтому он практически беспроблемный, очень хорошо работает для меня. В некоторых странных местах я использую C ++ / CLI, который обеспечивает невероятно плавную интеграцию для любой ситуации, когда у вас еще нет COM-интерфейсов (и вы можете предпочесть использовать существующие COM-интерфейсы, даже если они у вас есть). Наконец, есть p / invoke, который в основном позволяет вам вызывать любую C-подобную функцию, предоставляемую из DLL, поэтому позволяет напрямую вызывать любой Win32 API из C #.
Даниэль Уорвикер

4

Ну, C ++ был ратифицирован в 1998 году, но он использовался гораздо дольше, и ратификация в первую очередь определяла текущее использование, а не навязывала структуру. И поскольку C ++ был основан на C, а C имеет заголовочные файлы, в C ++ они тоже есть.

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

Скажем, у меня есть foo.cpp, и я хочу использовать код из файлов bar.h / bar.cpp.

Я могу #include "bar.h" в foo.cpp, а затем запрограммировать и скомпилировать foo.cpp, даже если bar.cpp не существует. Заголовочный файл действует как обещание компилятору, что классы / функции в bar.h будут существовать во время выполнения, и в нем есть все, что ему нужно уже знать.

Конечно, если функции в bar.h не имеют тел, когда я пытаюсь связать свою программу, она не будет связываться, и я получу ошибку.

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

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


3

Ему не нужен отдельный заголовочный файл с теми же функциями, что и в main. Это нужно только в том случае, если вы разрабатываете приложение, используя несколько файлов кода, и если вы используете функцию, которая не была ранее объявлена.

Это действительно проблема масштаба.


1

C ++ был ратифицирован в 1998 году, так почему же он разработан таким образом? Какие преимущества имеет наличие отдельного заголовочного файла?

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


1

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


1

Ну, вы можете прекрасно разработать C ++ без заголовочных файлов. Фактически, некоторые библиотеки, которые интенсивно используют шаблоны, не используют парадигму заголовочных файлов / файлов кода (см. Boost). Но в C / C ++ нельзя использовать то, что не объявлено. Один из практических способов справиться с этим - использовать заголовочные файлы. Кроме того, вы получаете преимущество совместного использования интерфейса без совместного использования кода / реализации. И я думаю, что создатели C не предполагали этого: когда вы используете общие заголовочные файлы, вы должны использовать знаменитые:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

// [...]
// my header
// [...]

#endif // MY_HEADER_SWEET_GUARDIAN

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

Итак, я думаю, что когда создавался C, проблемы с предварительным объявлением недооценивались, и теперь при использовании языка высокого уровня, такого как C ++, нам приходится иметь дело с такого рода вещами.

Еще одно бремя для нас, бедных пользователей C ++ ...


1

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

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