C ++中extern“ C”的作用是什么?


1629

extern "C"放入C ++代码到底能做什么?

例如:

extern "C" {
   void foo();
}

82
我想向您介绍这篇文章:http : //www.agner.org/optimize/calling_conventions.pdf它告诉您更多有关调用约定以及编译器之间的区别的信息。
山姆廖

1
@Litherum在我的头上,它告诉编译器使用C编译该代码范围,前提是您有一个交叉编译器。同样,这意味着您拥有一个具有该foo()功能的Cpp文件。
ha9u63ar 2013年

1
@ ha9u63ar它“不在我的头上”。您评论的其余全部内容也不正确。我建议您删除它。
TamaMcGlinn

Answers:


1555

extern“ C”使C ++中的函数名称具有'C'链接(编译器不会更改名称),以便客户端C代码可以使用兼容'C'的头文件链接到(即使用)您的函数,该头文件仅包含函数的声明。函数定义以二进制格式(由C ++编译器编译)包含,客户端“ C”链接器随后将使用“ C”名称链接至该二进制文件。

由于C ++具有重载的函数名,而C没有重载,因此C ++编译器不能仅使用函数名作为要链接的唯一ID,因此它通过添加有关自变量的信息来破坏名称。AC编译器不需要处理名称,因为您不能在C中重载函数名称。当您声明函数在C ++中具有外部“ C”链接时,C ++编译器不会将参数/参数类型信息添加到用于连锁。

众所周知,您可以显式地为每个单独的声明/定义指定“ C”链接,也可以使用一个块对一系列声明/定义进行分组以具有一定的链接:

extern "C" void foo(int);
extern "C"
{
   void g(char);
   int i;
}

如果您关心技术方面的问题,它们会在C ++ 03标准的7.5节中列出,此处是一个简短的摘要(着重于外部“ C”):

  • extern“ C”是一个链接规范
  • 每个编译器都需要提供“ C”链接
  • 链接规范应仅在命名空间范围内发生
  • 所有函数类型,函数名称和变量名称都有语言链接 请参阅Richard的注释:只有具有外部链接的函数名称和变量名称才具有语言链接。
  • 即使语言相同,具有不同语言链接的两个函数类型也都是不同类型
  • 链接规格嵌套,内部决定最终的链接
  • 类成员忽略外部“ C”
  • 最多一个具有特定名称的函数可以具有“ C”链接(无论名称空间如何)
  • extern“ C”强制一个函数具有外部链接(不能使其静态) 参见Richard的评论: 'extern“ C”'内部的'static'有效;如此声明的实体具有内部链接,因此没有语言链接
  • 从C ++到其他语言定义的对象以及从其他语言到C ++定义的对象的链接是实现定义的和语言相关的。只有两种语言实现的对象布局策略足够相似时,才能实现这种链接

21
C编译器不使用c ++那样的处理。因此,如果要从c ++程序调用ac接口,则必须明确声明c接口为“ extern c”。
山姆廖

58
@Faisal:即使交叉引用都是'extern“ C”,也不要尝试链接使用不同的C ++编译器构建的代码。在类的布局之间,或者在用于处理异常的机制之间,或者在使用前确保变量被初始化的机制之间,通常存在差异,或者其他类似的差异,另外,您可能需要两个单独的C ++运行时支持库(一个用于每个编译器)。
乔纳森·莱夫勒

8
'extern“ C”强制函数具有外部链接(无法使其静态)'不正确。'extern“ C”'内部的'static'有效;如此声明的实体具有内部链接,因此没有语言链接。
理查德·史密斯

14
“所有函数类型,函数名称和变量名称都有语言链接”也是不正确的。只有具有外部链接的函数名称和变量名称才具有语言链接。
理查德·史密斯

9
请注意,这extern "C" { int i; }是一个定义。在未定义的旁边,这可能不是您想要的void g(char);。要使其不明确,您将需要extern "C" { extern int i; }。另一方面,不带花括号的单声明语法确实使声明成为extern "C" int i;extern "C" { extern int i; }
未定义

