Зачем нам нужно включать .h, пока все работает, когда включен только файл .cpp?


18

Почему нам нужно включать .hи .cppфайлы, и файлы, в то время как мы можем заставить работать только .cppфайлы?

Например: создание file.hсодержащих объявлений, затем создание file.cppсодержащих определений и включение обоих в main.cpp.

Альтернативно: создание file.cppсодержащей декларации / определений (без прототипов), включая их в main.cpp.

Оба работают на меня. Я не вижу разницы. Может быть, поможет понимание процесса компиляции и компоновки.


Поделиться своими исследованиями помогает всем . Расскажите нам, что вы пробовали и почему это не соответствует вашим потребностям. Это свидетельствует о том, что вы потратили время, чтобы попытаться помочь себе, избавляет нас от повторения очевидных ответов и, прежде всего, помогает получить более конкретный и актуальный ответ. Также см. Как спросить
Гнат

Я настоятельно рекомендую посмотреть на соответствующие вопросы здесь, на стороне. programmers.stackexchange.com/questions/56215/… и programmers.stackexchange.com/questions/115368/… - хорошие чтения

Почему это на программистов, а не на SO?
Легкость гонки с Моникой

@LightnessRacesInOrbit, потому что это вопрос стиля кодирования, а не вопрос «как».
CashCow

@CashCow: Похоже, вы неправильно поняли SO, что не является «как» сайт. Похоже, вы неправильно поняли этот вопрос, а не вопрос стиля кодирования.
Легкость гонок с Моникой

Ответы:


29

Хотя вы можете включать .cppфайлы, как вы упомянули, это плохая идея.

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

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

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

пример

  • Foo.h -> содержит объявление (интерфейс) для класса Foo.
  • Foo.cpp -> содержит определение (реализацию) для класса Foo.
  • Main.cpp-> содержит основной метод, точку входа в программу. Этот код создает экземпляр Foo и использует его.

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

Компилятор сгенерирует Foo.oиз Foo.cppкоторого содержится весь код класса Foo в скомпилированной форме. Он также генерирует, Main.oкоторый включает основной метод и неразрешенные ссылки на класс Foo.

Сейчас идет компоновщик, который сочетает в себе два объектных файлов Foo.oи Main.oв исполняемый файл. Он видит неразрешенные ссылки Foo, Main.oно видит, что Foo.oсодержит необходимые символы, поэтому он «соединяет точки», так сказать. Вызов функции Main.oтеперь связан с фактическим местоположением скомпилированного кода, поэтому во время выполнения программа может перейти в правильное местоположение.

Если бы вы включили Foo.cppфайл в Main.cpp, было бы два определения класса Foo. Компоновщик увидит это и скажет: «Я не знаю, какой из них выбрать, так что это ошибка». Этап компиляции будет успешным, но соединение не будет. (Если вы просто не компилируете, Foo.cppно тогда почему это в отдельном .cppфайле?)

Наконец, идея различных типов файлов не имеет отношения к компилятору C / C ++. Он компилирует «текстовые файлы», которые, как мы надеемся, содержат действительный код для желаемого языка. Иногда это может быть в состоянии сказать язык на основе расширения файла. Например, скомпилируйте .cфайл без опций компилятора, и он примет C, а расширение .ccили .cppскажет, что он принимает C ++. Тем не менее, я могу легко сказать компилятору скомпилировать файл .hили даже .docxфайл как C ++, и он будет генерировать .oфайл object ( ), если он содержит допустимый код C ++ в текстовом формате. Эти расширения больше для пользы программиста. Если я вижу Foo.hи Foo.cpp, я сразу предполагаю, что первый содержит объявление класса, а второй содержит определение.


Я не понимаю аргумент, который вы приводите здесь. Если вы просто включили Foo.cppв него, Main.cppвам не нужно включать .hфайл, у вас есть на один файл меньше, вы все равно выиграете, разделив код на отдельные файлы для удобства чтения, и ваша команда компиляции будет проще. Хотя я понимаю важность заголовочных файлов, я не думаю, что это так
Бенджамин Грюнбаум

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

2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Самое важное предложение относительно вопроса.
marczellm

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

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

9

Узнайте больше о роли препроцессора C и C ++ , который концептуально является первой «фазой» компилятора C или C ++ (исторически это была отдельная программа /lib/cpp; теперь, по соображениям производительности, она интегрирована внутри самого компилятора cc1 или cc1plus). Прочитайте, в частности, документацию cppпрепроцессора GNU . Таким образом, на практике компилятор концептуально сначала обрабатывает ваш модуль компиляции (или модуль перевода ), а затем работает с предварительно обработанной формой.

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

  • макроопределения
  • определения типов (например typedef, structи classт. д., ...)
  • определения static inlineфункций
  • объявления внешних функций.

