Por que precisamos de um "C" externo {#include <foo.h>} em C ++?


136

Por que precisamos usar:

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

Especificamente:

  • Quando devemos usá-lo?

  • O que está acontecendo no nível do compilador / vinculador que exige que o usemos?

  • Como, em termos de compilação / vinculação, isso resolve os problemas que exigem que nós a usemos?

Respostas:


122

C e C ++ são superficialmente semelhantes, mas cada um compila em um conjunto muito diferente de código. Quando você inclui um arquivo de cabeçalho com um compilador C ++, o compilador espera o código C ++. Se, no entanto, for um cabeçalho C, o compilador espera que os dados contidos no arquivo de cabeçalho sejam compilados para um determinado formato - o C ++ 'ABI' ou 'Application Binary Interface', para que o vinculador seja ativado. É preferível passar dados C ++ para uma função que espera dados C.

(Para entrar no âmago da questão, a ABI do C ++ geralmente 'manipula' os nomes de suas funções / métodos, portanto, chamando printf()sem sinalizar o protótipo como uma função C, o C ++ realmente gera chamada de código _Zprintf, além de porcaria extra no final. )

Portanto: use extern "C" {...}ao incluir o cabeçalho CA - é simples assim. Caso contrário, você terá uma incompatibilidade no código compilado, e o vinculador será bloqueado. Para a maioria dos cabeçalhos, no entanto, você nem precisará disso externporque a maioria dos cabeçalhos C do sistema já consideram o fato de que eles podem ser incluídos no código C ++ e já no externcódigo.


1
Você poderia, por favor, elaborar mais sobre "a maioria dos cabeçalhos C do sistema já considerará o fato de que eles podem ser incluídos no código C ++ e já exteriores ao código". ?
Bulat M.

7
@BulatM. Eles contêm algo como isto: #ifdef __cplusplus extern "C" { #endif Portanto, quando incluídos em um arquivo C ++, eles ainda são tratados como um cabeçalho C.
Calmarius 13/03

111

extern "C" determina como os símbolos no arquivo de objeto gerado devem ser nomeados. Se uma função for declarada sem o externo "C", o nome do símbolo no arquivo de objeto usará o nome do C ++. Aqui está um exemplo.

Dado test.C assim:

void foo() { }

Compilar e listar símbolos no arquivo de objeto fornece:

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

A função foo é chamada "_Z3foov". Essa sequência contém informações de tipo para o tipo e parâmetros de retorno, entre outras coisas. Se você escrever test.C, assim:

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

Em seguida, compile e observe os símbolos:

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

Você recebe ligação C. O nome da função "foo" no arquivo de objeto é apenas "foo", e não possui todas as informações de tipo sofisticadas que derivam da seleção de nomes.

Você geralmente inclui um cabeçalho no extern "C" {} se o código que o acompanha foi compilado com um compilador C, mas você está tentando chamá-lo a partir do C ++. Ao fazer isso, você está dizendo ao compilador que todas as declarações no cabeçalho usarão a ligação C. Quando você vincula seu código, seus arquivos .o contêm referências a "foo", não a _Z3fooblah ", que, esperançosamente, corresponde ao que estiver na biblioteca à qual você está vinculando.

A maioria das bibliotecas modernas colocará guardas em torno desses cabeçalhos para que os símbolos sejam declarados com a ligação correta. Por exemplo, em muitos cabeçalhos padrão, você encontrará:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Isso garante que, quando o código C ++ inclua o cabeçalho, os símbolos no arquivo de objeto correspondam ao que está na biblioteca C. Você só deve colocar "C" externo {} em volta do cabeçalho C, se for antigo e ainda não tiver esses guardas.


22

No C ++, você pode ter diferentes entidades que compartilham um nome. Por exemplo, aqui está uma lista de funções todas nomeadas foo :

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

Para diferenciar entre todos, o compilador C ++ criará nomes exclusivos para cada um em um processo chamado decoração ou manipulação de nome. Compiladores C não fazem isso. Além disso, cada compilador C ++ pode fazer isso de uma maneira diferente.

extern "C" diz ao compilador C ++ para não executar nenhuma manipulação de nome no código entre chaves. Isso permite que você chame funções C de dentro do C ++.


14

Isso tem a ver com a maneira como os diferentes compiladores executam a manipulação de nomes. Um compilador C ++ manipulará o nome de um símbolo exportado do arquivo de cabeçalho de uma maneira completamente diferente do que um compilador C, portanto, ao tentar vincular, você receberá um erro no vinculador dizendo que faltam símbolos.

Para resolver isso, dizemos ao compilador C ++ para executar no modo "C", para que ele execute a troca de nomes da mesma maneira que o compilador C faria. Feito isso, os erros do vinculador foram corrigidos.


11

C e C ++ têm regras diferentes sobre nomes de símbolos. Os símbolos são como o vinculador sabe que a chamada para a função "openBankAccount" em um arquivo de objeto produzido pelo compilador é uma referência àquela função que você chamou "openBankAccount" em outro arquivo de objeto produzido a partir de um arquivo de origem diferente pelo mesmo (ou compatível) compilador. Isso permite que você crie um programa com mais de um arquivo de origem, o que é um alívio ao trabalhar em um projeto grande.

Em C, a regra é muito simples, todos os símbolos estão em um único espaço de nome. Portanto, o número inteiro "meias" é armazenado como "meias" e a função count_socks é armazenada como "count_socks".

Os vinculadores foram criados para C e outros idiomas como C com esta regra simples de nomeação de símbolos. Portanto, os símbolos no vinculador são apenas sequências simples.

Porém, em C ++, a linguagem permite que você tenha espaços para nome, polimorfismo e várias outras coisas que conflitam com uma regra tão simples. Todas as seis funções polimórficas chamadas "adicionar" precisam ter símbolos diferentes ou a incorreta será usada por outros arquivos de objeto. Isso é feito "confundindo" (que é um termo técnico) os nomes dos símbolos.

Ao vincular o código C ++ a bibliotecas ou código C, você precisa de "C" externo escrito em C, como arquivos de cabeçalho para as bibliotecas C, para informar ao seu compilador C ++ que esses nomes de símbolo não devem ser confundidos, enquanto o restante do é claro que seu código C ++ deve ser confundido ou não funcionará.


11

Quando devemos usá-lo?

Quando você está vinculando bibliotecas C em arquivos de objeto C ++

O que está acontecendo no nível do compilador / vinculador que exige que o usemos?

C e C ++ usam esquemas diferentes para nomear símbolos. Isso informa ao vinculador para usar o esquema de C ao vincular na biblioteca especificada.

Como, em termos de compilação / vinculação, isso resolve os problemas que exigem que nós a usemos?

O uso do esquema de nomenclatura C permite fazer referência a símbolos no estilo C. Caso contrário, o vinculador tentaria símbolos no estilo C ++ que não funcionariam.


7

Você deve usar "C" externo sempre que incluir um cabeçalho que defina funções que residem em um arquivo compilado por um compilador C, usado em um arquivo C ++. (Muitas bibliotecas C padrão podem incluir essa verificação em seus cabeçalhos para simplificar para o desenvolvedor)

Por exemplo, se você tiver um projeto com 3 arquivos, util.c, util.he main.cpp e os arquivos .c e .cpp forem compilados com o compilador C ++ (g ++, cc, etc), então não será ' realmente necessário e pode até causar erros no vinculador. Se o seu processo de construção usa um compilador C regular para o util.c, você precisará usar o "C" externo ao incluir o utilitário.h.

O que está acontecendo é que o C ++ codifica os parâmetros da função em seu nome. É assim que a sobrecarga de funções funciona. Tudo o que tende a acontecer com uma função C é a adição de um sublinhado ("_") ao início do nome. Sem usar extern "C", o vinculador procurará uma função chamada DoSomething @@ int @ float () quando o nome real da função for _DoSomething () ou apenas DoSomething ().

O uso externo de "C" resolve o problema acima, informando ao compilador C ++ que ele deve procurar uma função que siga a convenção de nomenclatura C em vez da convenção C ++.


7

O compilador C ++ cria nomes de símbolos de maneira diferente do compilador C. Portanto, se você estiver tentando fazer uma chamada para uma função que reside em um arquivo C, compilado como código C, precisará informar ao compilador C ++ que o nome dos símbolos que ele está tentando resolver tem uma aparência diferente da padrão; caso contrário, a etapa do link falhará.


6

A extern "C" {}construção instrui o compilador a não executar a manipulação de nomes declarados entre chaves. Normalmente, o compilador C ++ "aprimora" os nomes das funções para codificar informações de tipo sobre argumentos e o valor de retorno; isso é chamado de nome mutilado . A extern "C"construção evita a distorção.

É normalmente usado quando o código C ++ precisa chamar uma biblioteca da linguagem C. Também pode ser usado ao expor uma função C ++ (de uma DLL, por exemplo) a clientes C.


5

Isso é usado para resolver problemas de confusão de nomes. C externo significa que as funções estão em uma API de estilo C "plana".


0

Descompile um g++binário gerado para ver o que está acontecendo

Para entender por que externé necessário, a melhor coisa a fazer é entender o que está acontecendo em detalhes nos arquivos de objeto com um exemplo:

main.cpp

void f() {}
void g();

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

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

Compile com a saída ELF do GCC 4.8 Linux :

g++ -c main.cpp

Descompile a tabela de símbolos:

readelf -s main.o

A saída contém:

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

Interpretação

Nós vemos que:

  • efe egforam armazenados em símbolos com o mesmo nome que no código

  • os outros símbolos estavam mutilados. Vamos desmembrá-los:

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

Conclusão: os dois tipos de símbolos a seguir não foram mutilados:

  • definiram
  • declarado mas indefinido ( Ndx = UND), a ser fornecido no link ou no tempo de execução de outro arquivo de objeto

Então, você precisará de extern "C"ambos ao ligar:

  • C de C ++: diga g++para esperar símbolos não manipulados produzidos porgcc
  • C ++ de C: diga g++para gerar símbolos não manipulados para gccusar

Coisas que não funcionam no externo C

Torna-se óbvio que qualquer recurso do C ++ que exija a troca de nomes não funcionará dentro 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) { }
}

Exemplo de C executável mínimo de C ++

Para fins de integridade e novidades, consulte também: Como usar arquivos de origem C em um projeto C ++?

Chamar C a partir de C ++ é bastante fácil: cada função C possui apenas um símbolo não-mutilado possível, portanto, nenhum trabalho extra é necessário.

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

Corre:

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

Sem extern "C"o link falha com:

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

porque g++espera encontrar um mutilado f, que gccnão produziu.

Exemplo no GitHub .

Exemplo C ++ executável mínimo do exemplo C

Chamar C ++ é um pouco mais difícil: precisamos criar manualmente versões não-mutiladas de cada função que queremos expor.

Aqui ilustramos como expor sobrecargas da função 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);
}

Corre:

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

Sem extern "C"ele falha com:

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

porque g++gerou símbolos mutilados que gccnão podem ser encontrados.

Exemplo no GitHub .

Testado no Ubuntu 18.04.


1
Obrigado por explicar o voto negativo, tudo faz sentido agora.
Ciro Santilli escreveu:
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.