Чому для 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?" ;-p

Більш серйозно, однак, причина полягає в тому, що компілятор C ++ не може просто проникнути в інший блок перекладу і придумати, як використовувати його символи таким чином, як javac може і робить. Файл заголовка необхідний, щоб оголосити компілятору те, що він може очікувати, що він буде доступний під час посилання.

Так #includeце пряма текстова підміна. Якщо ви визначаєте все у заголовкових файлах, препроцесор в кінцевому підсумку створює величезну копію та вставлення кожного вихідного файлу у вашому проекті та подає його до компілятора. Те, що стандарт C ++ був ратифікований у 1998 році, нічого спільного з цим не має, це той факт, що середовище компіляції для C ++ базується настільки тісно, ​​що й для C.

Перетворення моїх коментарів, щоб відповісти на ваше подальше запитання:

Як компілятор знаходить .cpp-файл із кодом у ньому

Це не принаймні під час компіляції коду, який використовував файл заголовка. Функції, з якими ви пов'язуєтесь, навіть не потрібно писати ще, незважаючи на те, що компілятор знає, в якому .cppфайлі вони будуть знаходитись. Все, що повинен знати код виклику під час компіляції, виражається в декларації функції. Під час посилання ви надасте список .oфайлів або статичних або динамічних бібліотек, а заголовок, по суті, є обіцянкою, що визначення функцій будуть десь там.


3
Додайте до "Причини, які ви хочете відокремити, це:" І я найважливіша функція файлів заголовків: відокремити конструкцію кодової структури від реалізації, тому що: А. Коли ви потрапляєте в дійсно складні структури, в яких задіяно багато об'єктів, це набагато простіше просіювати файли заголовків і пам'ятати, як вони працюють разом, доповнені коментарями до заголовка. B. Коли одна людина не піклується про визначення всієї структури об'єкта, а хтось інший піклується про його реалізацію, він підтримує впорядкованість. Я думаю, що це робить складний код більш читабельним.
Андрес Канелла

Найпростішим способом я можу подумати про корисність розділення файлів заголовка від cpp - це відокремити інтерфейс проти впровадження, що справді допомагає середнім / великим проектам.
Крішна Оза

Я мав намір включити його в (2) "посилання проти коду без визначення". Гаразд, таким чином, ви все ще можете мати доступ до git repo з визначеннями в, але справа в тому, що ви можете скласти свій код окремо від реалізації його залежностей і потім посилання. Якщо буквально все, що ви хотіли, - це розділити інтерфейс / реалізацію на різні файли, не турбуючись про створення окремо, то ви можете це зробити, просто маючи foo_interface.h, включайте foo_implementation.h.
Стів Джессоп

4
@AndresCanella Ні, це не так. Це робить читання та підтримання не-власного коду кошмаром. Щоб повністю зрозуміти, що щось робить у коді, вам потрібно перейти через 2n файли замість n файлів. Це просто не нотація Big-Oh, 2n робить велику різницю в порівнянні з просто n.
Błażej Michalik

1
По-друге, це брехня, що допомагають заголовки. Наприклад, перевірте міні-джерело, так важко прослідкувати, звідки він починається, де передається контроль, де декларуються / визначаються речі. Якщо б він був побудований за допомогою окремих динамічних модулів, це було б засвоюваним, маючи сенс на одне, а потім переходити до модуль залежності. натомість вам потрібно слідувати заголовкам, і це робить читання будь-якого коду, написаного таким чином, пекло. навпаки, nodejs дає зрозуміти, звідки походить без будь-яких ifdefs, і ви можете легко визначити, з чого прийшов.
Дмитро

91

C ++ робить це так, оскільки C зробив це саме так, тому справжнє питання полягає в тому, чому C зробив це саме так? Вікіпедія трохи говорить на це.

