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

What is the effect of extern "C" in C++?

提问人:Litherum 提问时间:6/25/2009 最后编辑:Ciro Santilli OurBigBook.comLitherum 更新时间:6/9/2023 访问量:1193608

问:

放入 C++ 代码到底有什么作用?extern "C"

例如:

extern "C" {
   void foo();
}
C++ C 联动 名称-mangling extern-c

评论

115赞 Sam Liao 6/25/2009
我想向你介绍这篇文章: http://www.agner.org/optimize/calling_conventions.pdf 它告诉你更多关于调用约定和编译器之间的区别。

答:

26赞 Mark Rushakoff 6/25/2009 #1

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

32赞 Employed Russian 6/25/2009 #2

它以这样一种方式更改函数的链接,以便可以从 C 调用该函数。在实践中,这意味着函数名称没有被破坏

评论

3赞 Matthew Scharley 6/25/2009
残缺是常用的术语......不相信我曾经见过“装饰”与这个含义一起使用。
3赞 René Nyffenegger 2/1/2020
Microsoft(至少部分)在其文档中使用了装饰而不是残缺。他们甚至为他们的工具命名 undecorate (aka un-mangle) 一个名字 .undname
1982赞 Faisal Vali 6/25/2009 #3

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

由于 C++ 具有函数名称重载,而 C 没有,因此 C++ 编译器不能仅将函数名称用作要链接到的唯一 ID,因此它通过添加有关参数的信息来破坏名称。C 编译器不需要修改名称,因为您不能在 C 中重载函数名称。当您在 C++ 中声明函数具有链接时,C++ 编译器不会将参数/参数类型信息添加到用于链接的名称中。extern "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” 强制函数具有外部链接(不能使其静态) 参见 Richard 的评论: inside 是有效的;如此声明的实体具有内部联系,因此没有语言联系staticextern "C"
  • 从 C++ 到用其他语言定义的对象以及用其他语言用 C++ 定义的对象的链接是实现定义的,并且依赖于语言。只有当两种语言实现的对象布局策略足够相似时,才能实现这种联动

评论

39赞 Sam Liao 6/25/2009
C 编译器不使用 c++ 那样的 mangling。因此,如果要从 c++ 程序调用 c 接口,则必须将 c 接口明确声明为“extern c”。
70赞 Jonathan Leffler 6/25/2009
@Faisal:不要尝试链接使用不同C++编译器构建的代码,即使交叉引用都是“extern ”C”。类的布局、用于处理异常的机制、用于确保变量在使用前初始化的机制或其他此类差异之间通常存在差异,此外,您可能需要两个单独的 C++ 运行时支持库(每个编译器一个)。
6赞 Ben Voigt 1/17/2013
没有提到调用约定?没有提到该标准要求在函数指针上使用修饰符才能调用 C 链接函数(尽管在实践中许多实现并不关心)?extern "C"
19赞 Richard Smith 2/14/2013
“extern ”C“ 强制函数具有外部链接(不能使其静态)”不正确。'extern “C”' 中的 'static' 有效;如此声明的实体具有内部联系,因此没有语言联系。
19赞 aschepler 7/2/2014
请注意,这是一个定义。这可能不是您的预期,旁边是 的非定义。要使其成为非定义,您需要 .另一方面,不带大括号的 one-declaration 语法确实使声明成为非定义:与extern "C" { int i; }void g(char);extern "C" { extern int i; }extern "C" int i;extern "C" { extern int i; }
224赞 sud03r 6/25/2009 #4

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

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

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

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

这在使用和调用此类函数时会派上用场。dlsym()dlopen()

评论

