当一个名称或类型拥有特定的语言联系时,这意味着什么?

40
根据 ANSI ISO/IEC 14882:2003 的规定(第127页):
链接规范是可以嵌套的。当链接规范嵌套时,最内层的规范决定了所使用的语言。链接规范并不确定作用域。链接规范只能出现在命名空间范围(3.3)。在链接规范中,指定的语言链接适用于声明引入的所有函数声明符、函数名和变量名的函数类型。
extern "C" void f1(void(*pf)(int));
// the name f1 and its function type have C language
// linkage; pf is a pointer to a C function

extern "C" typedef void FUNC();
FUNC f2;
// the name f2 has C++ language linkage and the
// function's type has C language linkage

extern "C" FUNC f3;
// the name of function f3 and the function's type
// have C language linkage

void (*pf2)(FUNC*);
// the name of the variable pf2 has C++ linkage and
// the type of pf2 is pointer to C++ function that
// takes one parameter of type pointer to C function

这一切是什么意思?例如,函数f2()具有哪种链接方式,C语言或C++语言的链接方式?

正如@Johannes Schaub所指出的那样,标准中没有关于此的真正解释,因此在不同的编译器中可能会有不同的解释。

请解释以下对象文件的差异:

  • 带有C语言链接和C++语言链接的函数名称。
  • 带有C语言链接和C++语言链接的函数类型。

3
相关主题:https://dev59.com/ZW035IYBdhLWcg3wKsqa - Johannes Schaub - litb
5
@artyom.stv:也请阅读这篇文章。https://dev59.com/P3NA5IYBdhLWcg3wPLP-#1041880 - Sadique
@Acme,那个回答说:“当你在C++中声明一个函数具有extern“C”链接时,C++编译器不会将参数/参数类型信息添加到用于链接的名称中。”,但这似乎是一种猜测。标准没有在任何地方说明如何给函数指定该链接(除非我漏掉了,但这就是我问你的原因,因为你推荐了那个答案,你必须对其正确性有意见)。 - Johannes Schaub - litb
@Johannes Schaub - litb: 我理解,我的问题不正确(“f2()函数使用哪种链接方式...?”)。在目标文件中,C和C++语言的链接方式似乎没有区别,除了调用规则和名称修饰。我在问问题时没有理解这个问题(这就是为什么问题不够准确):) - artyom.stv
2
@artyom,即使标准本身声称函数具有某种链接性,但并未描述其含义,因此问题是完全有效的。实现使名称链接影响名称重整,类型链接影响调用约定。但是,类型链接对名称重整没有影响,名称链接对调用约定没有影响。说“函数具有链接性”甚至“模板具有链接性”似乎是无意义的,因为标准似乎没有在任何地方说明这意味着什么。最直观(依我之见)的解释是指名称具有某种链接性。 - Johannes Schaub - litb
显示剩余6条评论
7个回答

18

语言链接是指C++和非C++代码段之间的链接。通常,在C++程序中,所有函数名、函数类型甚至变量名都具有默认的C++语言链接。

C++目标代码可以使用预定义的链接说明符与使用其他源语言(如C)生成的另一个对象代码进行链接。

您可能已经了解到名称重整的概念,它对函数名、函数类型和变量名进行编码,以便为它们生成唯一的名称。这使得链接器能够区分常用名称(例如函数重载的情况)。名称重整在将C模块与使用C++编译器编译的库或目标文件链接时并不可取。为了防止这种情况下的名称重整,使用链接说明符。在这种情况下,extern "C" 是链接说明符。让我们举个例子(c++代码在这里提到:here):

typedef int (*pfun)(int);  // line 1
extern "C" void foo(pfun); // line 2
extern "C" int g(int)      // line 3
...
foo( g ); // Error!        // line 5

第一行声明 pfun 指向一个 C++ 函数,因为它缺少链接说明符。

因此,第二行声明 foo 是一个接受指向 C++ 函数的指针的 C 函数。

第五行试图用指向 C 函数 g 的指针调用 foo,出现类型不匹配的错误。

函数名称链接的差异:

让我们来看两个不同的文件:

一个带有 extern "c" 链接 (file1.cpp):

#include <iostream>
using namespace std;

extern "C"
{
void foo (int a, int b)
{
    cout << "here";
}
}

int main ()
{
    foo (10,20);
    return 0;
}

没有使用extern "c"链接的文件(file2.cpp):

#include <iostream>
using namespace std;

void foo (int a, int b)
{
    cout << "here";
}

int main ()
{
    foo (10,20);
    return 0;
}

现在将这两个文件编译并检查 objdump。

# g++ file1.cpp -o file1
# objdump -Dx file1

# g++ file2.cpp -o file2
# objdump -Dx file2

使用extern "C"链接时,函数foo不会进行名称重整。因此,任何使用它的程序(假设我们将其制作成共享库)都可以直接调用foo(使用像dlsymdlopen这样的辅助函数),而不考虑任何名称重整效果。

