题 外部“C”在C ++中有什么影响?


究竟做了什么 extern "C" 进入C ++代码吗?

例如:

extern "C" {
   void foo();
}

1219
2018-06-25 02:10


起源


我想向您介绍这篇文章: http://www.agner.org/optimize/calling_conventions.pdf 它告诉你更多关于调用约定和编译器之间的区别。 - Sam Liao
@Litherum在我的头脑中,它告诉编译器使用C编译该代码范围,因为你有一个交叉编译器。此外,这意味着您有一个Cpp文件 foo() 功能。 - ha9u63ar


答案:


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

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

您知道,您可以明确指定每个单独的声明/定义的“C”链接,或使用块将一系列声明/定义分组以具有特定的链接:

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

如果你关心技术问题,它们列在C ++ 03标准的7.5节中,这里是一个简短的总结(重点是extern“C”):

  • extern“C”是一个链接规范
  • 每个编译器都是 需要 提供“C”联动
  • 链接规范只应在命名空间范围内发生
  •  所有函数类型,函数名和变量名都有语言链接   见理查德的评论: 只有具有外部链接的函数名和变量名才具有语言链接
  • 具有不同语言链接的两种函数类型即使在其他方面相同也是不同的类型
  • 连接规格嵌套,内部确定最终的连接
  • 对于班级成员,忽略extern“C”
  • 最多一个具有特定名称的函数可以具有“C”链接(无论命名空间如何)
  •  extern“C”强制函数具有外部链接(不能使其静态)    见理查德的评论:    'extern'内部的'static'有效;如此声明的实体具有内部链接,因此没有语言链接
  • 从C ++到其他语言中定义的对象以及从其他语言在C ++中定义的对象的链接是实现定义的并且依赖于语言。只有在两种语言实现的对象布局策略足够相似的情况下才能实现这种联系

1219
2018-06-25 02:12



C编译器不使用c ++所做的修改。因此,如果您想从c ++程序调用c接口,则必须明确声明c接口为“extern c”。 - Sam Liao
@Faisal:不要尝试链接使用不同C ++编译器构建的代码,即使交叉引用都是'extern“C”'。类的布局,或用于处理异常的机制,或用于确保变量在使用前初始化的机制,或其他此类差异之间通常存在差异,此外,您可能需要两个单独的C ++运行时支持库(一个用于每个编译器)。 - Jonathan Leffler
@Leffler - 谢谢,你说得好点。我不是故意鼓励使用extern“C”来使用不同的C ++编译器。相反,我希望建议如果你没有编写需要由另一个C ++编译器链接的东西,你可能不需要extern“C”。 - Faisal Vali
'extern“C”强制一个函数有外部链接(不能使它静止)'是不正确的。 'extern'内部的'static'有效;如此声明的实体具有内部链接,因此没有语言链接。 - Richard Smith
'所有函数类型,函数名和变量名都有语言链接'也是不正确的。只有具有外部链接的函数名和变量名才具有语言链接。 - Richard Smith


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

您经常会在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"
}

我想象的完成了同样的事情。

不知道哪种方式更好,但我已经看到了两种方式。


244
2017-10-21 01:08



有一个明显的区别。对于前者,如果使用普通的gcc编译器编译此文件,它将生成一个不损坏函数名的对象。然后,如果您使用链接器链接C和C ++对象,它将找不到这些函数。您需要在第二个代码块中包含带有extern关键字的“遗留标头”文件。 - Anne van Rossum
@Anne:C ++编译器也会查找未编码的名称,因为它看到了 extern "C" 在标题中)。效果很好,多次使用这种技术。 - Ben Voigt
@Anne:那不对,第一个也没关系。它被C编译器忽略,并且与C ++中的第二个具有相同的效果。编译器不会在意它是否遇到 extern "C" 在它包括标题之前或之后。当它到达编译器时,它只是一个长的预处理文本流。 - Ben Voigt
@Anne,不,我认为你已经受到源中其他一些错误的影响,因为你所描述的是错误的。没有版本 g++ 对于任何目标,至少在过去17年中的任何时候,这都是错误的。第一个例子的重点是,无论你是使用C还是C ++编译器,都不会对名称进行名称修改。 extern "C" 块。 - Jonathan Wakely
“哪一个更好” - 当然,第一个变体更好:它允许在C和C ++代码中直接包含标题,而不包括任何进一步的要求。第二种方法是C头的解决方法作者忘记了C ++警卫(没问题,但是,如果这些是之后添加的,嵌套的extern“C”声明是可接受的......)。 - Aconcagua


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

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

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