0赞 Error 3/3/2018
你说的方便是什么意思?符号名称 = 函数名称是否会使传递给 DLSYM 的符号名称已知,或者其他事情?
3赞 Jonathan Klabunde Tomer 4/24/2018
@Error:是的。在一般情况下,dlopen() 一个 C++ 共享库只给出一个头文件并选择正确的函数来加载基本上是不可能的。(在 x86 上,有一个以 Itanium ABI 形式发布的名称修改规范,我所知道的所有 x86 编译器都使用它来修改 C++ 函数名称,但该语言中没有任何内容需要这样做。
12赞 Flami 4/10/2012 #5

extern "C"旨在被 C++ 编译器识别,并通知编译器所记录的函数正在(或将要)以 C 样式编译,以便在链接时,它从 C 链接到函数的正确版本。

439赞 UncaAlby 10/21/2012 #6

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

你经常会在 C 头中看到这样的代码:

#ifdef __cplusplus
extern "C" {
#endif

// all of your legacy C code here

#ifdef __cplusplus
}
#endif

这实现的是,它允许您将该 C 头文件与 C++ 代码一起使用,因为将定义宏。但是,您仍然可以将它与旧版 C 代码一起使用,其中未定义宏,因此它不会看到唯一的 C++ 构造。__cplusplus

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

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

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

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

评论

13赞 Anne van Rossum 4/12/2013
有明显的区别。如果是前者,如果你用普通的 gcc 编译器编译这个文件,它将生成一个函数名称没有被破坏的对象。如果随后使用链接器链接 C 和 C++ 对象,则不会找到函数。您需要像在第二个代码块中一样,使用 extern 关键字包含这些“旧版头文件”。
13赞 Ben Voigt 6/27/2014
@Anne:C++编译器也会查找未损坏的名称,因为它在标头中看到)。效果很好,多次使用这种技术。extern "C"
35赞 Ben Voigt 6/30/2014
@Anne:不对,第一个也没问题。它被 C 编译器忽略,并且与 C++ 中的第二个具有相同的效果。编译器不在乎它是在包含标头之前还是之后遇到。当它到达编译器时,无论如何,它只是一长串预处理的文本。extern "C"
11赞 Jonathan Wakely 1/20/2016
@Anne,不,我认为你受到了来源中其他一些错误的影响,因为你所描述的是错误的。至少在过去 17 年的任何时候,对于任何目标来说,没有一个版本会出错。第一个示例的重点是,无论您使用 C 还是 C++ 编译器,都不会对块中的名称进行名称修改。g++extern "C"
9赞 Aconcagua 8/9/2017
“哪个更好” - 当然,第一个变体更好:它允许直接包含标头,在 C 和 C++ 代码中没有任何进一步的要求。第二种方法是 C 标头的解决方法,作者忘记了 C++ 保护(没问题,但是,如果之后添加这些,则接受嵌套的外部“C”声明......
34赞 Sander Mertens 1/10/2013 #7

不是任何 C 头都可以通过简单地包装在外部“C”中来与 C++ 兼容。当 C 标头中的标识符与 C++ 关键字冲突时,C++ 编译器将对此提出申诉。

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

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

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

评论

20赞 M.M 1/27/2015
extern "C"表示使用 C 链接,如其他答案所述。这并不意味着“将内容编译为 C”或任何东西。 在 C++ 中无效,指定不同的链接不会改变这一点。int virtual;
3赞 Valentin H 11/1/2017
...或模式,任何包含语法错误的代码都不会编译。
4赞 Sander Mertens 11/2/2017
@ValentinHeinitz很自然地,尽管在 C 语言中使用“virtual”作为标识符并不是语法错误。我只是想指出,您不能通过在 C++ 周围放置 extern “C” 来自动使用任何 C 标头。
0赞 penguin359 2/14/2022
我刚刚遇到了一个不同的兼容性问题。C 标头在结构的某些 typedef 上使用 struct 前缀。它使用 gcc 和 clang 编译时没有错误或警告,但使用 g++ 和 clang++ 失败,因为只允许在原始标识符上使用 struct,而不是它的 typedef。我不得不修改标头以使其兼容 C++,而不仅仅是包装器,现在它可以在 C 和 C++ 版本上编译。-Wextraextern "C" {...}
4赞 Zero Infinity 7/1/2014 #8

我之前对 dll(动态链接库)文件使用了 'extern “C”' 来使 etc. main() 函数“可导出”,以便以后可以在 dll 的另一个可执行文件中使用它。 也许我曾经使用它的例子可能很有用。

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);
}

EXE文件

#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);
}

评论