327

只是想添加一些信息,因为我还没有看到它发布的信息。

您会经常在C标头中看到如下代码:

#ifdef __cplusplus
extern "C" {
#endif

// all of your legacy C code here

#ifdef __cplusplus
}
#endif

这样做是因为它允许您将C头文件与C ++代码一起使用,因为将定义宏“ __cplusplus”。但是你可以仍然使用旧的C代码,其中宏使用NOT定义,所以它不会看到独特的C ++结构。

虽然,我也看到过C ++代码,例如:

extern "C" {
#include "legacy_C_header.h"
}

我想可以完成几乎相同的事情。

不知道哪种方法更好,但是我都看过。


11
有明显的区别。对于前者,如果使用普通的gcc编译器编译该文件,它将生成一个对象,该对象的函数名称不完整。如果随后使用链接器链接C和C ++对象,它将找不到函数。与第二个代码块一样,您将需要在extern关键字中包含那些“旧标题”文件。
Anne van Rossum 2013年

8
@Anne:C ++编译器还将查找未修改的名称,因为它extern "C"在标头中看到了)。效果很好,多次使用了此技术。
Ben Voigt 2014年

20
@安妮:那是不对的,第一个也很好。它被C编译器忽略,并且具有与C ++中的第二个相同的效果。编译器不会在乎是否extern "C"在包含标头之前或之后遇到它。到编译器时,它只是一长串预处理文本。
Ben Voigt 2014年

8
@Anne,不,我认为您已经受到来源中其他错误的影响,因为您所描述的内容是错误的。g++至少在过去的17年中,没有任何版本的任何目标都犯错。第一个示例的全部要点是,无论使用C还是C ++编译器都无关紧要,不会对extern "C"块中的名称进行名称修饰。
Jonathan Wakely

7
“哪个更好”-当然,第一个变种更好:它允许在C和C ++代码中直接包含标头,而无任何其他要求。第二种方法是作者遗忘了C ++防护措施的C标头的解决方法(不过,如果以后添加这些防护,则可以接受嵌套的extern“ C”声明...)。
阿空加瓜

267

反编译g++生成的二进制文件以查看发生了什么

main.cpp

void f() {}
void g();

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

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

编译和反汇编生成的ELF输出:

g++ -c -std=c++11 -Wall -Wextra -pedantic -o main.o main.cpp
readelf -s main.o

输出包含:

     8: 0000000000000000     7 FUNC    GLOBAL DEFAULT    1 _Z1fv
     9: 0000000000000007     7 FUNC    GLOBAL DEFAULT    1 ef
    10: 000000000000000e    17 FUNC    GLOBAL DEFAULT    1 _Z1hv
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z1gv
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND eg

解释

我们看到:

  • efeg以与代码中相同的名称存储在符号中

  • 其他符号被重整。让我们解开它们:

    $ 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中不起作用的东西

显而易见,任何需要名称修饰的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非常容易:每个C函数只有一个可能的非分解符号,因此不需要额外的工作。

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++ 
 * because C does not know what this extern "C" thing is. */
#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 ++函数重载公开给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中测试。


21
自您以来的最佳答案:1)明确提到可以extern "C" {帮助您从C ++程序内部调用无损C函数,以及从C程序内部调用无损C ++函数,其他答案并不那么明显,以及 2)因为您展示了以下示例每。谢谢!
加布里埃尔·斯台普斯

3
我非常喜欢这个答案
selfboot '19

4
给出最佳答案,因为它显示了如何从c调用重载函数
Gaspa79 '19

1
@JaveneCPPMcGowan是什么让您觉得我有C ++老师?:-)
Ciro Santilli冠状病毒审查六四事件法轮功

205

