Dlaczego potrzebujemy zewnętrznego „C” {#include <foo.h>} w C ++?


136

Dlaczego potrzebujemy:

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

Konkretnie:

  • Kiedy powinniśmy go używać?

  • Co się dzieje na poziomie kompilatora / konsolidatora, co wymaga od nas użycia tego?

  • W jaki sposób kompilacja / linkowanie rozwiązuje problemy, które wymagają od nas korzystania?

Odpowiedzi:


122

C i C ++ są z pozoru podobne, ale każdy z nich kompiluje się do zupełnie innego zestawu kodu. Gdy dołączasz plik nagłówkowy do kompilatora C ++, kompilator oczekuje kodu C ++. Jeśli jednak jest to nagłówek C, kompilator oczekuje, że dane zawarte w pliku nagłówkowym zostaną skompilowane do określonego formatu - C ++ „ABI” lub „Application Binary Interface”, więc konsolidator się dławi. Jest to preferowane rozwiązanie niż przekazywanie danych C ++ do funkcji oczekującej danych w C.

(Aby dostać się do naprawdę drobiazgów, ABI w C ++ ogólnie `` zmienia '' nazwy swoich funkcji / metod, więc wywołując printf()bez oznaczania prototypu jako funkcji C, C ++ faktycznie wygeneruje wywołanie kodu _Zprintf, plus dodatkowe bzdury na końcu. )

A więc: użyj, extern "C" {...}gdy dołączasz nagłówek ac - to takie proste. W przeciwnym razie wystąpi niezgodność w skompilowanym kodzie i konsolidator się dusi. Jednak w przypadku większości nagłówków nie będziesz ich nawet potrzebować, externponieważ większość nagłówków systemowych C będzie już uwzględniać fakt, że mogą być zawarte w kodzie C ++ i już w externswoim kodzie.


1
Czy mógłbyś szerzej rozwinąć temat „większość nagłówków systemowych C będzie już uwzględniać fakt, że mogą być one zawarte w kodzie C ++ i już externują swój kod”. ?
Bulat M.

7
@BulatM. Zawierają coś takiego: #ifdef __cplusplus extern "C" { #endif Więc gdy są dołączone z pliku C ++, nadal są traktowane jako nagłówek C.
Calmarius

111

extern "C" określa, jak należy nazywać symbole w generowanym pliku obiektowym. Jeśli funkcja jest zadeklarowana bez zewnętrznego „C”, nazwa symbolu w pliku obiektowym będzie używać zamiany nazw C ++. Oto przykład.

Podany test C tak:

void foo() { }

Kompilowanie i wyświetlanie symboli w pliku obiektowym daje:

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

W rzeczywistości funkcja foo nazywa się „_Z3foov”. Ten ciąg zawiera między innymi informacje o typie zwracanego typu i parametrach. Jeśli zamiast tego napiszesz test.C w ten sposób:

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

Następnie skompiluj i spójrz na symbole:

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

Otrzymujesz połączenie C. Nazwa funkcji „foo” w pliku obiektowym to po prostu „foo” i nie zawiera ona wszystkich wyszukanych informacji o typie, które pochodzą z zamiany nazw.

Zwykle umieszczasz nagłówek w extern „C” {}, jeśli kod, który jest z nim związany, został skompilowany za pomocą kompilatora C, ale próbujesz wywołać go z C ++. Kiedy to zrobisz, mówisz kompilatorowi, że wszystkie deklaracje w nagłówku będą używać połączenia C. Kiedy połączysz swój kod, twoje pliki .o będą zawierały odniesienia do „foo”, a nie „_Z3fooblah”, co, miejmy nadzieję, pasuje do wszystkiego, co jest w bibliotece, do której tworzysz łącze.

Większość współczesnych bibliotek umieszcza osłony wokół takich nagłówków, aby symbole były deklarowane z odpowiednim połączeniem. np. w wielu standardowych nagłówkach znajdziesz:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Daje to pewność, że gdy kod C ++ zawiera nagłówek, symbole w pliku obiektowym są zgodne z tym, co jest w bibliotece C. Powinieneś tylko umieścić extern "C" {} wokół swojego nagłówka C, jeśli jest stary i nie ma już tych zabezpieczeń.


22

W C ++ możesz mieć różne encje, które mają wspólną nazwę. Na przykład tutaj jest lista funkcji o nazwach foo :

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

Aby je wszystkie rozróżnić, kompilator C ++ utworzy unikalne nazwy dla każdego z nich w procesie zwanym manipulowaniem nazwami lub dekorowaniem. Kompilatory C tego nie robią. Co więcej, każdy kompilator C ++ może to zrobić w inny sposób.

extern „C” mówi kompilatorowi C ++, aby nie wykonywał żadnych zmian nazw w kodzie wewnątrz nawiasów. Pozwala to na wywoływanie funkcji C z poziomu C ++.


14

Ma to związek ze sposobem, w jaki różne kompilatory wykonują manipulację nazwami. Kompilator C ++ zmienia nazwę symbolu wyeksportowanego z pliku nagłówkowego w zupełnie inny sposób niż kompilator C, więc podczas próby połączenia wystąpiłby błąd konsolidatora informujący, że brakuje symboli.

Aby rozwiązać ten problem, mówimy kompilatorowi C ++, aby działał w trybie „C”, więc dokonuje manipulacji nazw w taki sam sposób, jak kompilator C. Po wykonaniu tej czynności błędy konsolidatora zostały naprawione.


11

C i C ++ mają różne zasady dotyczące nazw symboli. Symbole to sposób, w jaki konsolidator wie, że wywołanie funkcji „openBankAccount” w jednym pliku obiektowym utworzonym przez kompilator jest odniesieniem do funkcji, którą nazwałeś „openBankAccount” w innym pliku obiektowym utworzonym z innego pliku źródłowego przez ten sam (lub zgodny) kompilator. Pozwala to na stworzenie programu z więcej niż jednego pliku źródłowego, co jest ulgą podczas pracy nad dużym projektem.

W C zasada jest bardzo prosta, symbole i tak znajdują się w jednej przestrzeni nazw. Zatem liczba całkowita „socks” jest przechowywana jako „socks”, a funkcja count_socks jest przechowywana jako „count_socks”.

Linkery zostały zbudowane dla C i innych języków, takich jak C, z tą prostą regułą nazewnictwa symboli. Zatem symbole w konsolidatorze to po prostu proste ciągi.

Ale w C ++ język pozwala mieć przestrzenie nazw, polimorfizm i różne inne rzeczy, które kolidują z tak prostą regułą. Wszystkie sześć twoich funkcji polimorficznych zwanych „add” musi mieć różne symbole, w przeciwnym razie niewłaściwy z nich zostanie użyty przez inne pliki obiektowe. Odbywa się to poprzez „zniekształcanie” (to termin techniczny) nazw symboli.

Kiedy łączysz kod C ++ z bibliotekami lub kodem C, potrzebujesz zewnętrznego „C” wszystkiego, co jest napisane w C, na przykład pliki nagłówkowe dla bibliotek C, aby poinformować kompilator C ++, że te nazwy symboli nie powinny być zniekształcane, podczas gdy reszta Twój kod C ++ oczywiście musi zostać zniekształcony, inaczej nie zadziała.


11

Kiedy powinniśmy go używać?

Kiedy łączysz biblioteki C z plikami obiektowymi C ++

Co się dzieje na poziomie kompilatora / konsolidatora, co wymaga od nas użycia tego?

C i C ++ używają różnych schematów nazywania symboli. Mówi to linkerowi, aby używał schematu C podczas łączenia w danej bibliotece.

W jaki sposób kompilacja / linkowanie rozwiązuje problemy, które wymagają od nas korzystania?

Użycie schematu nazewnictwa C umożliwia odwoływanie się do symboli w stylu C. W przeciwnym razie konsolidator wypróbowałby symbole w stylu C ++, które nie działałyby.


7

Powinieneś używać extern "C" za każdym razem, gdy dołączasz nagłówek definiujący funkcje rezydujące w pliku skompilowanym przez kompilator C, używanym w pliku C ++. (Wiele standardowych bibliotek C może zawierać to sprawdzenie w swoich nagłówkach, aby ułatwić programistę)

Na przykład, jeśli masz projekt z 3 plikami, util.c, util.h i main.cpp, a oba pliki .c i .cpp są kompilowane za pomocą kompilatora C ++ (g ++, cc itd.), To nie jest naprawdę potrzebne, a może nawet powodować błędy konsolidatora. Jeśli twój proces budowania używa zwykłego kompilatora C dla util.c, to będziesz musiał użyć extern "C", dołączając plik util.h.

Dzieje się tak, że C ++ koduje parametry funkcji w jej nazwie. Tak działa przeciążanie funkcji. Wszystko, co zwykle dzieje się z funkcją C, to dodanie podkreślenia („_”) na początku nazwy. Bez użycia extern "C" linker będzie szukał funkcji o nazwie DoSomething @@ int @ float (), gdy rzeczywista nazwa funkcji to _DoSomething () lub po prostu DoSomething ().

Użycie extern „C” rozwiązuje powyższy problem, mówiąc kompilatorowi C ++, że powinien szukać funkcji zgodnej z konwencją nazewnictwa języka C zamiast konwencji C ++.


7

Kompilator C ++ tworzy nazwy symboli inaczej niż kompilator C. Tak więc, jeśli próbujesz wywołać funkcję, która znajduje się w pliku C, skompilowanej jako kod w C, musisz powiedzieć kompilatorowi C ++, że nazwy symboli, które próbuje rozwiązać, wyglądają inaczej niż domyślnie; w przeciwnym razie krok łącza nie powiedzie się.


6

extern "C" {}Konstrukt instruuje kompilator nie wykonywać maglowania na nazw zadeklarowanych w nawiasach. Zwykle kompilator C ++ „ulepsza” nazwy funkcji, tak aby kodowały informacje o typach argumentów i zwracaną wartość; nazywa się to zniekształconą nazwą . extern "C"Konstrukcja zapobiega przekręcona.

Jest zwykle używany, gdy kod C ++ musi wywołać bibliotekę języka C. Może być również używany podczas udostępniania funkcji C ++ (na przykład z biblioteki DLL) klientom C.


5

Służy do rozwiązywania problemów związanych z zniekształcaniem nazw. extern C oznacza, że ​​funkcje znajdują się w „płaskim” interfejsie API w stylu C.


0

Zdekompiluj g++wygenerowany plik binarny, aby zobaczyć, co się dzieje

Aby zrozumieć, dlaczego externjest to konieczne, najlepiej jest zrozumieć szczegółowo, co dzieje się w plikach obiektowych, na przykładzie:

main.cpp

void f() {}
void g();

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

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

Skompiluj z wyjściem ELF GCC 4.8 Linux :

g++ -c main.cpp

Zdekompiluj tablicę symboli:

readelf -s main.o

Wynik zawiera:

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

Interpretacja

Widzimy to:

  • efi egzostały zapisane w symbolach o takiej samej nazwie jak w kodzie

  • inne symbole zostały zniekształcone. Rozplączmy je:

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

Wniosek: oba poniższe typy symboli nie zostały zniekształcone:

  • zdefiniowane
  • zadeklarowane, ale niezdefiniowane ( Ndx = UND), do podania w dowiązaniu lub w czasie wykonywania z innego pliku obiektowego

Więc do extern "C"dzwonienia będziesz potrzebować obu:

  • C z C ++: g++nakazuje oczekiwanie niezarządzonych symboli utworzonych przezgcc
  • C ++ z C: g++nakazuje generowanie niezarządzanych symboli gccdo użycia

Rzeczy, które nie działają w zewnętrznym C.

Staje się oczywiste, że żadna funkcja C ++, która wymaga zmiany nazw, nie będzie działać w środku 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) { }
}

