Чому ви можете мати визначення методу всередині файлу заголовка в C ++, коли в C ви не можете?


23

У C ви не можете мати визначення / реалізацію функції всередині файлу заголовка. Однак у C ++ ви можете мати повну реалізацію методу всередині файлу заголовка. Чому поведінка відрізняється?

Відповіді:


28

У випадку C, якщо ви визначаєте функцію у файлі заголовка, ця функція з’явиться у кожному складеному модулі, що включає цей заголовочний файл, і загальнодоступний символ буде експортований для функції. Отже, якщо функції additup визначені в header.h, а foo.c і bar.c обидва включають header.h, то foo.o і bar.o обидва включатимуть копії additup.

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

Якщо ви визнаєте функцію статичною, жоден символ не експортується. Об'єктні файли foo.o та bar.o як і раніше будуть містити окремі копії коду для функції, і вони зможуть ними користуватися, але лінкер не зможе побачити жодної копії функції, тому не скаржиться. Звичайно, жоден інший модуль також не зможе побачити функцію. І ваша програма буде роздута двома однаковими копіями тієї ж функції.

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

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

(Під "оголосити" я маю на увазі надання прототипу функції без тіла; під "визначити" я маю на увазі надання фактичного коду функції функції; це стандартна термінологія С.)


2
Це не така погана ідея - подібні речі можна знайти навіть у заголовках GNU Libc.
SK-логіка

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

2
@papiro Проблема полягає в тому, що обгортання захищає лише під час одного запуску компілятора. Отже, якщо foo.c компілюється в foo.o в один цикл, bar.c в bar.o в іншому, а foo.o і bar.o пов'язані в a.out в третій (як це характерно), це обгортання не перешкоджає декількох його примірників, по одному у кожному об'єктному файлі.
Девід Конрад

Чи не описана тут проблема, що #ifndef HEADER_Hмає запобігати?
Роберт Харві

27

C і C ++ поводяться дуже однаково в цьому плані - ви можете мати inlineфункції в заголовках. У C ++ будь-який метод, тіло якого знаходиться всередині визначення класу, неявно inline. Якщо ви хочете зробити те ж саме в C, оголосіть функції static inline.


" оголосити функціїstatic inline " ... і ви все одно будете мати кілька копій функції у кожному блоці перекладу, який її використовує. У C ++ з нефункціональною static inlineфункцією у вас буде лише одна копія. Щоб реально мати реалізацію в заголовку в C, ви повинні 1) позначити реалізацію як inline(наприклад inline void func(){do_something();}) та 2) насправді сказати, що ця функція буде в якомусь певному блоці перекладу (наприклад void func();).
Руслан

6

Поняття файлу заголовка потребує невеликого пояснення:

Або ви дасте файл у командному рядку компілятора, або виконайте "#include". Більшість компіляторів приймають командний файл із розширенням c, C, cpp, c ++ тощо як вихідний файл. Однак вони зазвичай включають в себе командний рядок, щоб дозволити також використовувати будь-яке довільне розширення до вихідного файлу.

Зазвичай файл, що задається в командному рядку, називається "Джерело", а той, що входить, називається "Заголовок".

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

Отже, для кожного файлу, який був наданий у командному рядку компілятора, величезний файл надається компілятору. Це може мати код / ​​дані, які будуть займати пам'ять та / або створювати символ, на який слід посилатися з інших файлів. Тепер кожен із них генерує зображення «об’єкта». Лінкер може дати "дублікат символу", якщо той самий символ знайдений у більш ніж двох об'єктних файлах, які пов'язані між собою. Можливо, це причина; не рекомендується вводити код у файл заголовка, який може створювати символи в об’єктному файлі.

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


3

Це можна зробити в C99: inlineфункції гарантовано надаються десь в іншому місці, тому якщо функція не була вкладена, її визначення переводиться в декларацію (тобто реалізація відкидається). І, звичайно, можна скористатися static.


1

Стандартні цитати 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" Символ - це слабкий символ, який конкретно не позначений як символ слабкого об'єкта. Коли слабкий визначений символ пов'язаний із нормально визначеним символом, нормально визначений символ використовується без помилок. Коли слабкий невизначений символ пов'язаний і символ не визначений, значення символу визначається систематично без помилок. У деяких системах великі регістри вказують на те, що вказано значення за замовчуванням.


-4

Можливо, з тієї ж причини, що ви повинні покласти повну реалізацію методу всередині визначення класу на Java.

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


1
Ні, насправді є реальна відповідь, і однією з головних цілей C ++ було бути назад сумісним із С.
Ед С.

4
Ні, він був розроблений як "Висока ступінь сумісності C", так і "Немає сумнівної несумісності з C". (обидва зі Stroustrup). Я погоджуюся, що можна було б дати більш глибоку відповідь, щоб підкреслити, чому ця конкретна несумісність не є безоплатною. Не соромтеся поставити один.
Пол Різник

Я мав би, але Саймон Ріхтер уже мав це, коли я розміщував це. Ми можемо посперечатися з різницею між "зворотним сумісним" та "Високим ступенем сумісності з С", але факт залишається фактом, що ця відповідь є невірною. Останнє твердження було б правильним, якби ми порівнювали C # і C ++, але не так з C і C ++.
Ед С.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.