在每个C ++程序中,所有非静态函数在二进制文件中均以符号表示。这些符号是特殊的文本字符串,可唯一标识程序中的功能。

在C语言中,符号名称与函数名称相同。这是可能的,因为在C中,没有两个非静态函数可以具有相同的名称。

因为C ++允许重载,并且具有C所不允许的许多功能(例如类,成员函数,异常规范),所以不可能简单地将函数名用作符号名。为了解决这个问题,C ++使用了所谓的名称修饰,将函数名称和所有必要的信息(例如参数的数量和大小)转换为仅由编译器和链接器处理的看起来很奇怪的字符串。

因此,如果将函数指定为extern C,则编译器不会对其进行名称处理,并且可以使用其符号名作为函数名称直接对其进行访问。

这在使用dlsym()dlopen()调用此类函数时非常方便。


方便是什么意思?是符号名称=函数名称会使传递给dlsym的符号名称已知,还是其他?
错误

1
@错误:是的。在通常情况下,仅给定头文件并选择正确的函数来加载dlopen()一个C ++共享库基本上是不可能的。(在x86上,已经以Itanium ABI的形式发布了名称处理规范,我所知道的所有x86编译器都用于处理C ++函数名,但是在该语言中没有任何要求。)
Jonathan Tomer

52

C ++修改函数名称以从过程语言创建面向对象的语言

大多数编程语言都不是在现有编程语言之上构建的。C ++是在C的基础上构建的,而且它是从过程编程语言构建的一种面向对象的编程语言,因此,存在诸如extern "C",它们提供与C的向后兼容性。

让我们看下面的例子:

#include <stdio.h>

// Two functions are defined with the same name
// but have different parameters

void printMe(int a) {
  printf("int: %i\n", a);
}

void printMe(char a) {
  printf("char: %c\n", a);
}

int main() {
  printMe("a");
  printMe(1);
  return 0;
}

AC编译器不会编译上面的示例,因为相同的函数printMe定义了两次(即使它们的参数int avs 也不相同char a)。

gcc -o printMe printMe.c && ./printMe;
1个错误。PrintMe被多次定义。

C ++编译器将编译以上示例。它不在乎printMe定义两次。

g ++ -o printMe printMe.c && ./printMe;

这是因为C ++编译器会根据其参数隐式重命名(mangles)函数。在C语言中,不支持此功能。但是,当在C之上构建C ++时,该语言被设计为面向对象的,并且需要支持使用相同名称的方法(函数)创建不同的类以及基于不同的方法覆盖方法(方法覆盖)的能力。参数。

extern "C" 说“不要破坏C函数名称”

但是,假设我们有一个名为“ parent.c” include的遗留C文件,该文件具有其他遗留C文件的函数名“ parent.h”,“ child.h”等。如果运行了遗留“ parent.c”文件通过C ++编译器,则函数名称将被破坏,并且它们将不再与“ parent.h”,“ child.h”等中指定的函数名称匹配-因此,这些外部文件中的函数名称也需要被弄坏了。在复杂的C程序中处理函数名称(具有很多依赖性)会导致代码损坏;因此提供一个可以告诉C ++编译器不要破坏函数名的关键字可能会很方便。

extern "C"关键字告诉C ++编译器不轧液机(重命名)C函数名。

例如:

extern "C" void printMe(int a);


extern "C"如果只有dll文件,我们可以不使用吗?我的意思是,如果我们没有头文件,而只有源文件(只是实现),并通过函数指针使用其功能。在这种状态下,我们只使用了函数(无论其名称如何)。
BattleTested

@tfmontague,对我来说,你把它钉对了!直在头上。
Artanis Zeratul '18

29

仅包装在外部“ C”中,就不能使任何C头与C ++兼容。当C头中的标识符与C ++关键字冲突时,C ++编译器将对此进行投诉。

例如,我看到以下代码在g ++中失败:

extern "C" {
struct method {
    int virtual;
};
}

Kinda很有道理,但是在将C代码移植到C ++时要牢记。