Minimalne uruchamialne C z przykładu C ++

Ze względu na kompletność i dla nowicjuszy zobacz także: Jak używać plików źródłowych C w projekcie C ++?

Wywołanie C z C ++ jest całkiem proste: każda funkcja C ma tylko jeden możliwy niezmieniony symbol, więc nie jest wymagana żadna dodatkowa praca.

main.cpp

#include <cassert>

#include "c.h"

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

ch

#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

cc

#include "c.h"

int f(void) { return 1; }

Biegać:

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

Bez extern "C"linku nie powiedzie się z:

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

ponieważ g++spodziewa się znaleźć zniekształconą f, która gccnie wyprodukowała.

Przykład na GitHub .

Minimalny runnable C ++ z przykładu C.

Wywołanie C ++ z jest nieco trudniejsze: musimy ręcznie stworzyć niezmienione wersje każdej funkcji, którą chcemy ujawnić.

Tutaj ilustrujemy, jak uwidocznić przeciążenia funkcji C ++ w 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);
}

Biegać:

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

Bez extern "C"niego zawodzi z:

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

ponieważ g++wygenerowano zniekształcone symbole, których gccnie można znaleźć.

Przykład na GitHub .

Testowane w Ubuntu 18.04.


1
Dziękuję za wyjaśnienie negatywnego głosu, teraz to wszystko ma sens.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.