Як файл заголовка C ++ може включати реалізацію?


78

Гаразд, якимось чином не експерт по C / C ++, але я думав, що сенс файлу заголовка - декларувати функції, тоді файл C / CPP повинен визначати реалізацію.

Однак, переглядаючи сьогодні деякий код C ++, я знайшов це у файлі заголовка класу ...

public:
    UInt32 GetNumberChannels() const { return _numberChannels; } // <-- Huh??

private:
    UInt32 _numberChannels;

То чому в заголовку є реалізація? Це пов’язано з constключовим словом? Це вбудований метод класу? Яка саме вигода / сенс робити це таким чином, ніж визначення реалізації у файлі CPP?


7
Функція вбудована .
Якийсь програміст, чувак,

1
RE constкваліфікатор; це означає лише, що метод не змінить стан об'єкта.
Michael

4
@Alex: ви неправильно вважаєте, що компілятор повинен вбудувати функцію. Компілятор / компонувальник повинен мати справу з кількома визначеннями (вбудовані функції не підпадають під дію одного правила визначення).
Michael Burr

3
@Alex ніякий компілятор не повинен це вводити. Це може вбудовувати його в якийсь переклад, але це не потрібно робити у всіх ТУ. Так, існує декілька визначень, але оскільки функція (неявно) оголошена вбудованою, компілятор позначає символ, якщо він не вбудовує його, і компонувальник знає, що він повинен вибрати лише один із експортованих символів. Те саме для екземплярів шаблону.
Arne Mertz

1
VC2010 не буде вбудовувати таку функцію, ЯКЩО, наприклад, її магічний "вбудований бюджет" вичерпаний.
ActiveTrayPrntrTagDataStrDrvr

Відповіді:


139

Гаразд, якимось чином не експерт по C / C ++, але я думав, що сенс файлу заголовка - декларувати функції, тоді файл C / CPP повинен визначати реалізацію.

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

Так, наприклад, якщо у вас є такі файли:

Foo.h:

#ifndef FooH
#define FooH

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

#endif

Foo.cpp:

#include "Foo.h"

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

Bar.cpp:

#include "Foo.h"

Foo f;
UInt32 chans = f.GetNumberChannels();

Препроцесор розбирає foo.cpp і Bar.cpp окремо і проводить наступний код , який компілятор потім розбирає:

Foo.cpp:

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

Bar.cpp:

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

Foo f;
UInt32 chans = f.GetNumberChannels();

Bar.cpp компілюється у Bar.obj і містить посилання для виклику Foo::GetNumberChannels(). Foo.cpp компілюється у Foo.obj і містить фактичну реалізацію Foo::GetNumberChannels(). Після компіляції компоновщик потім збігає файли .obj та зв’язує їх між собою, щоб отримати остаточний виконуваний файл.

То чому в заголовку є реалізація?

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

Це пов’язано з ключовим словом const?

Ні. constКлючове слово просто вказує компілятору, що метод не змінить стан об’єкта, до якого його викликають під час виконання.

Яка саме вигода / сенс робити це таким чином, ніж визначення реалізації у файлі CPP?

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


2
Чи означає це, за вашим поясненням, що ви можете оголосити клас безпосередньо у своєму файлі CPP і навіть оголосити вміст функцій-членів у фігурних дужках, що обертають це оголошення класу, так що вам не доведеться використовувати синтаксис :: поза цього? (Я розумію, що це не вважається хорошим кодуванням. Я запитую лише, чи є це дійсним кодуванням.) І з цього приводу це означатиме, що всі члени будуть вбудовані або принаймні позначені як такі? (І чи є щось, що ви можете сказати , не вкладайте цього?)
Марк А. Донохо

@MarqueIV Те, що ви описуєте, технічно можливо, але це завадить вам використовувати цей клас де-небудь за межами одиниці компіляції, визначеної файлом .cpp (по суті, сам файл cpp, унеможливлює # включення його до інших файлів. Що є величезний ні-ні!). Тим не менш, ви все одно можете використовувати вказівники або посилання на клас (просто ніколи не переорієнтовуйте їх або не отримуйте доступ до членів за допомогою вказівників чи посилань), якщо ви переадресуєте клас в інших файлах.
Agentlien

Так, я знаю, що я не міг їх використовувати таким чином. Це скоріше було щось на кшталт "ти можеш це зробити", а не "чи повинен ти це робити". Я більше запитував, чи буде він успішно скомпільований, що і буде. У будь-якому випадку, ви отримали відповідь через деталі, і ви також є єдиною людиною, яка вказала ключове слово const у своїй відповіді (принаймні, коли я все-таки схвалив його.) Дякую! :)
Mark A. Donohoe