Нові компільовані мови (наприклад, Java, C #) не використовують попередні декларації; ідентифікатори розпізнаються автоматично з вихідних файлів і читаються безпосередньо з символів динамічної бібліотеки. Це означає, що файли заголовків не потрібні.


13
+1 Ударив нігтем по голові. Це дійсно не вимагає багатослівного пояснення.
MSalters

6
Це не вдарило моїм нігтем по голові :( я все одно мушу шукати, чому C ++ має використовувати декларації вперед і чому він не може розпізнавати ідентифікатори з вихідних файлів і читати безпосередньо з динамічних символів бібліотеки, і чому C ++ зробив це так тільки тому, що C зробив це так: p
Олександр Тейлор

3
І ви кращий програміст, що зробив це @AlexanderTaylor :)
Дональд Берд

66

Деякі люди вважають переваги файлів заголовків:

  • Стверджується, що це дозволяє / примушує / дозволяє розділяти інтерфейс та реалізацію, але зазвичай це не так. Файли заголовків рясніють інформацією про реалізацію (наприклад, змінні учасника класу повинні бути вказані в заголовку, навіть якщо вони не є частиною загальнодоступного інтерфейсу), а функції можуть і часто є визначеними на вкладі в декларації класу у заголовку, знову знищуючи це розділення.
  • Іноді говорять про покращення часу компіляції, оскільки кожен блок перекладу може бути оброблений незалежно. І все-таки C ++ - це, мабуть, найповільніша мова, що існує, коли справа доходить до компіляції. Частиною причини є безліч багаторазових включень одного заголовка. Велика кількість заголовків включається декількома одиницями перекладу, вимагаючи їх декілька разів проаналізувати.

Зрештою, система заголовків - це артефакт із 70-х років, коли проектувався C. Тоді комп'ютери мали дуже мало пам’яті, і зберігати весь модуль у пам’яті просто не було варіантом. Компілятор повинен був почати читати файл у верхній частині, а потім продовжувати лінійно через вихідний код. Механізм заголовків дозволяє це. Компілятору не потрібно враховувати інші перекладацькі одиниці, він просто повинен читати код зверху вниз.

І 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, яка повертає недійсність і не приймає жодних параметрів. Тож якщо ви сприяєте коду, який намагається викликати SomeOtherFunction, не панікуйте, а натомість шукайте його.

Тепер уявіть, що у вас є SomeFunction та SomeOtherFunction у двох різних .c-файлах. Тоді вам доведеться #include "SomeOther.c" у Some.c. Тепер додайте деякі "приватні" функції в SomeOther.c. Оскільки C не знає приватних функцій, ця функція буде доступна і в Some.c.

Тут знаходяться .h файли: вони задають усі функції (та змінні), які ви хочете "експортувати" з файлу .c, доступ до якого можна отримати в інших .c файлах. Таким чином, ви отримуєте щось на зразок публічної / приватної сфери. Крім того, ви можете надати цей .h файл іншим людям без необхідності ділитися своїм вихідним кодом - .h файли також працюють проти компільованих .lib-файлів.

Отже, головна причина - це зручність, захист вихідного коду та трохи роз’єднання між частинами програми.

Це було, хоча C. C ++ представив класи та приватні / публічні модифікатори, тому, хоча ви все ще можете запитати, чи потрібні вони, C ++ AFAIK все ще вимагає декларування функцій перед їх використанням. Крім того, багато розробників C ++ є або були також розробниками C і перейняли свої концепції та звички на C ++ - чому змінити те, що не порушено?


5
Чому компілятор не може запустити код і знайти всі визначення функцій? Здається, щось, що було б досить легко запрограмувати в компілятор.
Маріус

3
Якщо у вас є джерело, якого у вас часто немає. Скомпільований C ++ - це ефективно машинний код із достатньою кількістю додаткової інформації для завантаження та посилання коду. Потім ви вказуєте процесор на точку входу і даєте йому працювати. Це принципово відрізняється від Java або C #, де код збирається в посередницький байт-код, що містить метадані на його вміст.
DevSolar

Я здогадуюсь, що в 1972 році це могло бути досить дорогою операцією для компілятора.
Michael Stum

Саме так сказав Майкл Стум. Коли ця поведінка була визначена, лінійне сканування через єдиний блок перекладу було єдиним, що можна було практично реалізувати.
jalf

3
Так - компіляція на 16 гірких стрічках із масовою торажем нетривіальна.
MSalters

11

Перша перевага: Якщо у вас немає файлів заголовків, вам доведеться включати вихідні файли в інші вихідні файли. Це призведе до того, що файли, що включають файли, будуть знову скомпільовані при зміні включеного файлу.

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


1
Це ви маєте на увазі, наприклад, наприклад, у C # 'вам доведеться включати вихідні файли в інші вихідні файли? Тому що, очевидно, ви цього не робите. Для другої переваги, я думаю, що це занадто залежно від мови: ви не будете використовувати .h файли, наприклад, Delphi
Vlagged

Вам доведеться перекомпілювати весь проект у будь-якому випадку, так чи справді перша перевага насправді?
Маріус

ОК, але я не думаю, що це мовна особливість. Більш щось практичне розглянути з декларацією С перед визначенням "проблема". Це як би хтось знаменитий сказав "це не помилка, що є особливістю" :)
Нейро

@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, тому дозволяє безпосередньо викликати будь-який API Win32 з 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

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

Це дійсно проблема сфери.


1

C ++ був ратифікований у 1998 році, тож чому він розроблений таким чином? Які переваги має окремий файл заголовка?

Фактично файли заголовків стають дуже корисними при першому дослідженні програм, перевірка файлів заголовків (використовуючи лише текстовий редактор) дає вам огляд архітектури програми, на відміну від інших мов, де вам потрібно використовувати складні інструменти для перегляду класів та їх членські функції.


1

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


1

Ну, ви бездоганно можете розробити C ++ без файлів заголовків. Насправді деякі бібліотеки, які інтенсивно використовують шаблони, не використовують парадигму файлів заголовків / кодів (див. Прискорення). Але в C / C ++ ви не можете використовувати те, що не задекларовано. Один практичний спосіб впоратися з цим - використовувати файли заголовків. Крім того, ви отримуєте перевагу спільного використання інтерфейсу без коду / реалізації. І я думаю, що це не було передбачено творцями C: Коли ви використовуєте спільні файли заголовків, ви повинні використовувати відомі:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

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

#endif // MY_HEADER_SWEET_GUARDIAN

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

Отже, я думаю, що коли С створювався, проблеми з декларуванням вперед були недооцінені, і зараз, коли використовуємо мову високого рівня, як C ++, ми маємо мати справу з подібними речами.

Ще одне тягар для нас, бідних користувачів C ++ ...


1

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

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