7赞 IInspectable 9/7/2016
伪。 并且无关。前者控制符号装饰,后者负责创建导出条目。您也可以使用 C++ 名称修饰导出符号。除了完全忽略了这个问题的重点之外,代码示例中还存在其他错误。首先,从 DLL 导出不会声明返回值。或者就此而言,称为约定。导入时,您将随机调用约定 () 归因于 ,并为 32 位构建使用错误的符号(应为 或 )。对不起,-1。extern "C"__declspec(dllexport)mainWINAPI_main_main@0
3赞 IInspectable 5/25/2017
这只是重复一遍,你不知道你在做什么,但这样做似乎对你有用,对于一些未公开的目标平台列表。您没有解决我在之前的评论中提出的问题。这仍然是一个反对票,因为这是非常错误的(还有更多,这不适合任何评论)。
3赞 IInspectable 5/25/2017
在 Stack Overflow 上发布答案意味着,您知道自己在做什么。这是意料之中的。至于您尝试“防止运行时堆栈损坏”:您的函数签名指定了 类型的返回值,但您的实现不返回任何内容。那会飞得很好......void*
3赞 IInspectable 5/25/2017
如果你实现了一些东西,看起来是有效的,纯粹是运气,那么你显然不知道你在做什么(你的“工作”样本属于这一类)。这是未定义的行为,看起来有效是未定义行为的有效形式。它仍然未定义。如果您将来更加勤奋,我将不胜感激。其中一部分可能是删除这个提议的答案。
3赞 IInspectable 5/25/2017
您正在将不返回任何内容的函数重新解释为返回指针的函数。纯粹的运气是,x86 对不匹配的函数签名非常宽容,尤其是整数类型的返回值。您的代码只能巧合地工作。如果你不同意,你需要解释为什么你的代码可以可靠地工作。
417赞 Ciro Santilli OurBigBook.com 5/29/2015 #9

反编译 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

解释

我们看到:

  • 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

在外部 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);
}

e..

#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++fgcc

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 标头时,外部“c”在哪里?

在 Ubuntu 18.04 中测试。

评论

53赞 Gabriel Staples 10/26/2018
最佳答案,因为 1) 明确提到这有助于您从 C++ 程序中调用未损坏的 C 函数,以及从 C 程序中调用未损坏的 C++ 函数,其他答案并不那么明显,以及 2) 因为您显示了每个示例的不同示例。谢谢!extern "C" {
1赞 paleonix 10/3/2020
我想知道像 unistd.h、sys/stat.h 和 sys.types.h 这样的 C 标头。他们似乎没有把“'C'”放在“extern”之后。从 C++ 代码中使用它们似乎仍然没有问题。原因是,这些是没有实现文件的纯头文件吗?
1赞 Ciro Santilli OurBigBook.com 10/3/2020
@Paul他们似乎用宏启用了外部 C:stackoverflow.com/questions/8087438/......我观察到 Ubuntu 20.04 上 unistd.h 的答案中提到的内容。但是,它可能依赖于 : gcc.gnu.org/onlinedocs/cpp/System-Headers.html__BEGIN_DECLScstdio#pragma GCC system_header
0赞 paleonix 10/4/2020
谢谢!奇怪的是,当我搜索时,这个问题没有出现,现在当我搜索那个特定的宏时,id 出现了......我想在这里链接它是件好事。由于__BEGIN_DECLS在 sys/cdefs.h 中定义,但这不包含在 unistd.h、sys/stat.h 和 sys/types.h 中,我猜 sys/cdefs.h 默认只包含在预处理器中?
0赞 Ciro Santilli OurBigBook.com 10/4/2020
@Paul不用担心,我们都因谷歌上帝的智慧而生死。它通过 包含在内。#include <features.h>
7赞 Yogeesh H T 11/17/2015 #10

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)
{
    //
    //
}
97赞 tim-montague 2/12/2017 #11

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

大多数编程语言都不是建立在现有编程语言之上的。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 编译器不会编译上述示例,因为同一函数被定义了两次(即使它们具有不同的参数 vs )。printMeint achar a

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

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

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