Захист заголовка у прикладі, здається, нічого не робить. Не могли б ви пояснити, коли захисний кожух набуде чинності, а коли ні?
Helin Wang

@RemyLebeau Дякую! Інше питання, якщо реалізація знаходиться у файлі заголовка із захистом заголовка. І цей файл заголовка був використаний проектом спільної бібліотеки та основним проектом. А головний проект використовує проект бібліотеки. Під час зв’язування компонент посилання скаржиться на ту саму функцію, яку визначили двічі (повторюваний символ)?
Helin Wang

37

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

Однак є один виняток. Якщо ви оголосите функцію вбудованою, вона звільняється від правила одного визначення. Це те, що відбувається тут, оскільки функції-члени, визначені всередині визначення класу, неявно вбудовані.

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

У вашому прикладі використання constпісля аргументу списку сигналізує, що функція-член не змінює об'єкт, для якого вона викликається. На практиці це означає, що буде розглядатися об'єкт, на який вказують thisусі члени класу та, за його підсумками const. Тобто спроба їх модифікації призведе до помилки під час компіляції.


2
"оскільки функції-члени, визначені всередині визначення класу, неявно вбудовані." Цінна інформація. Не знав цього. Але як щодо цього constслова?
Mark A. Donohoe

5
Дякуємо, що згадали правило одного визначення !
dubbaluga

6

Він неявно оголошується inline в силу того, що є функцією-членом, визначеною в оголошенні класу. Це не означає , що компілятор повинен вбудовувати, але це означає , що ви не будете порушувати одне правило визначення . Це абсолютно не пов'язано з const* . Це також не пов’язано з тривалістю та складністю функції.

Якби це була функція, що не є членом, тоді вам довелося б явно оголосити її як inline:

inline void foo() { std::cout << "foo!\n"; }

* Дивіться тут, щоб дізнатися більше про constкінець функції-члена.


Під одним правилом визначення, ви маєте тут на увазі, що якщо ця функція визначена в заголовку, функція не може бути визначена в іншому файлі cpp?
ашу

3

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

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

Дизайнери C ++ хотіли дозволити об'єктно-орієнтоване програмування з хорошим приховуванням даних, тому вони очікували побачити багато функцій getter та setter. Вони не хотіли необгрунтованого покарання за виконання. Отже, вони спроектували С ++ так, щоб геттери та сеттери могли бути не лише оголошені в заголовку, але й фактично реалізовані, тому вони могли б вбудуватися. Ця функція, яку ви показали, є геттером, і коли цей код С ++ компілюється, виклику функції не буде; код, щоб отримати це значення, буде просто скомпільовано на місці.

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


1

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

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

Я вважаю, ваш приклад відповідає першому випадку.


0

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


0

Стандартні лапки C ++

У проекті стандарту C ++ 17 N4659 10.1.6 "Вбудований специфікатор" сказано, що методи неявно вбудовані:

4 Функція, визначена у визначенні класу, є вбудованою функцією.

а далі далі ми бачимо, що вбудовані методи не тільки можуть, але і повинні бути визначені на всіх одиницях перекладу:

6 Вбудована функція або змінна повинна бути визначена в кожній одиниці перекладу, в якій вона використовується, і повинна мати абсолютно однакове визначення в кожному випадку (6.2).

Це також чітко зазначено в примітці до пункту 12.2.1 "Функції членів":

1 Функція-член може бути визначена (11.4) у визначенні класу, і в цьому випадку це вбудована функція-член (10.1.6) [...]

3 [Примітка: У програмі може бути щонайбільше одне визначення нелінійної функції-члена. У програмі може бути більше одного вбудованого визначення функції члена. Див. 6.2 та 10.1.6. - кінцева примітка]

Реалізація GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

int main() {
    MyClass().myMethod();
}

Складання та перегляд символів:

g++ -c main.cpp
nm -C main.o

вихід:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

тоді ми бачимо, man nmщо MyClass::myMethodсимвол позначений як слабкий на об'єктних файлах ELF, що означає, що він може з'являтися на декількох об'єктних файлах:

"W" "w" Символ - це слабкий символ, який спеціально не позначений як символ слабкого об'єкта. Коли слабко визначений символ пов'язаний з нормальним визначеним символом, нормально визначений символ використовується без помилок. Коли слабкий невизначений символ пов'язаний і символ не визначений, значення символу визначається специфічно для системи без помилок. У деяких системах регістр вказує на те, що вказано значення за замовчуванням.

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