在C++中,extern "C"有什么作用?

2130

extern "C"在C++代码中的作用是什么?

例如:

extern "C" {
   void foo();
}

115
我想向你介绍这篇文章:http://www.agner.org/optimize/calling_conventions.pdf 它会告诉你更多关于调用约定以及编译器间的差异。 - Sam Liao
18个回答

1979

extern "C" 使得 C++ 中的函数名具有 C 语言链接(编译器不会对名称进行重整),因此客户端 C 代码可以使用一个仅包含您函数声明的 C 兼容头文件来链接(使用)您的函数。您的函数定义包含在二进制格式中(由您的 C ++ 编译器编译),客户端 C 链接器将使用该 C 名称进行链接。

由于 C++ 具有函数名重载功能,而 C 不支持,因此 C++ 编译器无法仅使用函数名作为唯一 ID 进行链接,因此它通过添加关于参数的信息对名称进行重整。C 编译器不需要重整名称,因为在 C 中不能重载函数名。当您在 C++ 中声明具有 extern "C" 链接时,C++ 编译器不会向用于链接的名称添加参数/参数类型信息。

只需知道,您可以明确指定每个声明/定义的 extern "C" 链接,也可以使用一个块将一系列声明/定义分组以具有特定的链接:

extern "C" void foo(int);
extern "C"
{
   void g(char);
   int i;
}
如果你关心技术细节,它们在C++03标准的第7.5节中列出,这里是一个简要概述(重点在于extern "C"):
  • extern "C" 是一种链接说明
  • 每个编译器都必须提供"C"链接
  • 链接说明只能出现在命名空间范围内
  • 所有函数类型、函数名称和变量名称都有一种语言链接 请参见Richard的评论:只有具有外部链接的函数名称和变量名称才有语言链接
  • 具有不同语言链接的两种函数类型即使其他方面相同也是不同的类型
  • 链接说明可以嵌套,内部说明决定最终链接
  • extern "C" 对类成员无效
  • 一个特定名称的函数最多只能具有"C"链接(无论命名空间如何)
  • extern "C" 强制函数具有外部链接(不能将其设置为static) 请参见Richard的评论: extern "C"中的static是有效的。这样声明的实体具有内部链接,因此没有语言链接
  • C++到其他语言定义的对象以及从其他语言定义的C++对象的链接是实现定义的,并且依赖于语言。只有两个语言实现的对象布局策略足够相似时,才能实现这种链接。

39
C编译器不使用C++所使用的名称修饰,因此如果您想从C++程序调用C接口,您必须明确声明该C接口为"extern c"。请注意,这不会改变原来的意思,只是使之更加通俗易懂。 - Sam Liao
70
不要试图将使用不同C++编译器构建的代码连接起来,即使交叉引用都是“extern C”。类的布局、处理异常的机制、确保变量在使用前初始化的机制或其他类似差异经常存在,此外,您可能需要两个单独的C++运行时支持库(每个编译器一个)。 - Jonathan Leffler
18
"'extern "C"'的作用是让函数具有外部链接性(无法使其为static)。"这种说法是不正确的。在'extern "C"'内部使用'static'是合法的,这样声明的实体具有内部链接性,因此不具有语言链接性。" - Richard Smith
21
“所有函数类型、函数名和变量名都具有语言链接”这个说法是不正确的。只有具有外部链接的函数名和变量名才具有语言链接。 - Richard Smith
19
请注意,extern "C" { int i; } 是一个定义。这可能不是您的意图,因为void g(char);是非定义的。要使其成为非定义,您需要使用extern "C" { extern int i; }。另一方面,省略括号的单声明语法确实使声明成为非定义:extern "C" int i;extern "C" { extern int i; } 相同。 - aschepler
显示剩余14条评论

439

我想补充一些信息,因为我还没有看到它被发布过。

你经常会在C头文件中看到如下的代码:

#ifdef __cplusplus
extern "C" {
#endif

// all of your legacy C code here

#ifdef __cplusplus
}
#endif

这样做的好处是可以让你在C++代码中使用那个C头文件,因为宏__cplusplus会被定义。但你也可以在遗留的C代码中使用它,因为宏没有被定义,所以它将不会看到独有的C++结构。

虽然我也见过像这样的C++代码:

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

我想这样做会达到同样的效果。

不确定哪种方法更好,但我都见过。