这是因为 C++ 编译器根据函数的参数隐式重命名(修改)函数。该语言被设计为面向对象 - 使用同名的方法(函数)创建不同的类,并基于不同的参数覆盖方法名称(方法覆盖)。

说的是“不要篡改 C 函数名称”extern "C"

尽管 C++ 是基于 C 构建的,但修改可能会导致 C 代码混乱。例如,假设我们有一个名为“parent.c”的旧 C 文件,该文件的函数名称来自不同的头文件,“parent.h”、“child.h”等。如果我们通过 C++ 编译器运行“parent.c”,这将破坏该文件中的函数名称,并且它们将不再与头文件中指定的函数名称匹配。因此,“parent.h”和“child.h”头文件中的函数名称也需要修改。对于一些文件来说,这可能没问题,但如果 C 程序很复杂,修改可能会很慢并导致代码损坏,因此提供一个关键字告诉 C++ 编译器不要修改函数名称可能很方便。include

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

例如:

extern "C" void printMe(int a);

评论

0赞 BattleTested_закалённый в бою 11/4/2018
如果我们只有一个文件,我们可以不使用吗?我的意思是,如果我们没有头文件,而只有一个源文件(只是实现)并通过函数指针使用其功能。在这种状态下,我们只使用了函数(无论其名称如何)。extern "C"dll
1赞 Trombe 7/6/2017 #12

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

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

4赞 Manohar Reddy Poreddy 7/24/2018 #13

这个答案是为那些不耐烦/有最后期限的人准备的,下面只有部分/简单的解释:

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

因此
,在 C++ 中,使用 Name mangling 对 C 中的每个函数进行唯一标识,即使没有名称 mangling 对每个函数
进行唯一标识

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

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

0赞 Susobhan Das 2/15/2020 #14

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

C++编译器究竟是做什么的:它在编译过程中破坏了名称,因此我们需要告诉编译器特殊对待实现。C

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

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

评论

0赞 Jonathan Leffler 2/16/2020
欢迎使用 Stack Overflow。如果您决定回答一个已经确定且正确答案的旧问题,那么在当天晚些时候添加新答案可能不会给您带来任何积分。如果你有一些独特的新信息,或者你确信其他答案都是错误的,一定要添加一个新答案,但是在问题被问到很久之后,“另一个答案”给出相同的基本信息通常不会为你赢得太多的信任。坦率地说,我不认为这个答案有什么新意。
4赞 gnasher729 3/5/2020 #15

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

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

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

评论

0赞 Jonathan Leffler 3/6/2020
C++ 编译器可以编译函数 — 并且(受一些约束)它可以通过 C 编译器编译的代码调用。extern "C"
0赞 tothedistance 11/23/2022 #16

GCC 最近似乎也支持名称修改。即使在内部,如果您使用类或重载,它也会自动破坏。extern "c"

#include <stdio.h>
extern "C"{


struct myint{
    int i;
};

struct myint2
{
    int a;
    myint2(int a): a(a) {};
    operator myint() const {return myint{a};}
};

}

void f1(myint i){
    printf("%d", i.i);
}

int main(){
    myint2 a(1);
    f1(a);
}

我什至使用了很多cpp功能。但是代码编译和运行正常。如果你,你可以看到不是残缺的,但 myint 是。nmmain

1赞 MattTT 6/7/2023 #17

有趣的是,Visual Studio 和 dumpbin 都没有出现在这个线程中(直到现在)。所以我也想添加一些关于这方面的信息。
如果有意义,我们也可以将其合并到上述任何答案中。我不确定这里的 SO 哲学。
)


使用 Visual Studio 编译器进行编译时,每个符号前面都有一个前导下划线
示例:编译
extern "C"

extern "C"
void cf () {}

void cppf () {}

在生成的对象上运行,符号如下所示:dumpbin /symbols

01A 00000000 SECT7  notype ()    External     | _cf
01B 00000000 SECT5  notype ()    External     | ?cppf@@YAXXZ (void __cdecl cppf(void))

变量也是如此。

typedef struct
{ int a; int b; } CppStruct;

extern "C"
{
typedef struct
{ int a; int b; int c; } CStruct;
CStruct cCStruct;
CppStruct cCppStruct;
}
CStruct cppCStruct;
CppStruct cppCppStruct;

