Чому нам потрібно включати .h, коли все працює, включаючи лише файл .cpp?


18

Чому нам потрібно включати .hі .cppфайли, і файли, тоді як ми можемо змусити його працювати виключно, включивши .cppфайл?

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

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

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


Обмін дослідженнями допомагає всім . Розкажіть, що ви пробували і чому це не відповідало вашим потребам. Це свідчить про те, що ви знайшли час, щоб спробувати допомогти собі, це позбавляє нас від повторення очевидних відповідей, а найбільше це допомагає вам отримати більш конкретну та релевантну відповідь. Також дивіться Як просити
gnat

Я настійно рекомендую переглянути відповідні питання тут збоку. programmers.stackexchange.com/questions/56215/… та programmers.stackexchange.com/questions/115368/… - це добре читає

Чому це на програмістах, а не на SO?
Гонки легкості з Монікою

@LightnessRacesInOrbit, оскільки це питання стилю кодування, а не питання "як".
CashCow

@CashCow: Здається, що ви неправильно розумієте ТАК, що не є сайтом "як". Також звучить, як ви неправильно розумієте це питання, яке не є питанням стилю кодування.
Гонки легкості з Монікою

Відповіді:


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, тоді як a .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

@Snowman Right, на що, на мою думку, слід зосередитись на кількох одиницях компіляції. Додавання коду з / без файлів заголовків для розбиття коду на більш дрібні фрагменти є корисним та ортогональним для цілей заголовкових файлів. Якщо все, що ми хочемо зробити, це розділити його на файли заголовків файлів, нам нічого не купувати - як тільки нам потрібне динамічне посилання, умовне посилання та більше побудова заздалегідь, вони раптом дуже корисні.
Бенджамін Грюнбаум

Існує маса способів упорядкування джерела для компілятора / лінкера 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 ++ код генерується автоматично; тоді ви можете змусити програму-генератор робити цю копію та вставити, імітуючи роль препроцесора).

Справа в тому, що препроцесор виконує лише текстові операції. Ви можете (в принципі) повністю уникнути його копіюванням і вставкою, або замінити його іншим "препроцесором" або генератором коду С (наприклад, 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

Приклад Сніговика потребує невеликого розширення, щоб точно показати вам, чому потрібні файли .h.

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

Foo.h -> містить декларацію для класу Foo

Foo.cpp -> містить визначення (імпементація) класу Foo

Main.cpp -> використовує змінну типу Foo.

Bar.cpp -> також використовує змінну типу Foo.

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


1
Це теж не. Це можна вирішити точно так само, як це вирішується у .hфайлах у будь-якому випадку - з включенням охоронців і точно така ж проблема, як і у вас із .hфайлами. Це зовсім не те .h, для чого потрібні файли.
Бенджамін Грюнбаум

Я не впевнений, як би ви використовували включати охоронці, оскільки вони працюють лише для файлу (як це робить препроцесор). У цьому випадку, оскільки Main.cpp і Bar.cpp - це різні файли, Foo.cpp буде включений лише один раз в обидва (захищені чи ні, це не має ніякої різниці). Main.cpp і Bar.cpp будуть компілюватися. Але помилка посилання все ж виникає через подвійне визначення класу Foo. Однак є і спадщина, яку я навіть не згадував. Декларація для зовнішніх модулів (dll's) тощо
SkaarjFace

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

Я не сказав, що не складатиме. Але він не зв'яже і main.o, і bar.o в одному і тому ж виконаному файлі. Не пов'язуючи, який сенс у складанні взагалі.
SkaarjFace

Ага ... запуск коду для запуску? Ви все ще можете зв’язати його, але просто не пов'язувати файли один з одним - скоріше вони будуть тим самим блоком компіляції.
Бенджамін Грюенбаум

-2

Якщо ви пишете весь код в одному файлі, то це зробить ваш код некрасивим.
По-друге, ви не зможете поділитися своїм письмовим класом з іншими. Відповідно до інженерії програмного забезпечення, ви повинні писати клієнтський код окремо. Клієнт не повинен знати, як працює ваша програма. Їм просто потрібен вихід. Якщо ви записуєте всю програму в один і той же файл, це просочить безпеку програми.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.