Warum brauchen wir externes "C" {#include <foo.h>} in C ++?


137

Warum müssen wir verwenden:

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

Speziell:

  • Wann sollen wir es benutzen?

  • Was passiert auf Compiler- / Linker-Ebene, für das wir es verwenden müssen?

  • Wie löst dies in Bezug auf das Zusammenstellen / Verknüpfen die Probleme, die es erforderlich machen, es zu verwenden?

Antworten:


123

C und C ++ sind oberflächlich ähnlich, aber jeder kompiliert sich zu einem ganz anderen Satz von Code. Wenn Sie eine Header-Datei in einen C ++ - Compiler einfügen, erwartet der Compiler C ++ - Code. Wenn es sich jedoch um einen C-Header handelt, erwartet der Compiler, dass die in der Header-Datei enthaltenen Daten in ein bestimmtes Format kompiliert werden - das C ++ - 'ABI' oder 'Application Binary Interface', sodass der Linker erstickt. Dies ist der Übergabe von C ++ - Daten an eine Funktion vorzuziehen, die C-Daten erwartet.

(Um wirklich auf den Punkt zu kommen, "entstellt" C ++ 's ABI im Allgemeinen die Namen ihrer Funktionen / Methoden. Wenn Sie also aufrufen, printf()ohne den Prototyp als C-Funktion zu kennzeichnen, generiert C ++ tatsächlich Code-Aufrufe _Zprintfund zusätzlichen Mist am Ende. )

Also: Verwenden Sie diese extern "C" {...}Option, wenn Sie einen AC-Header einschließen - so einfach ist das. Andernfalls wird der kompilierte Code nicht übereinstimmen, und der Linker wird ersticken. Für die meisten Header benötigen Sie jedoch nicht einmal die, externda die meisten System-C-Header bereits die Tatsache berücksichtigen, dass sie möglicherweise im C ++ - Code und bereits in externihrem Code enthalten sind.


1
Könnten Sie bitte näher darauf eingehen, dass "die meisten System-C-Header bereits die Tatsache berücksichtigen, dass sie möglicherweise im C ++ - Code enthalten sind und ihren Code bereits extern enthalten". ?
Bulat M.

7
@BulatM. Sie enthalten ungefähr Folgendes: #ifdef __cplusplus extern "C" { #endif Wenn sie aus einer C ++ - Datei aufgenommen werden, werden sie weiterhin als C-Header behandelt.
Calmarius

111

extern "C" legt fest, wie Symbole in der generierten Objektdatei benannt werden sollen. Wenn eine Funktion ohne externes "C" deklariert wird, verwendet der Symbolname in der Objektdatei die C ++ - Namensverknüpfung. Hier ist ein Beispiel.

Gegeben test.C wie folgt:

void foo() { }

Das Kompilieren und Auflisten von Symbolen in der Objektdatei ergibt:

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

Die foo-Funktion heißt eigentlich "_Z3foov". Diese Zeichenfolge enthält unter anderem Typinformationen für den Rückgabetyp und die Parameter. Wenn Sie stattdessen test.C wie folgt schreiben:

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

Dann kompilieren und betrachten Sie Symbole:

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

Sie erhalten eine C-Verknüpfung. Der Name der "foo" -Funktion in der Objektdatei ist nur "foo" und enthält nicht alle ausgefallenen Typinformationen, die aus der Namensverfälschung stammen.

Sie fügen in der Regel einen Header in extern "C" {} ein, wenn der dazugehörige Code mit einem C-Compiler kompiliert wurde, Sie ihn jedoch aus C ++ aufrufen möchten. Wenn Sie dies tun, teilen Sie dem Compiler mit, dass alle Deklarationen im Header die C-Verknüpfung verwenden. Wenn Sie Ihren Code verknüpfen, enthalten Ihre .o-Dateien Verweise auf "foo", nicht auf "_Z3fooblah", was hoffentlich mit dem übereinstimmt, mit dem Sie in der Bibliothek verknüpfen.

Die meisten modernen Bibliotheken werden solche Header schützen, sodass Symbole mit der richtigen Verknüpfung deklariert werden. zB in vielen Standard-Headern finden Sie:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Dadurch wird sichergestellt, dass die Symbole in Ihrer Objektdatei mit denen in der C-Bibliothek übereinstimmen, wenn C ++ - Code den Header enthält. Sie sollten nur externes "C" {} um Ihren C-Header setzen müssen, wenn dieser alt ist und diese Wachen noch nicht hat.


22

In C ++ können Sie verschiedene Entitäten haben, die einen Namen gemeinsam haben. Zum Beispiel ist hier eine Liste von Funktionen, die alle foo heißen :

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

Um zwischen allen zu unterscheiden, erstellt der C ++ - Compiler in einem Prozess, der als Namensverknüpfung oder -dekoration bezeichnet wird, eindeutige Namen für jeden Namen. C-Compiler tun dies nicht. Darüber hinaus kann jeder C ++ - Compiler dies auf andere Weise tun.

extern "C" weist den C ++ - Compiler an, keine Namensänderung für den Code in geschweiften Klammern vorzunehmen. Auf diese Weise können Sie C-Funktionen in C ++ aufrufen.


14

Dies hängt mit der Art und Weise zusammen, wie die verschiedenen Compiler das Namens-Mangling durchführen. Ein C ++ - Compiler entstellt den Namen eines aus der Header-Datei exportierten Symbols auf völlig andere Weise als ein C-Compiler. Wenn Sie also versuchen, eine Verknüpfung herzustellen, wird ein Linkerfehler angezeigt, der besagt, dass Symbole fehlen.

Um dies zu beheben, weisen wir den C ++ - Compiler an, im "C" -Modus ausgeführt zu werden, damit die Namensverwaltung auf die gleiche Weise wie beim C-Compiler ausgeführt wird. Danach werden die Linkerfehler behoben.


11

C und C ++ haben unterschiedliche Regeln für Symbolnamen. Mit Symbolen weiß der Linker, dass der Aufruf der Funktion "openBankAccount" in einer vom Compiler erstellten Objektdatei eine Referenz auf die Funktion ist, die Sie "openBankAccount" in einer anderen Objektdatei genannt haben, die von derselben (oder kompatiblen) aus einer anderen Quelldatei erstellt wurde. Compiler. Auf diese Weise können Sie ein Programm aus mehr als einer Quelldatei erstellen. Dies ist eine Erleichterung bei der Arbeit an einem großen Projekt.

In C ist die Regel sehr einfach, Symbole befinden sich sowieso alle in einem einzigen Namensraum. Die Ganzzahl "socks" wird also als "socks" und die Funktion count_socks als "count_socks" gespeichert.

Mit dieser einfachen Symbolbenennungsregel wurden Linker für C und andere Sprachen wie C erstellt. Symbole im Linker sind also nur einfache Zeichenfolgen.

In C ++ können Sie in der Sprache Namespaces, Polymorphismus und verschiedene andere Dinge verwenden, die mit einer so einfachen Regel in Konflikt stehen. Alle sechs polymorphen Funktionen mit der Bezeichnung "Hinzufügen" müssen unterschiedliche Symbole haben. Andernfalls wird das falsche von anderen Objektdateien verwendet. Dies geschieht durch "Zerfleischen" (das ist ein technischer Begriff) der Namen von Symbolen.

Wenn Sie C ++ - Code mit C-Bibliotheken oder Code verknüpfen, benötigen Sie externes "C" für alles, was in C geschrieben ist, z. B. Header-Dateien für die C-Bibliotheken, um Ihrem C ++ - Compiler mitzuteilen, dass diese Symbolnamen nicht entstellt werden dürfen, während der Rest von Ihr C ++ - Code muss natürlich entstellt sein, sonst funktioniert es nicht.


11

Wann sollen wir es benutzen?

Wenn Sie C-Bibliotheken mit C ++ - Objektdateien verknüpfen

Was passiert auf Compiler- / Linker-Ebene, für das wir es verwenden müssen?

C und C ++ verwenden unterschiedliche Schemata für die Symbolbenennung. Dies weist den Linker an, das C-Schema zu verwenden, wenn er in der angegebenen Bibliothek verknüpft.

Wie löst dies in Bezug auf das Zusammenstellen / Verknüpfen die Probleme, die es erforderlich machen, es zu verwenden?

Mit dem C-Namensschema können Sie auf Symbole im C-Stil verweisen. Andernfalls würde der Linker Symbole im C ++ - Stil ausprobieren, die nicht funktionieren würden.


7

Sie sollten extern "C" immer dann verwenden, wenn Sie einen Header einfügen, der Funktionen definiert, die sich in einer Datei befinden, die von einem C-Compiler kompiliert und in einer C ++ - Datei verwendet wird. (Viele Standard-C-Bibliotheken enthalten diese Prüfung möglicherweise in ihren Headern, um sie für den Entwickler einfacher zu machen.)

Wenn Sie beispielsweise ein Projekt mit 3 Dateien, util.c, util.h und main.cpp, haben und sowohl die .c- als auch die .cpp-Dateien mit dem C ++ - Compiler (g ++, cc usw.) kompiliert werden, ist dies nicht der Fall. Es wird nicht wirklich benötigt und kann sogar Linkerfehler verursachen. Wenn Ihr Erstellungsprozess einen regulären C-Compiler für util.c verwendet, müssen Sie externes "C" verwenden, wenn Sie util.h einschließen.

Was passiert ist, dass C ++ die Parameter der Funktion in ihrem Namen codiert. So funktioniert Funktionsüberladung. Alles, was einer C-Funktion passieren kann, ist das Hinzufügen eines Unterstrichs ("_") am Anfang des Namens. Ohne externes "C" sucht der Linker nach einer Funktion namens DoSomething @@ int @ float (), wenn der tatsächliche Name der Funktion _DoSomething () oder nur DoSomething () lautet.

Die Verwendung von externem "C" löst das obige Problem, indem dem C ++ - Compiler mitgeteilt wird, dass er nach einer Funktion suchen soll, die der C-Namenskonvention anstelle der C ++ - folgt.


7

Der C ++ - Compiler erstellt Symbolnamen anders als der C-Compiler. Wenn Sie also versuchen, eine Funktion aufzurufen, die sich in einer C-Datei befindet, die als C-Code kompiliert wurde, müssen Sie dem C ++ - Compiler mitteilen, dass die Symbolnamen, die aufgelöst werden sollen, anders aussehen als standardmäßig. Andernfalls schlägt der Verknüpfungsschritt fehl.


6

Das extern "C" {}Konstrukt weist den Compiler an, die in geschweiften Klammern deklarierten Namen nicht zu entstellen. Normalerweise "erweitert" der C ++ - Compiler die Funktionsnamen so, dass sie Typinformationen über Argumente und den Rückgabewert codieren. Dies nennt man den verstümmelten Namen . Das extern "C"Konstrukt verhindert das Zerfleischen.

Es wird normalerweise verwendet, wenn C ++ - Code eine C-Sprachbibliothek aufrufen muss. Es kann auch verwendet werden, wenn eine C ++ - Funktion (z. B. aus einer DLL) C-Clients zur Verfügung gestellt wird.


5

Dies wird verwendet, um Probleme mit der Namensveränderung zu beheben. extern C bedeutet, dass sich die Funktionen in einer "flachen" API im C-Stil befinden.


0

Dekompilieren Sie eine g++generierte Binärdatei, um zu sehen, was los ist

Um zu verstehen, warum dies externerforderlich ist, müssen Sie am besten anhand eines Beispiels verstehen, was in den Objektdateien im Detail vor sich geht:

main.cpp

void f() {}
void g();

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

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

Kompilieren Sie mit der GCC 4.8 Linux ELF- Ausgabe:

g++ -c main.cpp

Dekompilieren Sie die Symboltabelle:

readelf -s main.o

Die Ausgabe enthält:

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

Deutung

Wir sehen das:

  • efund egwurden in Symbolen mit dem gleichen Namen wie im Code gespeichert

  • Die anderen Symbole wurden verstümmelt. Lassen Sie uns sie entwirren:

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

Schlussfolgerung: Beide der folgenden Symboltypen wurden nicht entstellt:

  • definiert
  • deklariert, aber undefiniert ( Ndx = UND), zur Verknüpfung oder Laufzeit aus einer anderen Objektdatei bereitzustellen

Sie benötigen also extern "C"beides, wenn Sie anrufen:

  • C aus C ++: Sagen Sie g++, dass Sie entwirrte Symbole erwarten sollen, die von erzeugt werdengcc
  • C ++ aus C: g++Weisen Sie an, nicht verschränkte Symbole für gccdie Verwendung zu generieren

Dinge, die in extern C nicht funktionieren

Es wird offensichtlich, dass jede C ++ - Funktion, die eine Namensveränderung erfordert, im Inneren nicht funktioniert 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) { }
}