0000000000400774 <foo>:
  400774:   55                      push   %rbp
  400775:   48 89 e5                mov    %rsp,%rbp
....
....
  400791:   c9                      leaveq 
  400792:   c3                      retq   

0000000000400793 <main>:
  400793:   55                      push   %rbp
  400794:   48 89 e5                mov    %rsp,%rbp
  400797:   be 14 00 00 00          mov    $0x14,%esi
  40079c:   bf 0a 00 00 00          mov    $0xa,%edi
  4007a1:   e8 ce ff ff ff          callq  400774 <foo>
  4007a6:   b8 00 00 00 00          mov    $0x0,%eax
  4007ab:   c9                      leaveq 

另一方面,如果没有使用extern "C",函数foo会按照某些预定义规则进行名称重整(由编译器/链接器知晓),因此应用程序不能直接通过指定名称foo来调用它。但是,如果愿意,可以使用重整后的名称(在本例中为_Z3fooii)来调用该函数,但显然没有人会这样做。

0000000000400774 <_Z3fooii>:
  400774:   55                      push   %rbp
  400775:   48 89 e5                mov    %rsp,%rbp
 ...
...
  400791:   c9                      leaveq 
  400792:   c3                      retq   

0000000000400793 <main>:
  400793:   55                      push   %rbp
  400794:   48 89 e5                mov    %rsp,%rbp
  400797:   be 14 00 00 00          mov    $0x14,%esi
  40079c:   bf 0a 00 00 00          mov    $0xa,%edi
  4007a1:   e8 ce ff ff ff          callq  400774 <_Z3fooii>
  4007a6:   b8 00 00 00 00          mov    $0x0,%eax
  4007ab:   c9                      leaveq 
  4007ac:   c3                      retq   

这个页面也是关于这个特定主题的一个不错的阅读材料。

一篇关于调用约定的清晰易懂的好文章:http://www.codeproject.com/KB/cpp/calling_conventions_demystified.aspx


那绝对有道理。我从来没有想到类型名称也需要进行名称混淆。 - Jeff Mercado

2
extern "C" typedef void FUNC();
FUNC f2;
// the name f2 has C++ language linkage and the
// function's type has C language linkage

名称FUNC被声明为"C"链接,因为第一行上写着extern "C"

名称f2具有C++链接,因为这是默认值,并且在第二行没有给出其他链接。

使用名称f2来引用具有C链接的函数并不会更改名称的链接。


2
这与程序的应用程序二进制接口(ABI)有关。
正如API指定程序源代码的外部接口一样,ABI指定程序的二进制代码(编译后的版本)的外部接口。
最初,C函数只有几种不同的形式。类似于:
int foo(int);

“would be prefixed by an underscore by the compiler, to form _foo, and then exported to be made available to other applications.”
然而,这还不够。例如在Windows API中,你会看到以下内容:
DWORD CreateWindowW(...);        //Original parameters
DWORD CreateWindowExW(..., ...); //More parameters

这是因为仅通过查看函数名无法区分函数的重载,所以人们开始通过添加“Ex”后缀(或类似的后缀)来更改它们。
这变得非常丑陋,而且仍然不允许操作符重载,这在C++中得到了实现。因此,C++提出了名称重整,将额外信息放入函数名称中,例如其参数的数据类型,并使其成为带有许多@符号的加密字符串。
这一切都很好,但它并不完全标准化。
当然,随着新语言和编译器的出现,每个编译器都会想出自己的方案,有些与其他编译器不兼容。因此,如果需要导入或导出外部函数,则需要指定编译器应查找哪种ABI,因此您在那里看到了extern "C++"。

2
所有这些是什么意思?例如,f2() 函数与 C 或 C++ 语言链接有什么关联?
extern "C" typedef void FUNC();
FUNC f2;
// the name f2 has C++ language linkage and the 
// function's type has C language linkage 

“你所称的"f2()函数"在其链接中有两个方面:

  • 在符号表中对其名称进行编码或不编码(具有C++语言链接),以及
  • 调用该函数所需的C或C++调用约定(C)。

要调用f2(),您需要在对象文件中找到其名称,该名称将是一个名为"f2"、不带参数的函数的编码版本。您可以通过编译上述代码并检查对象文件(例如使用GNU工具nm --demangle)来轻松验证此内容。

但是,为了调用该函数,预条件和后置条件的约定,如寄存器使用、堆栈设置等,都必须是C函数的约定。C和C++函数具有不同的调用约定是合法的,并且可能会这样做-例如-为了方便C++异常处理。

