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

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

例如:

extern "C" {
   void foo();
}

答案

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 ++ 定义的对象的链接是实现定义的和语言相关的。只有两种语言实现的对象布局策略足够相似时,才能实现这种链接

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

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

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

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

反编译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++. */
#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++期望找到gcc不会产生的错位f

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 中测试。

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

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

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

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

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

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

大多数编程语言都不是在现有编程语言之上构建的。 C ++ 是在 C 的基础上构建的,而且它是一种从过程编程语言构建的面向对象的编程语言,因此,存在像extern "C"这样的 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 a vs 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” 的旧版 C 文件,其中include其他旧版 C 文件中的 s 函数名,例如 “parent.h”,“child.h” 等。如果旧版 “parent.c” 文件为通过 C ++ 编译器运行,则函数名称将被修饰,并且它们将不再与在 “parent.h”,“child.h” 等中指定的函数名称匹配 - 因此,这些外部文件中的函数名称也将需要受到重创。在复杂的 C 程序中处理函数名称(具有很多依赖性)会导致代码损坏;因此提供一个可以告诉 C ++ 编译器不要破坏函数名的关键字可能会很方便。

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

例如:

extern "C" void printMe(int a);

它以一种可从 C 调用该函数的方式更改函数的链接。实际上,这意味着函数名称不会被修饰

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

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

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

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

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

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

我在 dll(动态链接库)文件中使用过 “extern“ C”” 来制作 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);
}