Обратите внимание, что условием (и удобством) является их размещение в заголовочном файле.

Конечно, для вашей реализации file.cppнужно все вышеперечисленное, поэтому хочется #include "file.h" сначала.

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

Дело в том, что препроцессор выполняет только текстовые операции. Вы можете (в принципе) полностью избежать этого путем копирования и вставки или заменить его другим «препроцессором» или генератором кода C (например, gpp или m4 ).

Дополнительной проблемой является то, что последние стандарты C (или C ++) определяют несколько стандартных заголовков. Большинство реализаций действительно реализуют эти стандартные заголовки в виде (специфичных для реализации) файлов , но я считаю, что для соответствующей реализации было бы возможно реализовать стандартные включения (например, #include <stdio.h>для C или #include <vector>для C ++) с некоторыми магическими приемами (например, с использованием некоторой базы данных или некоторых информация внутри компилятора).

При использовании компиляторов GCC (например, gccили g++) вы можете использовать -Hфлаг, чтобы получать информацию о каждом включении, и -C -Eфлаги, чтобы получить предварительно обработанную форму. Конечно, есть много других флагов компилятора, влияющих на предварительную обработку (например, -I /some/dir/для добавления /some/dir/для поиска во включаемых файлах и -Dдля предопределения некоторого макроса препроцессора и т. Д., И т. Д. ....).

NB. Будущие версии C ++ (возможно, C ++ 20 , возможно, даже позже) могут иметь модули C ++ .


1
Если ссылки гниют, будет ли это хорошим ответом?
Дэн Пичельман,

4
Я отредактировал свой ответ, чтобы улучшить его, и я тщательно выбираю ссылки - на википедию или документацию GNU - которые, как я считаю, будут оставаться актуальными в течение длительного времени.
Василий Старынкевич,

3

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

Отсюда идиома заголовка C ++. Это соглашение по причине.

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


0

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

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

Файл заголовков обеспечивает решение двух проблем разработки приложений:

  1. Разделение интерфейса и реализации
  2. Улучшено время компиляции / ссылки для больших программ

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

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

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

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

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

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

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


0

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

Самый простой ответ - обслуживание кода.

Есть моменты, когда разумно создать класс:

  • все встроено в заголовок
  • все в модуле компиляции и вообще без заголовка.

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

(Шаблоны, которые должны быть встроены, это немного другая проблема).

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

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

  • Класс «impl», который используется только тем классом, который он реализует. Это деталь реализации этого класса, и внешне она не используется.

  • Реализация абстрактного базового класса, который создается каким-то фабричным методом, который возвращает указатель / ссылку / умный указатель) в базовый класс. Заводской метод был бы выставлен самим классом, не было бы. (Кроме того, если у класса есть экземпляр, который регистрируется в таблице через статический экземпляр, его даже не нужно показывать через фабрику).

  • Класс типа «функтор».

Другими словами, если вы не хотите, чтобы кто-либо включал заголовок.

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

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

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


-1

Пример Snowman нуждается в небольшом расширении, чтобы точно показать, зачем нужны файлы .h.

Добавьте в игру еще один класс Bar, который также зависит от класса Foo.

Foo.h -> содержит объявление для класса Foo

Foo.cpp -> содержит определение (имплементацию) класса Foo

Main.cpp -> использует переменную типа Foo.

Bar.cpp -> тоже использует переменную типа Foo.

Теперь все файлы cpp должны включать Foo.h. Было бы ошибкой включать Foo.cpp в более чем один другой файл cpp. Компоновщик потерпит неудачу, потому что класс Foo будет определен более одного раза.


1
Это тоже не так. Это может быть решено точно так же, как это решается в .hфайлах, в любом случае - с включенной защитой, и это точно та же проблема, что и у вас с .hфайлами. Это не то, что .hфайлы вообще.
Бенджамин Грюнбаум

Я не уверен, как бы вы использовали встроенные охранники, потому что они работают только для одного файла (как это делает препроцессор). В этом случае, поскольку Main.cpp и Bar.cpp - это разные файлы, Foo.cpp будет включен в один из них только один раз (охраняемый или нет). Main.cpp и Bar.cpp будут компилироваться. Но ошибка ссылки все еще происходит из-за двойного определения класса Foo. Однако есть и наследство, о котором я даже не упомянул. Декларации для внешних модулей (DLL) и т. Д.
SkaarjFace

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

Я не сказал, что это не будет компилироваться. Но он не будет связывать main.o и bar.o в одном исполняемом файле. Без ссылки какой смысл в компиляции вообще.
SkaarjFace

Э-э ... заставить код работать? Вы все еще можете связать его, но просто не можете связать файлы друг с другом - скорее они будут одним и тем же модулем компиляции.
Бенджамин Грюнбаум

-2

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

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