请解释一下对象文件中具有C语言链接和C++语言链接的函数名称之间的区别。
在C++中,函数名称被编码为包含其参数类型和返回类型的字符串,这是为了支持函数重载。而在C语言中,函数名称只是简单的文本字符串,没有任何附加信息。因此,在对象文件中,具有C语言链接的函数名称与C++语言链接的函数名称不同,前者只是简单的文本字符串,而后者是编码后的字符串。这意味着如果您想在C代码中调用一个由C++编写的函数,您需要知道其编码后的名称。
  • 对于C语言链接,f2()将是生成的目标文件中的符号
  • 对于C++语言链接,会有一个被编码的版本的“名为f2且不带参数的函数”(例如GNU中的_Z2f2v,反编码后为f2()

关于具有C语言链接和C++语言链接的函数类型。

如上所述,这是关于调用函数地址处代码时使用的寄存器/堆栈使用约定。这些元信息不一定存储在对象的符号表信息中(并且肯定不是符号名称键本身的一部分)。

此外,因为每个函数采用其中一个调用约定,编译器需要知道在跟随指向函数的指针时要使用哪个调用约定:有了这个认识,我认为问题中剩下的代码就变得清晰了。

http://developers.sun.com/solaris/articles/mixing.html中有一个很好的讨论 - 特别是我推荐阅读Working with Pointers to Functions部分。


2
“f2的名称具有C++语言链接” 在C++语言链接中,函数的名称不仅定义了它,而且它的参数类型和返回值也定义了它。 在这种情况下,您有: void f2(void); 但是您可以定义为: void f2(int a); 因为链接将把它们视为不同的类型,这是在C语言中无法做到的。
“函数的类型具有C语言链接” 我不知道细节,但我知道它的高级别。 基本上,它使得C++编译的函数可以从C链接。 如果我没记错,在C和C++中,将参数传递给函数的方式是不同的。 在这种情况下,函数f2将按照C编译器的方式传递参数。 这样,该函数将可以从C和C++链接。

Cе’ҢC++зҡ„cdeclи°ғз”ЁзәҰе®ҡжңүд»Җд№ҲеҢәеҲ«пјҹпјҲжҲ‘и®ӨдёәcdeclжҳҜж ҮеҮҶеҢ–зҡ„пјҢдё”еңЁCе’ҢC++дёӯиЎҢдёәзӣёеҗҢпјү - artyom.stv
extern "C"这一行告诉编译器,发送给链接器的外部信息应该使用C调用约定和名称修饰(例如,前面加上一个下划线)。 - Roee Gavirel
@Roee Gavirel:名称修饰-是的。但调用约定-这通常是如何编写的。 "C调用约定"是什么意思? C和C ++调用约定之间有区别吗? - artyom.stv
@artyom:调用约定都是特定于平台的,是的,许多平台默认情况下对C和C++使用不同的调用约定。 - Ben Voigt
@artyom.stv 调用约定是用于定义如何调用函数/方法等的术语 - 它处理参数如何传递和返回 - 哪个寄存器或堆栈位置保存哪个参数和返回值,非基元(结构体等)如何传递到/从函数返回,谁清理堆栈。它们在给定平台上的 C 和 C++ 可能不同。这与名称重整不同,后者更多地涉及函数/方法在需要时的命名和查找方式(例如在链接期间)。 - nos
显示剩余2条评论

1
众所周知,在C/C++代码翻译中,由两个主要阶段组成:编译和链接。当编译器生成目标文件时,它会将信息传递给链接器,指定在哪些目标文件中调用或引用了给定函数。在C语言中,函数只有一个名称和相应的定义。
// file1.c
void foo(void) {}

在编译后,file1.obj 存储了代码以及关于 foo 符号定义的信息。

但是当涉及到 C++ 时,符号名称变得更加复杂。一个函数可能会被重载或成为类的成员。但链接器并不想知道这些。为了保持旧链接器的简单性和可重用性,它需要一个单一的名称来表示 foo,无论它是什么:

void foo(void) {}
void foo(int) {}
void ClassA::foo(void) {}

但现在它不能再被称为foo了,因此名称混淆就出现了。我们可能会从编译器得到一些变体,如foo_void、foo_int、foo_void_classa等。最后,链接器很高兴,因为所有这些看起来都像简单的符号。

当我们想要在C++代码中调用使用C编译器编译的foo函数时,我们必须告诉编译器我们希望将foo作为C风格的foo而不是C++编译器可能会假设的foo_void。这是通过以下方式完成的:

extern "C" void foo();

现在编译器知道foo是使用C编译器编译的,并将向链接器传递信息,说明此代码调用了foo。链接器将与file1.obj中的foo定义进行匹配。所以我认为这就是全部。

一些其他指令,如cdecl或stdcall,是Windows特定的,它们告诉函数调用中的参数如何传递。是的,对于C和C++来说,它是cdecl。但Windows API函数使用stdcall - Pascal约定(简单性和历史上Microsoft曾经提供过Pascal的Windows开发环境)。


0
每个函数、函数类型和对象都有一个语言链接,它被指定为一个简单的字符串。默认情况下,链接是"C++"。唯一的其他标准语言链接是"C"。所有其他语言链接和与不同语言链接相关联的属性都是实现定义的。

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