Minimal ausführbares C aus C ++ - Beispiel

Der Vollständigkeit halber und für die Neulinge da draußen siehe auch: Wie verwende ich C-Quelldateien in einem C ++ - Projekt?

Das Aufrufen von C aus C ++ ist ziemlich einfach: Jede C-Funktion verfügt nur über ein mögliches nicht entstelltes Symbol, sodass keine zusätzliche Arbeit erforderlich ist.

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; }

Lauf:

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

Ohne extern "C"den Link scheitert mit:

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

weil g++erwartet, eine verstümmelte zu finden f, die gccnicht produziert hat.

Beispiel auf GitHub .

Minimal ausführbares C ++ aus C-Beispiel

Das Aufrufen von C ++ von ist etwas schwieriger: Wir müssen manuell nicht entstellte Versionen jeder Funktion erstellen, die wir verfügbar machen möchten.

Hier zeigen wir, wie C ++ - Funktionsüberladungen C ausgesetzt werden.

Haupt 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);
}

Lauf:

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

Ohne extern "C"scheitert es mit:

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

weil g++erzeugte verstümmelte Symbole, die gccnicht finden können.

Beispiel auf GitHub .

Getestet in Ubuntu 18.04.


1
Vielen Dank für die Erklärung der Ablehnung. Jetzt macht alles Sinn.
Ciro Santilli 法轮功 冠状 病 六四 事件 19
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.