因此,如果您将函数指定为extern C,则编译器不会对其执行名称修改,而是可以直接执行 使用其符号名称作为函数名称进行访问。

这在使用时很方便 dlsym() 和 dlopen() 用于调用此类函数。


170
2018-06-25 05:22



你的意思是什么?是符号名称=函数名称会使符号名称传递给已知的dlsym,还是其他东西? - Error
@Error:是的。在一般情况下,基本上不可能只给出一个C ++共享库,只给出一个头文件并选择要加载的正确函数。 (在x86上,有一个以Itanium ABI形式发布的名称修改规范,我知道所有x86编译器都用来破坏C ++函数名称,但语言中没有任何内容需要这个。) - Jonathan Tomer


让我们 反编译生成的目标文件g ++ 看看这个实现中发生了什么。

生成示例

输入:

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 a.cpp

反编译符号表:

readelf -s a.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

解释

我们看到:

  • ef 和 eg 存储在与代码中名称相同的符号中

  • 其他符号被破坏了。让我们解开他们:

    $ 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) { }
}

122
2018-05-29 10:06





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

大多数编程语言都不是在现有编程语言的基础上构建的。 C ++建立在C语言之上,而且它是一种面向对象的编程语言,它是用过程编程语言构建的,因此有C ++关键字就像 extern 提供与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;
}

C编译器不会编译上面的例子,因为函数相同 printMe 被定义两次(即使它们具有不同的参数 int a VS char a)。

gcc -o printMe printMe.c && ./printMe;
1错误。 PrintMe定义不止一次。

C ++编译器将编译上面的示例。它并不关心 printMe 定义了两次。

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

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

Extern说“不要破坏功能名称”

但是,假设我们有一个名为“parent.c”的遗留C文件 include来自其他遗留C文件的函数名称,“parent.h”,“child.h”等。如果遗留的“parent.c”文件是通过C ++编译器运行的,那么函数名称将被破坏,它们将会不再匹配“parent.h”,“child.h”等中指定的函数名称 - 因此这些外部文件中的函数名称也需要进行修改。这可能会变得非常混乱。因此,提供一个可以告诉C ++编译器不会破坏函数名的关键字可能会很方便。

extern keyword告诉C ++编译器不要破坏(重命名)函数名称。用法示例: extern void printMe(int a);


24
2018-02-12 01:50





它改变了函数的链接,使得函数可以从C调用。实际上,这意味着函数名不是 错位


22
2018-06-25 02:12



修剪或装饰什么是正确的术语? - ojblass
Mangled是一般使用的术语......不要相信我曾经见过'装饰'这个含义。 - Matthew Scharley
这里叫做装饰 en.wikipedia.org/wiki/Name_mangling - Error


没有任何C-header将使用extern“C”进行编译。当C-header中的标识符与C ++关键字冲突时,C ++编译器会抱怨这一点。

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

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

Kinda有意义,但在将C代码移植到C ++时需要牢记。


21
2018-01-09 22:16



extern "C" 意味着使用C链接,如其他答案所述。它并不意味着“将内容编译为C”或任何东西。 int virtual; 在C ++中无效,指定不同的链接不会改变它。 - M.M
...或模式通常,任何包含语法错误的代码都不会编译。 - Valentin Heinitz
@ValentinHeinitz自然而然,虽然在C中使用“virtual”作为标识符不是语法错误。我只想指出你不能自动使用 任何 C ++中的C头,通过在它周围放置外部“C”。 - Sander Mertens


它通知C ++编译器在链接时以C风格查找这些函数的名称,因为在C和C ++中编译的函数的名称在链接阶段是不同的。


16
2018-06-25 02:12





extern“C”意味着被C ++编译器识别并通知编译器所提到的函数是(或将)以C风格编译的。因此,在链接时,它从C链接到正确的函数版本。


13
2018-04-10 09:46