141

为什么我们需要使用:

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

具体来说:

  • 我们应该什么时候使用它?

  • 在需要我们使用它的编译器/链接器级别发生了什么?

  • 这在编译/链接方面如何解决需要我们使用它的问题?

4

11 回答 11

132

C 和 C++ 表面上相似,但各自编译成一组非常不同的代码。当您在 C++ 编译器中包含头文件时,编译器需要 C++ 代码。但是,如果它是 C 头文件,则编译器期望头文件中包含的数据被编译成某种格式——C++ 'ABI' 或“应用程序二进制接口”,因此链接器会阻塞。这比将 C++ 数据传递给需要 C 数据的函数更可取。

(要深入了解细节,C++ 的 ABI 通常会“破坏”它们的函数/方法的名称,因此printf()在不将原型标记为 C 函数的情况下调用,C++ 实际上会生成代码调用_Zprintf,最后加上额外的废话。 )

所以:extern "C" {...}在包含 ac 标头时使用——就这么简单。否则,您将在编译的代码中出现不匹配,并且链接器会阻塞。然而,对于大多数头文件,您甚至不需要,extern因为大多数系统 C 头文件已经说明了它们可能包含在 C++ 代码中并且已经包含在extern "C"它们的代码中的事实。

于 2008-09-15T23:27:38.867 回答
114

extern "C" 确定生成的目标文件中的符号应如何命名。如果函数声明时没有 extern "C",则目标文件中的符号名称将使用 C++ 名称修饰。这是一个例子。

给定 test.C 像这样:

void foo() { }

在目标文件中编译和列出符号给出:

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

foo 函数实际上称为“_Z3foov”。此字符串包含返回类型和参数的类型信息等。如果您改为这样编写 test.C :

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

然后编译并查看符号:

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

你得到C链接。目标文件中“foo”函数的名称只是“foo”,它没有来自名称修饰的所有花哨的类型信息。

如果附带的代码是使用 C 编译器编译的,但您尝试从 C++ 调用它,则通常在 extern "C" {} 中包含一个标头。当你这样做时,你告诉编译器头文件中的所有声明都将使用 C 链接。当您链接代码时,您的 .o 文件将包含对“foo”的引用,而不是“_Z3fooblah”,它希望与您链接的库中的任何内容相匹配。

大多数现代库都会在此类标头周围设置保护措施,以便使用正确的链接声明符号。例如,在许多标准标题中,您会发现:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

这确保当 C++ 代码包含标头时,目标文件中的符号与 C 库中的符号匹配。如果 C 标头很旧并且还没有这些保护,则您只需要在 C 标头周围放置 extern "C" {}。

于 2008-09-15T23:38:54.987 回答
23

在 C++ 中,您可以拥有共享名称的不同实体。例如,这里是一个全名为foo的函数列表:

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

为了区分它们,C++ 编译器将在称为名称修饰或装饰的过程中为每个创建唯一名称。C 编译器不这样做。此外,每个 C++ 编译器执行此操作的方式可能不同。

extern "C" 告诉 C++ 编译器不要对大括号内的代码执行任何名称修改。这允许您从 C++ 中调用 C 函数。

于 2008-09-15T23:27:51.597 回答
14

它与不同编译器执行名称修饰的方式有关。C++ 编译器将以与 C 编译器完全不同的方式破坏从头文件导出的符号名称,因此当您尝试链接时,您会收到一个链接器错误,提示缺少符号。

为了解决这个问题,我们告诉 C++ 编译器以“C”模式运行,因此它以与 C 编译器相同的方式执行名称修改。这样做后,链接器错误就得到了修复。

于 2008-09-15T23:24:32.370 回答
11

C 和 C++ 对符号名称有不同的规则。符号是链接器如何知道在编译器生成的一个目标文件中对函数“openBankAccount”的调用是对您在另一个目标文件中调用的那个函数的引用编译器。这使您可以使用多个源文件制作程序,这在处理大型项目时是一种解脱。

在 C 中,规则非常简单,符号都在一个名称空间中。所以整数“socks”存储为“socks”,函数count_socks存储为“count_socks”。

