Чому мають заголовки та .cpp файли? [зачинено]


484

Чому у C ++ є файли заголовків та .cpp-файли?


3
Пов'язані питання: stackoverflow.com/questions/1945846 / ...
Spoike

це загальна парадигма OOP, .h - клас оголошення, а cpp - це визначення. Одному не потрібно знати, як воно реалізується, він / вона повинен знати лише інтерфейс.
Manish Kakati

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

Бувають випадки, коли файли заголовків є важливими для компіляції - не лише уподобання організації чи спосіб розповсюдження попередньо складених бібліотек. Скажімо, у вас є структура, в якій game.c залежить від BOTH physics.c та math.c; physics.c також залежить від математики. Якщо ви включили .c файли і забули про .h файли назавжди, у вас були б повторювані декларації з math.c і не було надії на компіляцію. Це те, що має для мене найбільш сенс, чому важливі файли заголовків. Сподіваюся, це допоможе комусь іншому.
Samy Bencherif

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

Відповіді:


201

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

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

Це не ідеально, і ви зазвичай вдаєтеся до таких методів, як Pimpl Idiom, щоб правильно розділити інтерфейс та реалізацію, але це вдалий початок.


177
Не дуже правда. Заголовок все ще містить основну частину реалізації. З тих пір, коли змінні приватного примірника були частиною інтерфейсу класу? Функції приватного члена? Тоді, що, до біса, вони роблять у загальнодоступному заголовку? І воно ще більше розпадається з шаблонами.
jalf

13
Ось чому я сказав, що це не ідеально, і ідіома Пімпла потрібна для більшої розлуки. Шаблони - це зовсім інша банка глистів - навіть якщо ключове слово "експорт" повністю підтримувалось у більшості компіляторів, все одно це буде мені синтаксичний цукор, а не справжнє розділення.
Йоріс Тіммерманс

4
Як інші мови справляються з цим? наприклад - Java? У Java немає концепції файлу заголовка.
Лазер

8
@Lazer: Java простіше розбирати. Компілятор Java може розібрати файл, не знаючи всіх класів в інших файлах, і перевірити типи пізніше. У C ++ багато конструкцій неоднозначні без інформації про тип, тому компілятору C ++ потрібна інформація про посилання на типи для розбору файлу. Тому для цього потрібні заголовки.
Нікі

15
@nikie: Що стосується "полегшення" розбору? Якщо у Java була граматика, яка була принаймні такою ж складною, як C ++, вона все одно могла б просто використовувати файли Java. В будь-якому випадку, що з C? C легко розбирає, але використовує як заголовки, так і файли c.
Томас Едінг

609

С ++ компіляція

Компіляція на C ++ виконується в 2 основні фази:

  1. Перший - це компіляція "вихідних" текстових файлів у бінарні "об'єктні" файли. Файл CPP - це скомпільований файл і збирається без будь-яких знань про інші файли CPP (або навіть бібліотеки), за винятком випадків, коли він подається до нього через неочищене оголошення або включення заголовка Файл CPP зазвичай компілюється у .OBJ або .O "об'єктний" файл.

  2. Друге - це з'єднання всіх файлів "об'єкта", а отже, створення остаточного бінарного файлу (або бібліотеки, або виконуваного файлу).

Де ГЕС поміщається у всьому цьому процесі?

Поганий самотній файл CPP ...

Компіляція кожного файлу CPP не залежить від усіх інших файлів CPP, що означає, що якщо A.CPP потрібен символ, визначений у B.CPP, наприклад:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

Він не компілюється, оскільки у A.CPP немає способу дізнатися, чи існує "doSomethingElse" ... Якщо в A.CPP немає декларації, наприклад:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

Потім, якщо у вас є C.CPP, який використовує той самий символ, ви копіюєте / вставляєте декларацію ...

КОПІЯ / ПАСТИЧНИЙ АЛЕРТ!

Так, є проблема. Копії / пасти небезпечні та важкі в обслуговуванні. Що означає, що було б здорово, якби ми мали якийсь спосіб НЕ скопіювати / вставити, а все-таки оголосити символ ... Як це зробити? До включення деякого текстового файлу, який зазвичай суфіксовано .h, .hxx, .h ++ або, моїм кращим для файлів C ++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

Як includeпрацює?

Включення файлу, по суті, буде аналізувати, а потім копіювати та вставляти його вміст у файл CPP.

Наприклад, у наступному коді із заголовком A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... джерело B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... стане після включення:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

Одна невелика річ - навіщо включати B.HPP до B.CPP?

У поточному випадку це не потрібно, і B.HPP має doSomethingElseдекларацію функції, а B.CPP має doSomethingElseвизначення функції (що саме по собі є декларацією). Але в більш загальному випадку, коли B.HPP використовується для декларацій (та вбудованого коду), не може бути відповідного визначення (наприклад, перерахунки, прості структури тощо), тому включення може знадобитися, якщо B.CPP використовує ці декларації від B.HPP. Загалом, джерело має "гарний смак", щоб за замовчуванням включити його заголовок.

Висновок

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

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

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

