Навіщо нам потрібен зовнішній "C" {#include <foo.h>} в C ++?


136

Для чого нам потрібно використовувати:

extern "C" {
#include <foo.h>
}

Конкретно:

  • Коли ми повинні ним користуватися?

  • Що відбувається на рівні компілятора / лінкера, що вимагає від нас його використовувати?

  • Як з точки зору складання / зв’язування це вирішує проблеми, які вимагають від нас його використання?

Відповіді:


122

C і C ++ поверхнево схожі, але кожен складається в дуже різний набір коду. Якщо ви додасте файл заголовка з компілятором C ++, компілятор очікує код C ++. Якщо ж це заголовок С, тоді компілятор очікує, що дані, що містяться у заголовковому файлі, будуть зібрані у певному форматі - C ++ 'ABI' або 'Бінарний інтерфейс програми', тому лінкер закривається. Це краще для передачі даних C ++ до функції, що очікує даних C.

(Щоб потрапити в справжній попрілий, ABI C ++, як правило, "маніпулює" іменами своїх функцій / методів, тому, називаючи, printf()не позначивши прототип як функцію C, C ++ фактично генерує виклик коду _Zprintf, а також додаткове лайно в кінці. )

Отже: використовуйте, extern "C" {...}включаючи заголовок змінного струму - це так просто. Інакше у вас буде невідповідність у складеному коді, і лінкер задихнеться. Однак для більшості заголовків вам навіть не знадобиться, externтому що більшість системних заголовків C вже будуть враховувати той факт, що вони можуть бути включені кодом C ++ і вже externїх кодом.


1
Чи можете ви детальніше розглянути детальніше про те, що "більшість системних заголовків C вже будуть враховувати той факт, що вони можуть бути включені в код C ++ і вже висувати їх код". ?
Булат М.

7
@BulatM. Вони містять щось подібне: #ifdef __cplusplus extern "C" { #endif Отже, якщо вони включені у файл C ++, вони все ще розглядаються як заголовки C.
Кальмарій

111

extern "C" визначає, як символи в створеному файлі об'єкта повинні бути названі. Якщо функція оголошена без зовнішнього "C", ім'я символу в файлі об'єкта використовуватиме C ++ ім'я mangling. Ось приклад.

Дано test.C так:

void foo() { }

Компіляція та перелік символів у файлі об'єкта дає:

$ g++ -c test.C
$ nm test.o
0000000000000000 T _Z3foov
                 U __gxx_personality_v0

Функція foo насправді називається "_Z3foov". Цей рядок, серед іншого, містить інформацію про тип для типу повернення та параметрів. Якщо ви замість цього пишете test.C так:

extern "C" {
    void foo() { }
}

Потім складіть і подивіться на символи:

$ g++ -c test.C
$ nm test.o
                 U __gxx_personality_v0
0000000000000000 T foo

Ви отримуєте C-зв'язок. Назва функції "foo" у файлі об'єкта є просто "foo", і вона не має всієї фантазійної інформації про тип, що походить від керування іменами.

Як правило, ви включаєте заголовок в екстерн "C" {}, якщо код, що йде разом з ним, був компільований компілятором C, але ви намагаєтеся викликати його з C ++. Коли ви це зробите, ви повідомляєте компілятору, що всі декларації в заголовку будуть використовувати зв'язок C. Коли ви пов'язуєте свій код, ваші .o файли будуть містити посилання на "foo", а не на "_Z3fooblah", який, сподіваємось, відповідає тому, що знаходиться в бібліотеці, з якою ви посилаєтесь.

Більшість сучасних бібліотек розміщують варти навколо таких заголовків, щоб символи оголошувались правильним зв'язком. наприклад, у багатьох стандартних заголовках ви знайдете:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Це гарантує, що коли код C ++ включає заголовок, символи у вашому об’єктному файлі відповідають тому, що знаходиться в бібліотеці С. Вам слід лише поставити зовнішній "C" {} навколо заголовка C, якщо він старий, а цих захисників уже немає.


22

У C ++ ви можете мати різні об'єкти, які поділяють ім’я. Наприклад, ось перелік функцій, які всі назвали foo :

  • A::foo()
  • B::foo()
  • C::foo(int)
  • C::foo(std::string)

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

extern "C" повідомляє компілятору C ++ не виконувати жодне керування іменами коду в дужках. Це дозволяє викликати функції C зсередини C ++.


14

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

Щоб вирішити це, ми говоримо компілятору C ++ запускати в режимі "C", тому він виконує маніпулювання іменами так само, як і компілятор C. Виконавши це, помилки лінкера виправляються.


11

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

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

За допомогою цього простого правила іменування символів були створені посилання для C та інших мов, як C. Тож символи в лінкері - це просто прості рядки.

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

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


11

Коли ми повинні ним користуватися?

Коли ви з'єднуєте C-бібліотеки з файлами об'єктів C ++

Що відбувається на рівні компілятора / лінкера, що вимагає від нас його використовувати?

C і C ++ використовують різні схеми для іменування символів. Це вказує лінкеру використовувати схему С під час посилання в дану бібліотеку.

Як з точки зору складання / зв’язування це вирішує проблеми, які вимагають від нас його використання?

Використання схеми іменування C дозволяє посилатися на символи у стилі C. Інакше лінкер спробує символи стилю C ++, які б не працювали.


7

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

Наприклад, якщо у вас є проект з 3-ма файлами, util.c, util.h, і main.cpp, а файли .c і .cpp компілюються за допомогою компілятора C ++ (g ++, cc тощо), тоді це isn ' t дійсно потрібно, і навіть може спричинити помилки лінкера. Якщо ваш процес збирання використовує звичайний компілятор C для util.c, тоді вам потрібно буде використовувати зовнішній "C", включаючи util.h.

Що відбувається, це те, що C ++ кодує параметри функції у своєму імені. Ось так працює перевантаження функцій. Все, що має тенденцію відбуватися із функцією С, - це додавання підкреслення ("_") до початку імені. Без використання зовнішнього "C" лінкер шукатиме функцію з назвою DoSomething @@ int @ float (), коли власне ім'я функції - _DoSomething () або просто DoSomething ().

Використання зовнішнього "C" вирішує вищезазначену проблему, повідомляючи компілятору C ++, що він повинен шукати функцію, яка слідує конвенції іменування C замість C ++.


7

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


6

extern "C" {}Конструкція інструктує компілятор не виконувати перекручуючи по іменах , оголошених в фігурних дужках. Зазвичай компілятор C ++ "розширює" назви функцій, щоб вони кодували інформацію про аргументи та повернене значення; це називається згорбленим іменем . extern "C"Конструкція запобігає перекручування.

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


5

Це використовується для вирішення проблем, пов'язаних із керуванням іменами. extern C означає, що функції знаходяться в "плоскому" API стилю C.


0

Декомпілюйте g++створений двійковий файл, щоб побачити, що відбувається

Щоб зрозуміти, чому externце необхідно, найкраще зробити, це зрозуміти, що докладно відбувається в об’єктних файлах на прикладі:

main.cpp

void f() {}
void g();

extern "C" {
    void ef() {}
    void eg();
}

/* Prevent g and eg from being optimized away. */
void h() { g(); eg(); }

Компілюйте з GCC 4.8 Linux ELF output:

g++ -c main.cpp

Декомпілюйте таблицю символів:

readelf -s main.o

Вихід містить:

Num:    Value          Size Type    Bind   Vis      Ndx Name
  8: 0000000000000000     6 FUNC    GLOBAL DEFAULT    1 _Z1fv
  9: 0000000000000006     6 FUNC    GLOBAL DEFAULT    1 ef
 10: 000000000000000c    16 FUNC    GLOBAL DEFAULT    1 _Z1hv
 11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z1gv
 12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND eg

Інтерпретація

Ми бачимо, що:

  • efі egзберігалися в символах з тим же найменуванням, що і в коді

  • інші символи були зіпсовані. Розв’яжемо їх:

    $ c++filt _Z1fv
    f()
    $ c++filt _Z1hv
    h()
    $ c++filt _Z1gv
    g()

Висновок: обидва наступні типи символів не були змінені:

  • визначений
  • оголошено, але невизначено ( Ndx = UND), надаватимуться за посиланням або час запуску з іншого файлу об'єкта

Тож вам потрібно буде extern "C"і те, і інше:

  • C від C ++: скажіть, g++щоб очікувати беззмінних символів, вироблених користувачемgcc
  • C ++ від C: скажіть g++генерувати беззмінні символи для gccвикористання

Речі, які не працюють у зовнішній С

Стає очевидним, що будь-яка функція C ++, яка вимагає керування іменами, не буде працювати всередині extern C:

extern "C" {
    // Overloading.
    // error: declaration of C function ‘void f(int)’ conflicts with
    void f();
    void f(int i);

    // Templates.
    // error: template with C linkage
    template <class C> void f(C i) { }
}

Мінімальний цикл, що можна виконати на прикладі C ++

Для повноти та для новичок там див. Також: Як використовувати вихідні файли C у проекті C ++?

Викликати C із C ++ досить просто: кожна функція C має лише один можливий некерований символ, тому додаткової роботи не потрібно.

main.cpp

#include <cassert>

#include "c.h"

int main() {
    assert(f() == 1);
}

гл

#ifndef C_H
#define C_H

/* This ifdef allows the header to be used from both C and C++. */
#ifdef __cplusplus
extern "C" {
#endif
int f();
#ifdef __cplusplus
}
#endif

#endif

куб

#include "c.h"

int f(void) { return 1; }

Виконати:

g++ -c -o main.o -std=c++98 main.cpp
gcc -c -o c.o -std=c89 c.c
g++ -o main.out main.o c.o
./main.out

Без extern "C"посилання не вдається:

main.cpp:6: undefined reference to `f()'

тому що g++розраховує знайти згорбленого f, що gccне призвело.

Приклад на GitHub .

Мінімальний цикл C ++ з прикладу C

Викликати C ++ від трохи складніше: нам потрібно створити вручну версії кожної функції, яку ми хочемо викрити.

Тут ми проілюструємо, як піддавати функцію C ++ перевантаження на C.

main.c

#include <assert.h>

#include "cpp.h"

int main(void) {
    assert(f_int(1) == 2);
    assert(f_float(1.0) == 3);
    return 0;
}

cpp.h

#ifndef CPP_H
#define CPP_H

#ifdef __cplusplus
// C cannot see these overloaded prototypes, or else it would get confused.
int f(int i);
int f(float i);
extern "C" {
#endif
int f_int(int i);
int f_float(float i);
#ifdef __cplusplus
}
#endif

#endif

cpp.cpp

#include "cpp.h"

int f(int i) {
    return i + 1;
}

int f(float i) {
    return i + 2;
}

int f_int(int i) {
    return f(i);
}

int f_float(float i) {
    return f(i);
}

Виконати:

gcc -c -o main.o -std=c89 -Wextra main.c
g++ -c -o cpp.o -std=c++98 cpp.cpp
g++ -o main.out main.o cpp.o
./main.out

Без extern "C"цього не вдається:

main.c:6: undefined reference to `f_int'
main.c:7: undefined reference to `f_float'

тому що g++генеруються розіграні символи, яких gccнеможливо знайти.

Приклад на GitHub .

Тестовано в Ubuntu 18.04.


1
Дякуємо, що пояснили голосування, зараз все має сенс.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.