Perché abbiamo bisogno di "C" esterno {#include <foo.h>} in C ++?


136

Perché dobbiamo usare:

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

In particolare:

  • Quando dovremmo usarlo?

  • Cosa sta succedendo a livello di compilatore / linker che ci richiede di usarlo?

  • Come in termini di compilazione / collegamento questo risolve i problemi che ci richiedono di usarlo?

Risposte:


122

C e C ++ sono superficialmente simili, ma ognuno si compila in un set di codice molto diverso. Quando si include un file di intestazione con un compilatore C ++, il compilatore si aspetta il codice C ++. Se, tuttavia, si tratta di un'intestazione C, il compilatore si aspetta che i dati contenuti nel file di intestazione vengano compilati in un determinato formato: "ABI" C ++ o "Application Binary Interface", quindi il linker si blocca. Ciò è preferibile al passaggio di dati C ++ a una funzione in attesa di dati C.

(Per entrare nel nocciolo della realtà, l'ABI di C ++ generalmente 'manipola' i nomi delle loro funzioni / metodi, quindi chiamando printf()senza contrassegnare il prototipo come una funzione C, il C ++ genererà effettivamente la chiamata del codice _Zprintf, oltre alla merda extra alla fine. )

Quindi: usare extern "C" {...}quando si include l'intestazione ac: è così semplice. Altrimenti, avrai una discrepanza nel codice compilato e il linker si strozzerà. Per la maggior parte delle intestazioni, tuttavia, non ti servirà nemmeno externperché la maggior parte delle intestazioni C del sistema terrà già conto del fatto che potrebbero essere incluse dal codice C ++ e già dal externloro codice.


1
Potresti per favore approfondire di più "la maggior parte delle intestazioni di sistema C spiegherà già che potrebbero essere incluse nel codice C ++ e già esternare il loro codice". ?
Bulat M.,

7
@BulatM. Contengono qualcosa del genere: #ifdef __cplusplus extern "C" { #endif quindi, se inclusi in un file C ++, vengono comunque trattati come intestazione C.
Calmarius,

111

extern "C" determina come devono essere denominati i simboli nel file oggetto generato. Se una funzione viene dichiarata senza "C" esterno, il nome del simbolo nel file oggetto utilizzerà la modifica del nome C ++. Ecco un esempio

Dato test.C in questo modo:

void foo() { }

Compilare ed elencare i simboli nel file oggetto fornisce:

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

La funzione foo è in realtà chiamata "_Z3foov". Questa stringa contiene informazioni sul tipo per il tipo e i parametri di ritorno, tra le altre cose. Se invece scrivi test.C in questo modo:

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

Quindi compilare e guardare i simboli:

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

Ottieni il collegamento C. Il nome della funzione "pippo" nel file oggetto è solo "pippo" e non ha tutte le informazioni sul tipo di fantasia che derivano dalla modifica del nome.

Generalmente includi un'intestazione all'interno di "C" {} esterno se il codice che lo accompagna è stato compilato con un compilatore C ma stai provando a chiamarlo da C ++. Quando lo fai, stai dicendo al compilatore che tutte le dichiarazioni nell'intestazione useranno il collegamento C. Quando colleghi il tuo codice, i tuoi file .o conterranno riferimenti a "foo", non a "_Z3fooblah", che si spera corrisponda a qualsiasi cosa ci sia nella libreria a cui stai collegando.

La maggior parte delle biblioteche moderne metterà le protezioni attorno a tali intestazioni in modo che i simboli vengano dichiarati con il giusto collegamento. ad esempio in molte intestazioni standard troverai:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Questo si assicura che quando il codice C ++ include l'intestazione, i simboli nel file oggetto corrispondono a quelli contenuti nella libreria C. Dovresti solo mettere "C" esterno {} attorno alla tua intestazione C se è vecchio e non ha già queste protezioni.


22

In C ++, puoi avere diverse entità che condividono un nome. Ad esempio, ecco un elenco di funzioni tutte denominate pippo :

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

Al fine di differenziare tutti, il compilatore C ++ creerà nomi univoci per ciascuno di essi in un processo chiamato name-mangling o decorating. I compilatori C non lo fanno. Inoltre, ogni compilatore C ++ può farlo in un modo diverso.

extern "C" dice al compilatore C ++ di non eseguire alcuna modifica del nome sul codice tra parentesi graffe. Ciò consente di chiamare le funzioni C dall'interno di C ++.


14

Ha a che fare con il modo in cui i diversi compilatori eseguono la manipolazione del nome. Un compilatore C ++ altera il nome di un simbolo esportato dal file di intestazione in un modo completamente diverso rispetto a un compilatore C, quindi quando si tenta di collegarsi, si ottiene un errore del linker che dice che mancavano i simboli.

Per risolvere questo, diciamo al compilatore C ++ di funzionare in modalità "C", quindi esegue la modifica del nome allo stesso modo del compilatore C. Fatto ciò, gli errori del linker sono stati corretti.


11

C e C ++ hanno regole diverse sui nomi dei simboli. I simboli sono il modo in cui il linker sa che la chiamata alla funzione "openBankAccount" in un file oggetto prodotta dal compilatore è un riferimento a quella funzione che hai chiamato "openBankAccount" in un altro file oggetto prodotto da un altro file sorgente dallo stesso (o compatibile) compilatore. Ciò consente di creare un programma da più di un file sorgente, il che è un sollievo quando si lavora su un grande progetto.

In C la regola è molto semplice, i simboli sono comunque tutti in un unico spazio dei nomi. Pertanto, l'intero "socks" viene memorizzato come "socks" e la funzione count_socks viene memorizzata come "count_socks".

I linker sono stati creati per C e altri linguaggi come C con questa semplice regola di denominazione dei simboli. Quindi i simboli nel linker sono solo stringhe semplici.

Ma in C ++ il linguaggio ti consente di avere spazi dei nomi, polimorfismo e varie altre cose che sono in conflitto con una regola così semplice. Tutte e sei le funzioni polimorfiche chiamate "aggiungi" devono avere simboli diversi, altrimenti verrà utilizzato quello sbagliato da altri file oggetto. Questo viene fatto "manipolando" (che è un termine tecnico) i nomi dei simboli.

Quando si collega il codice C ++ alle librerie o al codice C, è necessario esternamente "C" qualsiasi cosa scritta in C, come file di intestazione per le librerie C, per dire al compilatore C ++ che questi nomi di simboli non devono essere alterati, mentre il resto di il tuo codice C ++ ovviamente deve essere alterato o non funzionerà.


11

Quando dovremmo usarlo?

Quando si collegano C libaries in file di oggetti C ++

Cosa sta succedendo a livello di compilatore / linker che ci richiede di usarlo?

C e C ++ utilizzano schemi diversi per la denominazione dei simboli. Questo dice al linker di usare lo schema di C quando si collega nella libreria data.

Come in termini di compilazione / collegamento questo risolve i problemi che ci richiedono di usarlo?

L'uso dello schema di denominazione C consente di fare riferimento a simboli in stile C. Altrimenti il ​​linker proverebbe simboli in stile C ++ che non funzionerebbero.


7

Dovresti usare la "C" esterna ogni volta che includi un'intestazione che definisce le funzioni che risiedono in un file compilato da un compilatore C, usato in un file C ++. (Molte librerie C standard possono includere questo controllo nelle loro intestazioni per renderlo più semplice per lo sviluppatore)

Ad esempio, se si dispone di un progetto con 3 file, util.c, util.h e main.cpp e entrambi i file .c e .cpp vengono compilati con il compilatore C ++ (g ++, cc, ecc.), Allora non è ' è davvero necessario e può persino causare errori del linker. Se il tuo processo di compilazione utilizza un normale compilatore C per util.c, dovrai utilizzare "C" esterno quando includi util.h.

Quello che sta succedendo è che C ++ codifica i parametri della funzione nel suo nome. Ecco come funziona il sovraccarico delle funzioni. Tutto ciò che tende a succedere in una funzione C è l'aggiunta di un trattino basso ("_") all'inizio del nome. Senza usare la "C" esterna, il linker cercherà una funzione chiamata DoSomething @@ int @ float () quando il nome effettivo della funzione è _DoSomething () o solo DoSomething ().

L'uso di "C" esterno risolve il problema sopra indicato dicendo al compilatore C ++ che dovrebbe cercare una funzione che segue la convenzione di denominazione C anziché quella C ++.


7

Il compilatore C ++ crea nomi di simboli in modo diverso rispetto al compilatore C. Pertanto, se si sta tentando di effettuare una chiamata a una funzione che risiede in un file C, compilato come codice C, è necessario indicare al compilatore C ++ che i nomi dei simboli che sta tentando di risolvere sembrano diversi da quelli predefiniti; altrimenti il ​​passaggio del collegamento fallirà.


6

Il extern "C" {}costrutto indica al compilatore di non eseguire mangling sui nomi dichiarati tra parentesi graffe. Normalmente, il compilatore C ++ "migliora" i nomi delle funzioni in modo da codificare le informazioni sul tipo relative agli argomenti e al valore restituito; questo si chiama il nome maledetto . Il extern "C"costrutto impedisce la distruzione.

Viene generalmente utilizzato quando il codice C ++ deve chiamare una libreria in linguaggio C. Può anche essere usato quando si espone una funzione C ++ (da una DLL, ad esempio) ai client C.


5

Viene utilizzato per risolvere i problemi di modifica del nome. extern C significa che le funzioni si trovano in un'API di tipo C "piatta".


0

Decompila un g++binario generato per vedere cosa sta succedendo

Per capire perché externè necessario, la cosa migliore da fare è capire cosa sta succedendo in dettaglio nei file oggetto con un esempio:

main.cpp

void f() {}
void g();

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

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

Compilare con l' output ELF Linux di GCC 4.8 :

g++ -c main.cpp

Decompilare la tabella dei simboli:

readelf -s main.o

L'output contiene:

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

Interpretazione

Lo vediamo:

  • efe egsono stati memorizzati in simboli con lo stesso nome del codice

  • gli altri simboli erano mutilati. Districiamoli:

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

Conclusione: entrambi i seguenti tipi di simboli non sono stati alterati:

  • definito
  • dichiarato ma non definito ( Ndx = UND), da fornire al collegamento o in fase di esecuzione da un altro file oggetto

Quindi avrai bisogno di extern "C"entrambi quando chiami:

  • C da C ++: dire g++di aspettarsi simboli non confusi prodotti dagcc
  • C ++ da C: g++indica di generare simboli non confusi gccda usare

Cose che non funzionano nell'esterno C

Diventa ovvio che qualsiasi funzione C ++ che richiede la modifica del nome non funzionerà all'interno 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) { }
}

Eseguibile minimo C dall'esempio C ++

Per completezza e per i neofiti là fuori, vedi anche: Come usare i file sorgente C in un progetto C ++?

Chiamare C da C ++ è abbastanza semplice: ogni funzione C ha un solo simbolo non distorto, quindi non è necessario alcun lavoro extra.

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

Correre:

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

Senza extern "C"il collegamento fallisce con:

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

perché si g++aspetta di trovare un uomo mutilato f, che gccnon ha prodotto.

Esempio su GitHub .

Esempio C ++ eseguibile minimo da C.

Chiamare C ++ da è un po 'più difficile: dobbiamo creare manualmente versioni non alterate di ogni funzione che vogliamo esporre.

Qui illustriamo come esporre i sovraccarichi della funzione C ++ a 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);
}

Correre:

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

Senza di extern "C"essa fallisce con:

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

perché ha g++generato simboli alterati che gccnon è possibile trovare.

Esempio su GitHub .

Testato su Ubuntu 18.04.


1
Grazie per aver spiegato il downvote, ora tutto ha senso.
Ciro Santilli 19 冠状 病 六四 事件 法轮功
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.