13
有一个明显的区别。在前一种情况下,如果您使用普通的gcc编译器编译此文件,它将生成一个函数名称未被混淆的目标文件。如果您随后使用链接器链接C和C++对象,它将找不到这些函数。您需要像第二个代码块中那样在“遗留头文件”中包含extern关键字。 - Anne van Rossum
13
@Anne: C++编译器也会查找未编码的名称,因为它在头文件中看到了extern "C"。这种技术非常好用,我已经用过很多次了。 - Ben Voigt
35
@Anne:那不对,第一个也可以。在C语言中,它被C编译器忽略,和第二个一样。编译器无论在包含头文件之前还是之后遇到extern "C"都不会有所区别。在它传送给编译器之前,已经被预处理成了一个长字符串。 - Ben Voigt
11
@Anne,不,我认为你受到了源代码中其他错误的影响,因为你所描述的是错误的。至少在过去17年中的任何时间,没有任何版本的g++会在任何目标上出现这种错误。第一个示例的整个重点在于,无论您使用C还是C ++编译器,都不会对extern“C”块中的名称进行名称修饰。 - Jonathan Wakely
9
“哪一个更好” - 毫无疑问,第一种方式更好:它允许在C和C++代码中直接包含头文件,不需要任何进一步的要求。第二种方法是对于作者忘记添加C++ guards的C头文件的一种解决办法(不过,如果之后添加这些,嵌套的extern“ C”声明是被接受的...)。 - Aconcagua
显示剩余8条评论

414
反编译由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要使用的未经重整的符号

在extern 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函数只有一个可能的非重载符号,因此不需要额外的工作。

main.cpp

#include <cassert>

#include "c.h"

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

c.h

#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

c.c

#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 上的示例

C 头文件在 C++ 中被包含时 extern "c" 在哪里?

  • C++ 版本的 C 标准库头文件(比如 cstdio)可能依赖于 #pragma GCC system_header,这个链接:https://gcc.gnu.org/onlinedocs/cpp/System-Headers.html 描述道:“对于一些目标平台(例如 RS/6000 AIX),GCC 在以 C++ 编译时隐式地用 'extern "C"' 块围绕所有系统头文件。”,但我没有完全确认过。
  • POSIX 标准库头文件(比如 /usr/include/unistd.h)通过 __BEGIN_DECLS 在 Ubuntu 20.04 上被覆盖,具体内容可以查看这个链接:Do I need an extern "C" block to include standard POSIX C headers?。这个宏通过 #include <features.h> 包含。

在 Ubuntu 18.04 上测试通过。


52
由于您明确提到extern "C" {可以帮助您在C++程序中调用未被mangled的C函数,并且在C程序中调用未被mangled的C ++函数,而其他答案没有这么明显,同时您还给出了每种情况的不同示例,因此这是最佳答案。谢谢! - Gabriel Staples
1
我想了解一下C头文件,例如unistd.h、sys/stat.h和sys/types.h。它们似乎没有在“extern”后面加上“'C'”。从C++代码中使用它们似乎仍然没有问题。这是因为它们是纯头文件而没有实现文件的原因吗? - paleonix
1
@Paul,他们似乎使用宏__BEGIN_DECLS启用了extern C:https://dev59.com/questions/HWsz5IYBdhLWcg3wJUjJ#8087539 我在Ubuntu 20.04上观察到了那个答案中提到的内容,适用于unistd.h。然而,对于cstdio,它可能依赖于#pragma GCC system_header:https://gcc.gnu.org/onlinedocs/cpp/System-Headers.html - Ciro Santilli OurBigBook.com
谢谢!奇怪的是,当我搜索时,这个问题没有出现,但当我搜索特定的宏时,它出现了...我想把它链接在这里也是好事。由于__BEGIN_DECLS在sys/cdefs.h中定义,但在unistd.h、sys/stat.h和sys/types.h中都没有包含,我猜测sys/cdefs.h只是默认情况下被预处理器包含? - paleonix
@Paul 不用担心,我们都要顺应 Google 上帝的意愿而生存和死亡。它通过 #include <features.h> 包含进来。 - Ciro Santilli OurBigBook.com
好的,我想我已经看够了一段时间的POSIX/系统头文件了... - paleonix

224
在每个C++程序中,所有非静态函数都作为符号表示在二进制文件中。这些符号是特殊的文本字符串,用于唯一标识程序中的函数。
在C语言中,符号名与函数名相同。这是因为在C语言中,没有两个非静态函数可以具有相同的名称。
由于C++允许重载并且具有许多C语言不具备的功能,例如类,成员函数和异常规格,因此不可能仅使用函数名作为符号名。为解决这个问题,C++使用所谓的名称修饰(name mangling),将函数名和所有必要信息(例如参数的数量和大小)转换为一些只有编译器和链接器才能处理的奇怪字符串。
因此,如果您指定一个函数为extern C,则编译器不对其进行名称修饰,并且可以直接使用它的符号名称作为函数名访问。
在使用dlsym()和dlopen()调用这些函数时,这非常方便。

“Handy” 是什么意思?符号名称 = 函数名称会使得传递给 dlsym 的符号名称被识别,还是其他的事情? - Error
3
@错误:是的。通常情况下,仅凭头文件就无法dlopen()C ++共享库并选择正确的函数进行加载。(在x86上,有一个名为Itanium ABI的发布名称编码规范,所有我知道的x86编译器都使用它来编码C ++函数名称,但语言中没有任何要求这样做。) - Jonathan Klabunde Tomer

97

C++通过编译时名称重整,将过程式语言变成面向对象的语言

大多数编程语言并非在现有编程语言之上构建而成。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;
}