14
extern "C"如其他答案所述,使用C链接的方式。这并不意味着“将内容编译为C”或任何东西。int virtual;在C ++中是无效的,并且指定其他链接不会改变这一点。
MM

1
...或模式,任何包含语法错误的代码都不会编译。
Valentin Heinitz '17

4
@ValentinHeinitz自然,尽管在C中使用“虚拟”作为标识符不是语法错误。我只是想指出,您不能通过在外部加上“ C”来自动使用C ++中的任何 C标头。
Sander Mertens



12

extern“ C”旨在由C ++编译器识别,并通知编译器所注明的功能已(或将要)以C样式进行编译。这样,在链接时,它会链接到C的正确版本的函数。


6

我在dll(动态链接库)文件中使用过“ extern“ C””来制作etc. main()函数“ exportable”,因此以后可以在dll中的另一个可执行文件中使用它。也许我曾经在哪里使用它的示例可能会有用。

动态链接库

#include <string.h>
#include <windows.h>

using namespace std;

#define DLL extern "C" __declspec(dllexport)
//I defined DLL for dllexport function
DLL main ()
{
    MessageBox(NULL,"Hi from DLL","DLL",MB_OK);
}

可执行程序

#include <string.h>
#include <windows.h>

using namespace std;

typedef LPVOID (WINAPI*Function)();//make a placeholder for function from dll
Function mainDLLFunc;//make a variable for function placeholder

int main()
{
    char winDir[MAX_PATH];//will hold path of above dll
    GetCurrentDirectory(sizeof(winDir),winDir);//dll is in same dir as exe
    strcat(winDir,"\\exmple.dll");//concentrate dll name with path
    HINSTANCE DLL = LoadLibrary(winDir);//load example dll
    if(DLL==NULL)
    {
        FreeLibrary((HMODULE)DLL);//if load fails exit
        return 0;
    }
    mainDLLFunc=(Function)GetProcAddress((HMODULE)DLL, "main");
    //defined variable is used to assign a function from dll
    //GetProcAddress is used to locate function with pre defined extern name "DLL"
    //and matcing function name
    if(mainDLLFunc==NULL)
    {
        FreeLibrary((HMODULE)DLL);//if it fails exit
        return 0;
    }
    mainDLLFunc();//run exported function 
    FreeLibrary((HMODULE)DLL);
}

4
虚假。extern "C"__declspec(dllexport)无关。前者控制符号装饰,后者负责创建出口条目。您也可以使用C ++名称修饰导出符号。除了完全忽略此问题的要点之外,代码示例中还存在其他错误。首先,main从您的DLL导出不会声明返回值。或致电约定,就此而言。导入时,您应为随机调用约定(WINAPI)赋予属性,并对32位版本使用错误的符号(应为_main_main@0)。对不起,-1。
IInspectable'9

1
只是重复(您不知道)您在做什么,但是对于某些未公开的目标平台列表,以这种方式执行似乎对您有用。您没有解决我在之前的评论中提出的问题。由于存在严重错误,这仍然是一个不赞成的投票(还有很多,这些都不能在单个评论中找到)。
IInspectable '17

1
在Stack Overflow上发布答案暗示着您知道自己在做什么。这是预期的。至于您尝试“防止运行中的堆栈损坏”:您的函数签名指定了type的返回值void*,但是您的实现未返回任何内容。那真的会飞的很好...
IInspectable '17

1
如果实施某件事,似乎是靠运气,那显然是行得通的,那么您显然知道自己在做什么(您的“工作”样本属于该类别)。这是未定义的行为,并且看起来有效是未定义行为的有效形式。仍然没有定义。如果您日后更加努力,我将不胜感激。部分原因可能是删除了此提议的答案。
IInspectable '17