або навіть простіше

#pragma once

// The declarations in the B.hpp file

2
@nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?Не потрібно. Поки CPP "включає" HPP, прекомпілятор автоматично зробить копіювальну вставку вмісту файла HPP у файл CPP. Я оновив відповідь, щоб уточнити це.
paercebal

7
@Bob : While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Ні, це не так. Він знає лише типи, надані користувачем, які в половині часу навіть не будуть заважати читати повернене значення. Потім відбувається неявна конверсія. І тоді, коли у вас є код: foo(bar)ви навіть не можете бути впевнені foo, що це функція. Таким чином, компілятор повинен мати доступ до інформації в заголовках, щоб вирішити, чи джерело компілюється правильно, чи ні ... Потім, як тільки код буде складений, лінкер просто з'єднає між собою виклики функцій.
paercebal

3
@Bob: [продовження] ... Тепер, лінкер міг би виконати роботу, виконану компілятором, яка, можливо, зробила б ваш варіант можливим. (Я думаю, це предмет пропозиції "модулів" для наступного стандарту). Seems, they're just a pretty ugly arbitrary design.: Якщо C ++ був створений в 2012 році Але пам’ятайте, що C ++ був побудований на C у 1980-х роках, і в той час обмеження були на той час зовсім іншими (IIRC, для прийняття було вирішено зберегти ті ж лінкери, що й C).
paercebal

1
@paercebal Дякую за пояснення та замітки, paercebal! Чому я не можу бути впевнений, що foo(bar)це функція - якщо вона отримана як вказівник? Насправді, кажучи про поганий дизайн, я звинувачую C, а не C ++. Мені дійсно не подобаються деякі обмеження чистого C, наприклад, файли заголовків або функції, що повертають одне і тільки одне значення, в той час як беручи декілька аргументів на вході (чи не є природним, щоб введення та вихід поводилися аналогічно ; чому декілька аргументів, але один вихід?) :)
Борис Бурков

1
@Bobo:: Why can't I be sure, that foo(bar) is a functionfoo може бути типом, тож ви б назвали конструктор класів. In fact, speaking of bad design, I blame C, not C++: Я можу звинувачувати С у багатьох речах, але будучи спроектованими у 70-х роках, це не одна з них. Знову ж таки, обмеження того часу ... such as having header files or having functions return one and only one value: кортежі можуть допомогти пом’якшити це, а також передавати аргументи за посиланням. Тепер, яким би був синтаксис для повернення повернених кількох значень, і чи варто було б змінити мову?
paercebal

93

Оскільки C, звідки виникла концепція, вже 30 років, і тоді це був єдиний життєздатний спосіб зв’язати разом код з кількох файлів.

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


3
Цікаво, чому файли заголовків (або що було насправді потрібно для компіляції / посилання) не були просто "створені автоматично"?
Матін Ульхак

54

Оскільки в C ++ остаточний виконуваний код не несе ніякої символьної інформації, це більш-менш чистий машинний код.

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


16

Тому що C ++ успадкував їх від C. На жаль.


4
Чому успадкування C ++ від C є невдалим?
Локеш

3
@Lokesh Через багаж :(
陳 力

1
Як це може бути відповіддю?
Шуво Саркер

14
@ShuvoSarker, оскільки, як показали тисячі мов, немає технічного пояснення для того, щоб програмісти C ++ змушували два рази записувати підписи функцій. Відповідь "чому?" це "історія".
Борис

15

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

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

Більшість мов після C / C ++ зберігають цю інформацію у висновку (наприклад, байт-код Java) або вони взагалі не використовують попередньо складений формат, завжди їх розповсюджують у вихідному вигляді та збирають на льоту (Python, Perl).


Циклічні посилання не працюватимуть. Тобто ви не можете створити a.lib з a.cpp перед тим, як створити b.lib з b.cpp, але ви також не можете створити b.lib перед a.lib.
MSalters

20
Java це вирішила, Python може це зробити, будь-яка сучасна мова може це зробити. Але в той час, коли був винайдений C, оперативна пам’ять була настільки дорогою і дефіцитною, що вона просто не була можливою.
Аарон Дігулла

6

Це препроцесорний спосіб декларування інтерфейсів. Ви поміщаєте інтерфейс (декларації методу) у файл заголовка, а реалізацію - у cpp. Програмам, які використовують вашу бібліотеку, потрібно знати лише інтерфейс, до якого вони можуть отримати доступ через #include.


4

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

У рамках одного проекту використовуються файли заголовків, IMHO, принаймні для двох цілей:

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

3
Чому не вдалося постачальникам бібліотек просто доставити створений файл "заголовка"? Файл "заголовка", який не має попереднього процесора, повинен давати набагато кращі показники (якщо реально не вдалося реалізувати).
Том Хоутін - тайклін

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

-5

Відповідаючи на відповідь MadKeithV ,

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

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

Тож якщо у нас є щось подібне

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

У нас будуть помилки, коли ми намагатимемося створити проект, оскільки A є частиною B, із заголовками ми б уникнули подібного головного болю ...

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