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

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

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


Последующий вопрос:

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


person Marius    schedule 20.08.2009    source источник
comment
Если вы хотите редактировать только один файл, зайдите на lzz (www.lazycplusplus.com).   -  person Richard Corden    schedule 20.08.2009
comment
Точный дубликат: stackoverflow.com/questions/333889. Почти дубликат: stackoverflow.com/questions/752793   -  person Peter Mortensen    schedule 20.08.2009


Ответы (13)


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

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

Причины, по которым вы можете захотеть разделиться, следующие:

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

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

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

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

Преобразование моих комментариев в ответ на ваш последующий вопрос:

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

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

person Steve Jessop    schedule 20.08.2009
comment
Чтобы добавить к Причинам, по которым вы, возможно, захотите разделить, следующие: & Я считаю, что наиболее важной функцией файлов заголовков является: Отделение проектирования структуры кода от реализации, потому что: A. Когда вы попадаете в действительно сложные структуры, включающие много объектов, легче просеивать файлы заголовков и помнить, как они работают вместе, дополняя их комментариями заголовков. Б. Один человек не заботился об определении всей структуры объекта, а кто-то еще заботился о реализации, он поддерживает порядок. В целом, я считаю, что это делает сложный код более читабельным. - person Andres Canella; 12.07.2012
comment
Проще говоря, я могу думать о полезности разделения файлов заголовков и cpp - это разделение интерфейса и реализаций, что действительно помогает для средних / больших проектов. - person Krishna Oza; 19.03.2015
comment
Я намеревался включить это в (2), ссылку на код без определений. Хорошо, так что у вас все еще может быть доступ к репозиторию git с определениями, но дело в том, что вы можете скомпилировать свой код отдельно от реализаций его зависимостей, а затем связать. Если буквально все, что вам нужно, - это разделить интерфейс / реализацию на разные файлы, не заботясь о сборке отдельно, вы можете сделать это, просто включив foo_interface.h в foo_implementation.h. - person Steve Jessop; 19.03.2015
comment
@AndresCanella Нет, это не так. Это превращает чтение и поддержку чужого кода в кошмар. Чтобы полностью понять, что что-то делает в коде, вам нужно пролистать 2n файлов вместо n файлов. Это просто не нотация Big-Oh, 2n имеет большое значение по сравнению с просто n. - person Błażej Michalik; 10.04.2016
comment
Во-вторых, то, что заголовки помогают, это ложь. проверьте, например, источник minix, так сложно отследить, где он начинается, где передается управление, где вещи объявляются / определяются ... если он был построен с помощью отдельных динамических модулей, его можно было бы усвоить, если бы понять что-то одно, а затем перейти к модуль зависимости. вместо этого вам нужно следить за заголовками, и это делает чтение любого кода, написанного таким образом, адом. в отличие от этого, nodejs дает понять, откуда что пришло, без каких-либо ifdef, и вы можете легко определить, откуда что пришло. - person Dmitry; 16.08.2016

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

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

person Donald Byrd    schedule 20.08.2009
comment
+1 Бьет гвоздь по голове. Это действительно не требует подробных объяснений. - person MSalters; 20.08.2009
comment
Это не попало мне в голову :( Мне все еще нужно выяснить, почему C ++ должен использовать форвардные объявления и почему он не может распознавать идентификаторы из исходных файлов и читать напрямую из символов динамической библиотеки, и почему C ++ сделал это так путь только потому, что C сделал это таким образом: p - person Alexander Taylor; 01.08.2015
comment
И ты лучший программист, @AlexanderTaylor :) - person Donald Byrd; 02.08.2015

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

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

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

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

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

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

person jalf    schedule 20.08.2009
comment
ЭТО лучший ответ на странице. Спасибо! - person Chuck Le Butt; 31.05.2020
comment
Этот ответ должен быть принятым, поскольку он действительно объясняет, почему C ++ был разработан таким образом, а не почему вы можете захотеть отделить - person SubMachine; 12.08.2020
comment
Есть новости по этому поводу? - person Andreas K.; 17.02.2021
comment
Мне это нравится. Удобство использования всегда должно быть на первом месте. Я надеюсь, что это то, к чему движется C ++. - person kakyo; 29.04.2021