1
您正在将不返回任何内容的函数重新解释为返回指针的函数。真幸运,x86对于不匹配的函数签名,尤其是整数类型的返回值,非常宽容。您的代码仅通过巧合起作用。如果您不同意,则需要解释为什么代码可以可靠地工作。
IInspectable '17

5

extern "C"是一个链接规范,用于调用Cpp源文件中的C函数。我们可以调用C函数,编写变量和包含标头。函数在外部实体中声明,并且在外部定义。语法是

类型1:

extern "language" function-prototype

类型2:

extern "language"
{
     function-prototype
};

例如:

#include<iostream>
using namespace std;

extern "C"
{
     #include<stdio.h>    // Include C Header
     int n;               // Declare a Variable
     void func(int,int);  // Declare a function (function prototype)
}

int main()
{
    func(int a, int b);   // Calling function . . .
    return 0;
}

// Function definition . . .
void func(int m, int n)
{
    //
    //
}

3

这个答案是针对急躁的/有最后期限的人,下面仅是部分/简单的解释:

  • 在C ++中,您可以通过重载在类中使用相同的名称(例如,由于它们都是相同的名称,因此无法从dll等中按原样导出)。解决这些问题的方法是将它们转换为不同的字符串(称为符号) ),符号说明了函数的名称以及参数,因此即使具有相同名称,每个函数也可以唯一标识(也称为名称修改)
  • 在C中,您没有重载,函数名是唯一的(因此,不需要用于唯一标识函数名的单独字符串,因此,符号本身就是函数名)

因此,
在C ++
中,即使名称没有处理每个函数的唯一标识,名称处理中的每个函数都具有唯一标识

要更改C ++的行为,即指定不应该对特定函数进行名称修饰,无论出于何种原因,都可以在函数名称前使用extern“ C”,例如从dll中导出具有特定名称的函数。 ,供其客户使用。

阅读其他答案,以获得更详细/更正确的答案。


1

当混合使用C和C ++时(即a。从C ++调用C函数;和b。从C调用C ++函数),C ++名称修改会导致链接问题。从技术上讲,仅当已使用相应的编译器将被调用方函数编译为二进制(很可能是* .a库文件)时,才会发生此问题。

因此,我们需要使用extern“ C”来禁用C ++中的名称修饰。


0

在不与其他良好答案冲突的情况下,我将添加一些示例。

到底是什么 C ++编译器的是:它会在编译过程中弄乱名称,因此我们需要告诉编译器特别对待 C实现。

当我们制作C ++类并添加时extern "C",我们告诉C ++编译器我们正在使用C调用约定。

原因(我们正在从C ++调用C实现): 要么我们想从C ++调用C函数,要么从C调用C ++函数(C ++类...等在C语言中不起作用)。


欢迎使用堆栈溢出。如果您决定回答一个已经建立并正确答案的较早的问题,那么在当天晚些时候添加一个新答案可能不会给您任何功劳。如果您有一些与众不同的新信息,或者您确信其他答案都是错误的,则一定要添加一个新答案,但是“又一个答案”在问了很长时间后给出相同的基本信息通常是不会的。不能赚很多钱。坦白说,我认为这个答案没有什么新意。
乔纳森·莱夫勒

好吧,我应该记住您的观点-好的
Susobhan Das

-1

C编译器编译的函数void f()和C ++编译器编译的同名函数void f()是不同的函数。如果您使用C编写了该函数,然后尝试从C ++调用它,则链接器将查找C ++函数,但找不到C函数。

extern“ C”告诉C ++编译器您有一个由C编译器编译的函数。一旦告诉它它是由C编译器编译的,则C ++编译器将知道如何正确调用它。

它还允许C ++编译器以C编译器可以调用它的方式来编译C ++函数。该函数正式是C函数,但是由于它是由C ++编译器编译的,因此可以使用所有C ++功能并具有所有C ++关键字。


C ++编译器可以编译extern "C"函数-并且(受某些约束)它可以由C编译器编译的代码调用。
乔纳森·勒夫勒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.