dumpbin /symbols

009 00000000 SECT3  notype       External     | _cCStruct
00A 0000000C SECT3  notype       External     | _cCppStruct
00B 00000014 SECT3  notype       External     | ?cppCStruct@@3UCStruct@@A (struct CStruct cppCStruct)
00C 00000020 SECT3  notype       External     | ?cppCppStruct@@3UCppStruct@@A (struct CppStruct cppCppStruct)

旁注:对于 C++ 符号,还会在括号中显示未损坏的符号名称。
旁注 2:如您所见,不影响类型定义。
dumpbinextern "C"


小心!如果一个变量在没有之前声明(例如在头文件中),那么它将使用 C++ 链接进行编译,恕不另行通知:extern "C"

extern CppStruct ifcStruct;
extern int       ifcVar;
/* ... */

extern "C"
{
CppStruct ifcStruct;
int       ifcVar = 0;
}

dumpbin /symbols

00C 00000000 SECT4  notype       External     | ?ifcStruct@@3UCppStruct@@A (struct CppStruct ifcStruct)
00D 00000008 SECT4  notype       External     | ?ifcVar@@3HA (int ifcVar)

但是,当一个函数在之前没有 某个地方声明时,(Microsoft)编译器会给出一个不同的错误消息:extern "C"

test.cpp(20): error C2732: linkage specification contradicts earlier specification for 'ifcf'
test.cpp(20): note: see declaration of 'ifcf'

此处将讨论造成这种差异的原因。


据我所知,还告诉编译器使用 C 调用约定
另请参阅从 DLL 导出 C++ 类 - Eli Bendersky 的网站Google 仍然可以找到它,但该网站似乎已经死了):
extern "C"

extern "C" __declspec(dllexport) IKlass* __cdecl create_klass()
让我们按顺序看看每个部分的含义:

  • extern "C"- 告诉 C++ 编译器链接器应为此函数使用 C 调用约定和名称修改。名称本身是从 DLL 中导出的,未被破坏 (create_klass)
  • __declspec(dllexport)- 告诉链接器从 DLL 中导出符号。或者,可以将名称放在提供给链接器的 .def 文件中。create_klasscreate_klass
  • __cdecl- 重复使用 C 调用约定(与调用约定相反)。这里不是绝对必要的,但我为了完整性而包含它(在应用程序代码的 typedef 中也是如此)。__stdcalliklass_factory

另请参阅此常见问题解答:如何混合使用 C 和 C++

0赞 Dražen Grašovec 12/14/2023 #18

这是一个带有示例代码的演示。

由于两个函数共享相同的名称,此代码显然不会在 C 中编译,但可以在 C++ 中编译,这允许重载函数:

#include <stdio.h>

const char* get_message2(void); 
int get_message2(char*); 

int main() {
   char test[] = "BBBBBBB";
   get_message2(test);
   return 0;
}

const char* get_message2() {
    char test[] = "AAAAAAA";
    return "message2";
};

int get_message2(char* arg) {
    char test[] = "CCCCCCC";
   return 0;
}

在 C++ 中,函数名称不是唯一的符号标识符,C++ 编译器根据函数原型为符号名称添加“前缀”和“后缀”:

drazen@HP-ProBook-640G1:~/proba$ readelf  -a proba | grep message
    34: 0000000000001199    70 FUNC    GLOBAL DEFAULT   16 _Z12get_message2v
    36: 00000000000011df    72 FUNC    GLOBAL DEFAULT   16 _Z12get_message2Pc

但是,如果我们想让我们的函数有一个 C 链接,我们必须添加修饰符extern "C"

const char* get_message2(void); 
extern "C" {
   int get_message2(char*); 
}

现在我们的函数有一个旧的 C 链接:int get_message2(char*);

drazen@HP-ProBook-640G1:~/proba$ readelf  -a proba | grep message
    32: 00000000000011df    72 FUNC    GLOBAL DEFAULT   16 get_message2
    35: 0000000000001199    70 FUNC    GLOBAL DEFAULT   16 _Z12get_message2v