Насколько я понимаю (ограниченно - я обычно не являюсь разработчиком 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 ++ представил классы и модификаторы private / public, поэтому, хотя вы все еще можете спросить, нужны ли они, C ++ AFAIK по-прежнему требует объявления функций перед их использованием. Кроме того, многие разработчики C ++ являются или были разработчиками C и переняли свои концепции и привычки на C ++ - зачем менять то, что не сломано?

person Michael Stum    schedule 20.08.2009
comment
Почему компилятор не может просмотреть код и найти все определения функций? Похоже, что-то, что было бы довольно легко запрограммировать в компилятор. - person Marius; 20.08.2009
comment
Если у вас есть исходный код, которого у вас часто нет. Скомпилированный C ++ - это, по сути, машинный код с достаточным количеством дополнительной информации для загрузки и связывания кода. Затем вы указываете ЦП на точку входа и позволяете ему работать. Это принципиально отличается от Java или C #, где код компилируется в промежуточный байт-код, содержащий метаданные о его содержимом. - person DevSolar; 20.08.2009
comment
Я предполагаю, что в 1972 году это могло быть довольно дорогой операцией для компилятора. - person Michael Stum; 20.08.2009
comment
В точности то, что сказал Майкл Штум. Когда это поведение было определено, линейное сканирование через одну единицу трансляции было единственным, что можно было реализовать на практике. - person jalf; 20.08.2009
comment
Ага - компилировать на биттере 16 с ленточной массой нетривиально. - person MSalters; 20.08.2009
comment
Я новичок в изучении C ++, но это объяснение кормления ложкой действительно поразило меня в уме навсегда, вместо того, чтобы принимать терминологию. Вот как это делает C, поэтому C ++ наследует его ..... не могли бы вы посоветовать мне, когда использовать индивидуальный файлы заголовков ??? .... Я предполагаю, что это может быть подходящим для очень большого приложения, где необходимо поддерживать отдельные файлы c ++, что вызывает необходимость в файлах заголовков - person repzero; 29.05.2016
comment
@MichaelStum .... не будет ли создание и поддержка файлов заголовков немного утомительнее (хотя бы) ?? ... например, если я внесу несколько изменений в типы переменных в файле cpp, у меня будет для вставки / изменения объявлений переменных в заголовочный файл .... хм .... - person repzero; 29.05.2016
comment
это лучший ответ не только на этой странице, но и во всем Интернете. Не могли бы вы подробнее рассказать о том, что. Также вы можете передать этот файл .h другим людям, не делясь своим исходным кодом - файлы .h работают и с скомпилированными файлами .lib ... Вот это похоже на истинную причину Файлы .h существуют. поскольку, например, в java, если вы запутаете имена методов jars, это не будет полезной библиотекой для раздачи. так что вы все еще можете запутать тела, но сохраните эти имена методов в скомпилированном исходном коде. что упростило бы декомпиляцию исходного кода для людей. - person Puddle; 11.06.2018
comment
@Puddle Я не думаю, что это истинная причина, потому что в 70-х, когда был разработан C, совместное использование исходного кода было нормой, а не исключением. Я считаю, что это потому, что произвольный доступ к файлам был невозможен - тогда использование магнитных лент было обычным явлением, и поэтому язык можно было скомпилировать, только когда-либо просматривая файлы, а не назад или прыгая. Файлы .h кажутся отличным способом продвинуть объявления вперед, не внося еще большего беспорядка конфликтующих реализаций. - person Michael Stum; 11.06.2018
comment
@MichaelStum, но почему тогда? зачем им это держать? язык - это понимание цели того, что пишет программист. каждый может понять, как создавать заголовки на основе всех классов. это бессмысленная задача, если она буквально ничего не делает, а только помогает компилировать c ++. мы продвинулись дальше и могли бы сделать это автоматизированным, если бы он больше ничего не делал. если это не служит другой цели ... - person Puddle; 11.06.2018

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

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

person erelender    schedule 20.08.2009
comment
Вы имеете в виду, например, в C # «вам нужно будет включать исходные файлы в другие исходные файлы»? Потому что, очевидно, нет. Что касается второго преимущества, я думаю, что это слишком зависит от языка: вы не будете использовать файлы .h, например, Delphi - person Vlagged; 20.08.2009
comment
В любом случае вам придется перекомпилировать весь проект, так что первое преимущество действительно в счет? - person Marius; 20.08.2009
comment
хорошо, но я не думаю, что это особенность языка. Более практично иметь дело с объявлением C перед проблемой определения. Это как если бы кто-то известный сказал, что это не ошибка, а особенность :) - person neuro; 20.08.2009
comment
@Marius: Да, это действительно важно. Связывание всего проекта отличается от компиляции и связывания всего проекта. По мере увеличения количества файлов в проекте их компиляция становится очень утомительной. @Vlagged: Вы правы, но я не сравнивал C ++ с другим языком. Я сравнил использование только исходных файлов с исходными и заголовочными файлами. - person erelender; 20.08.2009
comment
C # не включает исходные файлы в другие, но вам все равно нужно ссылаться на модули - и это заставляет компилятор извлекать исходные файлы (или отражать их в двоичном коде) для анализа символов, которые использует ваш код. - person gbjbaanb; 20.08.2009
comment
Так подождите, вы серьезно утверждаете, что модель компиляции C ++ более эффективна, чем модель C #? Система заголовков вызывает огромное количество накладных расходов, которых можно было бы избежать в разумной системе сборки. - person jalf; 20.08.2009
comment
Да, если альтернативой простому файлу заголовка является изобретение виртуальной машины, отражения, общей среды выполнения и т. Д.! - person Martin Beckett; 20.08.2009
comment
Как я уже сказал, я не думал об этом вопросе как о сравнении языков, а просто о сравнении сценариев использования. - person erelender; 20.08.2009
comment
Прошло почти десять лет с тех пор, как вы ответили, но может ли кто-нибудь объяснить, как файлы заголовков предотвращают совместное использование кода? и я имею в виду, логику этого. в Java единственное средство защиты кода, о котором я знаю, - это запутывание. как это работает в C ++ с заголовочными файлами? - person Puddle; 11.06.2018
comment
как знание имен позволяет использовать скомпилированный исходный код? как они связываются? слова все еще в двоичном формате или что-то в этом роде? что, если вы изменили имя в файле заголовка? будет ли он по-прежнему ссылаться на метод, который он представляет? - person Puddle; 11.06.2018
comment
Я считаю, что эти два преимущества - одно. - person aderchox; 16.08.2020

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

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

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

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

