Чому у C ++ є файли заголовків та .cpp-файли?
Чому у C ++ є файли заголовків та .cpp-файли?
Відповіді:
Ну, головною причиною було б відокремлення інтерфейсу від реалізації. Заголовок оголошує "що" буде робити клас (або все, що реалізується), тоді як файл cpp визначає "як" він буде виконувати ці функції.
Це зменшує залежності, тому код, який використовує заголовок, не обов'язково повинен знати всі деталі реалізації та будь-які інші класи / заголовки, необхідні лише для цього. Це зменшить час компіляції, а також кількість необхідних перекомпіляцій, коли щось у впровадженні зміниться.
Це не ідеально, і ви зазвичай вдаєтеся до таких методів, як Pimpl Idiom, щоб правильно розділити інтерфейс та реалізацію, але це вдалий початок.
Компіляція на C ++ виконується в 2 основні фази:
Перший - це компіляція "вихідних" текстових файлів у бінарні "об'єктні" файли. Файл CPP - це скомпільований файл і збирається без будь-яких знань про інші файли CPP (або навіть бібліотеки), за винятком випадків, коли він подається до нього через неочищене оголошення або включення заголовка Файл CPP зазвичай компілюється у .OBJ або .O "об'єктний" файл.
Друге - це з'єднання всіх файлів "об'єкта", а отже, створення остаточного бінарного файлу (або бібліотеки, або виконуваного файлу).
Де ГЕС поміщається у всьому цьому процесі?
Компіляція кожного файлу 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 має 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
You still have to copy paste the signature from header file to cpp file, don't you?
Не потрібно. Поки CPP "включає" HPP, прекомпілятор автоматично зробить копіювальну вставку вмісту файла HPP у файл CPP. Я оновив відповідь, щоб уточнити це.
While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself
. Ні, це не так. Він знає лише типи, надані користувачем, які в половині часу навіть не будуть заважати читати повернене значення. Потім відбувається неявна конверсія. І тоді, коли у вас є код: foo(bar)
ви навіть не можете бути впевнені foo
, що це функція. Таким чином, компілятор повинен мати доступ до інформації в заголовках, щоб вирішити, чи джерело компілюється правильно, чи ні ... Потім, як тільки код буде складений, лінкер просто з'єднає між собою виклики функцій.
Seems, they're just a pretty ugly arbitrary design.
: Якщо C ++ був створений в 2012 році Але пам’ятайте, що C ++ був побудований на C у 1980-х роках, і в той час обмеження були на той час зовсім іншими (IIRC, для прийняття було вирішено зберегти ті ж лінкери, що й C).
foo(bar)
це функція - якщо вона отримана як вказівник? Насправді, кажучи про поганий дизайн, я звинувачую C, а не C ++. Мені дійсно не подобаються деякі обмеження чистого C, наприклад, файли заголовків або функції, що повертають одне і тільки одне значення, в той час як беручи декілька аргументів на вході (чи не є природним, щоб введення та вихід поводилися аналогічно ; чому декілька аргументів, але один вихід?) :)
Why can't I be sure, that foo(bar) is a function
foo може бути типом, тож ви б назвали конструктор класів. 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
: кортежі можуть допомогти пом’якшити це, а також передавати аргументи за посиланням. Тепер, яким би був синтаксис для повернення повернених кількох значень, і чи варто було б змінити мову?
Оскільки C, звідки виникла концепція, вже 30 років, і тоді це був єдиний життєздатний спосіб зв’язати разом код з кількох файлів.
Сьогодні це жахливий хак, який повністю знищує час компіляції в C ++, викликає незліченну кількість непотрібних залежностей (оскільки визначення класів у файлі заголовка розкривають занадто багато інформації про реалізацію) тощо.
Тому що C ++ успадкував їх від C. На жаль.
Оскільки люди, які розробили формат бібліотеки, не хотіли "витрачати" простір на рідко використовувану інформацію, таку як макроси препроцесора C та декларації функцій.
Оскільки вам потрібна ця інформація, щоб сказати вашому компілятору "ця функція доступна пізніше, коли лінкер виконує свою роботу", вони повинні були створити другий файл, де ця спільна інформація може зберігатися.
Більшість мов після C / C ++ зберігають цю інформацію у висновку (наприклад, байт-код Java) або вони взагалі не використовують попередньо складений формат, завжди їх розповсюджують у вихідному вигляді та збирають на льоту (Python, Perl).
Це препроцесорний спосіб декларування інтерфейсів. Ви поміщаєте інтерфейс (декларації методу) у файл заголовка, а реалізацію - у cpp. Програмам, які використовують вашу бібліотеку, потрібно знати лише інтерфейс, до якого вони можуть отримати доступ через #include.
Часто вам потрібно мати визначення інтерфейсу без необхідності передавати весь код. Наприклад, якщо у вас є спільна бібліотека, ви надсилаєте з нею файл заголовка, який визначає всі функції та символи, що використовуються у спільній бібліотеці. Без файлів заголовка вам потрібно буде доставити джерело.
У рамках одного проекту використовуються файли заголовків, IMHO, принаймні для двох цілей:
Відповідаючи на відповідь MadKeithV ,
Це зменшує залежності, тому код, який використовує заголовок, не обов'язково повинен знати всі деталі реалізації та будь-які інші класи / заголовки, необхідні лише для цього. Це зменшить час компіляції, а також кількість необхідних перекомпіляцій, коли щось у впровадженні зміниться.
Ще одна причина полягає в тому, що заголовок дає унікальний ідентифікатор кожному класу.
Тож якщо у нас є щось подібне
class A {..};
class B : public A {...};
class C {
include A.cpp;
include B.cpp;
.....
};
У нас будуть помилки, коли ми намагатимемося створити проект, оскільки A є частиною B, із заголовками ми б уникнули подібного головного болю ...