因为上述示例中定义了相同名称的函数printMe,虽然它们有不同的参数int achar a,所以C编译器将无法编译。

gcc -o printMe printMe.c && ./printMe;
1 error. PrintMe is defined more than once.

但是,C++编译器可以编译上述示例。它并不在乎printMe被定义了两次。

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

这是因为C++编译器会根据参数名隐式地为函数进行重命名(mangles)。该语言旨在创建具有相同名称的方法(函数)的不同类,并基于不同的参数来覆盖方法名(method overriding)。

extern "C"表示“不要重命名C函数名”

即使C++是基于C构建的,混淆对于C代码可能会造成麻烦。例如,假设我们有一个名为“parent.c”的遗留C文件,该文件include来自不同头文件“parent.h”、“child.h”等的函数名称。如果我们将“parent.c”通过C++编译器运行,它将混淆该文件中的函数名,并且它们将不再与头文件中指定的函数名匹配。因此,“parent.h”和“child.h”头文件中的函数名也需要进行混淆。对于少数文件,这可能没问题,但如果C程序很复杂,混淆可能会导致代码错误和执行速度变慢,因此提供一个关键字告诉C++编译器不要混淆函数名可能是方便的。

extern "C"关键字告诉C++编译器不要混淆(重命名)C函数名。

例如:

extern "C" void printMe(int a);


如果我们只有一个dll文件,没有头文件,只有源文件(仅实现),并且通过函数指针使用其函数,那么我们可以不使用extern "C"吗?在这种情况下,我们只是使用函数(而不考虑其名称)。 - BattleTested_закалённый в бою

34

仅仅通过用extern "C"来包装C头文件并不能使所有的C头文件与C++兼容。当C头文件中的标识符与C++关键字冲突时,C++编译器将会报错。

例如,我曾在g++编译器下看到以下代码无法编译通过:

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

有点合理,但在将C代码移植到C++时要记住这一点。


20
extern "C" 表示使用 C 语言链接,正如其他回答所述。它并不意味着将内容编译为 C 语言或其他任何内容。在 C++ 中,int virtual; 是无效的,并且指定不同的链接方式也无法改变这一点。 - M.M
3
通常情况下,无论是哪种编程语言的代码,只要包含语法错误,就无法编译。 - Valentin H
4
@ValentinHeinitz 自然地,虽然在C中使用“virtual”作为标识符不是语法错误。我只是想指出,您不能仅仅通过将extern "C"放在周围就自动在C++中使用任何C头文件。 - Sander Mertens
我遇到了一个不同的兼容性问题。C头文件在一些结构体的typedef中使用了struct前缀。它在gcc和clang的-Wextra下编译时没有出错或警告,但在g++和clang++下失败,因为struct只允许在原始标识符上使用,而不是其typedef上。我不得不修改头文件以使其C++兼容,除了extern "C"{...}包装器之外,现在它可以在C和C++版本上编译。 - penguin359

32

它以这样一种方式改变函数的链接方式,使得该函数可以从C中调用。实际上,这意味着函数名没有名称混淆


3
“Mangled”是通常用来描述的术语……我认为我从未见过“decorated”被用于这个意思。 - Matthew Scharley
3
微软(至少部分地)在其文档中使用“修饰名称”(decorated)而不是“符号名”(mangled)。它们甚至将其用于命名工具以使一个名称不带修饰(也称为解除符号名)的操作,即“undname”。 - René Nyffenegger

26

该指令告知C++编译器在链接时以C语言风格查找这些函数的名称,因为在链接阶段,用C和C++编译的函数的名称是不同的。


12

extern "C" 的作用是告诉 C++ 编译器该函数是以 C 语言风格编写的(或将会被编写成这种风格),以便在链接时正确地链接到 C 语言版本的函数。


7

extern "C" 是一种链接规范,用于在 C++ 源文件中调用 C 函数。我们可以调用 C 函数、编写变量和包含头文件。函数在 extern 实体中声明,但其定义在外部。语法如下:

类型 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)
{
    //
    //
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接