person VoidPointer    schedule 20.08.2009
comment
Я согласен. У меня тут похожая идея: stackoverflow .com / questions / 3702132 / - person smwikipedia; 13.09.2010

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

Основная причина использования файлов заголовков - это возможность раздельной компиляции файлов и минимизация зависимостей.

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

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

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

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

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

person Mark Krenitsky    schedule 20.08.2009

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

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

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

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

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

person Daniel Earwicker    schedule 20.08.2009
comment
Как вы интегрируете C # и C ++? Через COM? - person Peter Mortensen; 20.08.2009
comment
Есть три основных способа, лучший из которых зависит от вашего существующего кода. Я использовал все три. Чаще всего я использую COM, потому что мой существующий код уже спроектирован вокруг него, так что он практически без проблем работает для меня. В некоторых странных местах я использую C ++ / CLI, который обеспечивает невероятно плавную интеграцию для любой ситуации, когда у вас еще нет COM-интерфейсов (и вы можете предпочесть использование существующих COM-интерфейсов, даже если они у вас есть). Наконец, есть p / invoke, который в основном позволяет вам вызывать любую C-подобную функцию, доступную из DLL, поэтому позволяет напрямую вызывать любой Win32 API из C #. - person Daniel Earwicker; 20.08.2009

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

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

person Alex Rodrigues    schedule 20.08.2009

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

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

person Diaa Sami    schedule 20.08.2009

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

person Paolo Tedesco    schedule 20.08.2009

Что ж, вы можете отлично разработать 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 ++ ...

person neuro    schedule 20.08.2009

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

person Tadeusz Kopec    schedule 20.08.2009