Почему вы можете иметь определение метода внутри заголовочного файла в C ++, а в C вы не можете?

23

В C вы не можете иметь определение / реализацию функции внутри заголовочного файла. Тем не менее, в C ++ вы можете иметь полную реализацию метода внутри заголовочного файла. Почему поведение отличается?

Джошуа Партоги
источник

Ответы:

28

В C, если вы определите функцию в файле заголовка, то эта функция появится в каждом компилируемом модуле, который включает этот файл заголовка, и для этой функции будет экспортирован открытый символ. Так, если функция additup определена в header.h, и foo.c и bar.c оба включают header.h, то foo.o и bar.o будут включать копии additup.

Когда вы перейдете связать эти два объектных файла вместе, компоновщик увидит, что символ additup определен более одного раза, и не допустит этого.

Если вы объявите функцию статической, то символ не будет экспортирован. Объектные файлы foo.o и bar.o будут по-прежнему содержать отдельные копии кода для функции, и они смогут их использовать, но компоновщик не сможет увидеть ни одну копию функции, поэтому он не буду жаловаться Конечно, ни один другой модуль не сможет увидеть эту функцию. И ваша программа будет раздута с двумя одинаковыми копиями одной и той же функции.

Если вы только объявите функцию в заголовочном файле, но не определите ее, а затем определите ее только в одном модуле, то компоновщик увидит одну копию функции, и каждый модуль в вашей программе сможет увидеть ее и используй это. И ваша скомпилированная программа будет содержать только одну копию функции.

Таким образом, вы можете иметь определение функции в заголовочном файле на C, это просто плохой стиль, плохая форма и общая плохая идея.

(Под «объявить» я имею в виду предоставление прототипа функции без тела; под «определением» я подразумеваю фактический код тела функции; это стандартная терминология Си.)

Дэвид Конрад
источник
2
Это не такая плохая идея - такого рода вещи можно найти даже в заголовках GNU Libc.
SK-logic
а как насчет идиоматического переноса заголовочного файла в директиву условной компиляции? Тогда, даже если функция, объявленная в заголовке AND, будет загружена только один раз. Я новичок в C, поэтому я могу быть недоразумением.
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 ++ и т. Д. В качестве исходного файла. Однако они обычно включают параметр командной строки, чтобы разрешить использование любого произвольного расширения для исходного файла.

Обычно файл, указанный в командной строке, называется «Источник», а включенный файл называется «Заголовок».

Шаг препроцессора фактически берет их все и заставляет все выглядеть как один большой файл для компилятора. То, что было в заголовке или в источнике, на самом деле не имеет значения на данный момент. Обычно есть опция компилятора, которая может показать результаты этого этапа.

Таким образом, для каждого файла, который был указан в командной строке компилятора, огромный файл предоставляется компилятору. Это может иметь код / ​​данные, которые будут занимать память и / или создавать символ для ссылки из других файлов. Теперь каждый из них будет генерировать изображение объекта. Компоновщик может дать «дубликат символа», если один и тот же символ найден в более чем двух объектных файлах, которые связаны друг с другом. Возможно, это причина; не рекомендуется помещать код в заголовочный файл, который может создавать символы в объектном файле.

«Встроенные» обычно встроены ... но при отладке они могут не быть встроенными. Так почему же компоновщик не дает многократно определенных ошибок? Простые ... Это «слабые» символы, и пока все данные / код для слабого символа из всех объектов имеют одинаковый размер и содержание, связанная копия будет сохранять одну копию и отбрасывать копию из других объектов. Оно работает.

vrdhn
источник
3

Вы можете сделать это в C99: inlineфункции гарантированно будут предоставлены где-то еще, поэтому, если функция не была встроенной, ее определение переводится в объявление (т. Е. Реализация отбрасывается). И, конечно, вы можете использовать static.

SK-логика
источник
1

С ++ стандартные кавычки

В проекте стандарта C ++ 17 N4659 10.1.6 « Встроенный спецификатор» говорится, что методы неявно встроены:

4 Функция, определенная в определении класса, является встроенной функцией.

и далее далее мы видим, что встроенные методы не только могут, но и должны быть определены во всех единицах перевода:

6 Встроенная функция или переменная должна быть определена в каждой единице перевода, в которой она используется odr, и должна иметь точно такое же определение в каждом случае (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" Символ - это слабый символ, который не был специально помечен как символ слабого объекта. Когда слабый определенный символ связан с нормальным определенным символом, нормальный определенный символ используется без ошибок. Когда слабый неопределенный символ связан, а символ не определен, значение символа определяется системным образом без ошибок. В некоторых системах верхний регистр указывает, что задано значение по умолчанию.

Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
источник
-4

Вероятно, по той же причине, по которой вы должны поместить полную реализацию метода в определение класса в Java.

Они могут выглядеть одинаково, с извилистыми скобками и многими одинаковыми ключевыми словами, но это разные языки.

Пол Бучер
источник
1
Нет, на самом деле существует реальный ответ, и одна из основных целей C ++ заключалась в обратной совместимости с C.
Ed S.
4
Нет, он был разработан, чтобы иметь «высокую степень совместимости с C» и «отсутствие безвозмездной несовместимости с C». (оба из Страуструпа). Я согласен с тем, что можно дать более глубокий ответ, чтобы подчеркнуть, почему эта конкретная несовместимость не является безвозмездной. Не стесняйтесь поставить один.
Пол Мясник
Я хотел бы, но Саймон Рихтер уже имел, когда я отправил это. Мы можем поспорить о разнице между «обратной совместимостью» и «высокой степенью совместимости с Си», но факт остается фактом, что этот ответ неверен. Последнее утверждение было бы правильным, если бы мы сравнивали C # и C ++, но не так сильно с C и C ++.
Эд С.