使用这个简单的符号命名规则为 C 和其他语言(如 C)构建了链接器。所以链接器中的符号只是简单的字符串。

但是在 C++ 中,该语言允许您拥有名称空间、多态性以及与这样一个简单规则相冲突的各种其他事物。所有六个名为“add”的多态函数都需要有不同的符号,否则其他目标文件将使用错误的符号。这是通过“修改”(这是一个技术术语)符号名称来完成的。

将 C++ 代码链接到 C 库或代码时,您需要 extern "C" 用 C 编写的任何内容,例如 C 库的头文件,以告诉您的 C++ 编译器这些符号名称不会被破坏,而其余的您的 C++ 代码当然必须被破坏,否则它将无法工作。

于 2008-09-15T23:28:54.543 回答
11

我们应该什么时候使用它?

当您将 C 库链接到 C++ 目标文件时

在需要我们使用它的编译器/链接器级别发生了什么?

C 和 C++ 使用不同的符号命名方案。这告诉链接器在给定库中链接时使用 C 的方案。

这在编译/链接方面如何解决需要我们使用它的问题?

使用 C 命名方案允许您引用 C 样式的符号。否则,链接器将尝试不起作用的 C++ 样式符号。

于 2008-09-15T23:29:41.343 回答
7

C++ 编译器创建符号名称的方式与 C 编译器不同。因此,如果您尝试调用驻留在 C 文件中并编译为 C 代码的函数,您需要告诉 C++ 编译器它尝试解析的符号名称与默认值不同;否则链接步骤将失败。

于 2008-09-15T23:25:34.893 回答
7

任何时候你都应该使用 extern "C",只要你包含一个定义函数的头文件,这个函数驻留在由 C 编译器编译的文件中,用于 C++ 文件。(许多标准 C 库可能会在其头文件中包含此检查,以使开发人员更简单)

例如,如果您有一个包含 3 个文件 util.c、util.h 和 main.cpp 的项目,并且 .c 和 .cpp 文件都是使用 C++ 编译器(g++、cc 等)编译的,那么它不是t 确实需要,甚至可能导致链接器错误。如果您的构建过程对 util.c 使用常规 C 编译器,那么在包含 util.h 时您将需要使用 extern "C"。

正在发生的事情是 C++ 在其名称中对函数的参数进行了编码。这就是函数重载的工作原理。C 函数往往会在名称的开头添加下划线(“_”)。如果函数的实际名称是 _DoSomething() 或只是 DoSomething(),则不使用 extern "C" 链接器将寻找名为 DoSomething@@int@float() 的函数。

使用 extern "C" 通过告诉 C++ 编译器它应该寻找一个遵循 C 命名约定而不是 C++ 的函数来解决上述问题。

于 2008-09-15T23:39:06.967 回答
6

extern "C" {}构造指示编译器不要对大括号内声明的名称执行修改。通常,C++ 编译器会“增强”函数名称,以便它们对有关参数和返回值的类型信息进行编码;这被称为重名。该extern "C"构造可防止损坏。

它通常在 C++ 代码需要调用 C 语言库时使用。当将 C++ 函数(例如来自 DLL)暴露给 C 客户端时,也可以使用它。

于 2008-09-15T23:30:25.567 回答
5

这用于解决名称修改问题。extern C 表示这些函数在“平面”C 风格的 API 中。

于 2008-09-15T23:25:09.217 回答
1

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

要了解为什么extern是必要的,最好的办法是通过示例详细了解目标文件中发生的事情:

主文件

void f() {}
void g();

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

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

使用 GCC 4.8 Linux ELF输出编译:

g++ -c main.cpp

反编译符号表:

readelf -s main.o

输出包含:

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

解释

我们看到:

  • 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 函数只有一个可能的未损坏符号,因此不需要额外的工作。

主文件

#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

抄送

#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++期望找到一个 mangled f,它gcc没有产生。

GitHub 上的示例

C 示例中的最小可运行 C++

从中调用 C++ 有点困难:我们必须手动创建要公开的每个函数的非损坏版本。

在这里,我们说明了如何将 C++ 函数重载暴露给 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 中测试。

于 2019-05-14T19:00:33.790 回答