链接器实际上对多重定义的“内联”函数做了什么?

时间:2016-02-05 21:03:52

标签: c++ c linker inline

在C和C ++中,具有外部链接的inline函数当然可以在链接时具有多个定义,假设这些定义都是(希望)相同的。 (我当然是指使用inline链接规范声明的函数,而不是编译器或链接时优化器实际内联的函数。)

那么,当遇到函数的多个定义时,常见的链接器通常会做什么?特别是:

  • 最终的可执行文件或共享库中是否包含所有定义?
  • 对函数的所有调用都是针对相同的定义吗?
  • 一个或多个C和C ++ ISO标准是否需要上述问题的答案,如果没有,大多数常见平台都会做同样的事情吗?

P.S。是的,我知道C和C ++是单独的语言,但它们都支持inline,并且它们的编译器输出通常可以通过相同的链接器链接(例如GCC' s ld),所以我相信他们在这方面没有任何区别。

5 个答案:

答案 0 :(得分:3)

链接器只需要弄清楚如何对所有定义进行重复数据删除。当然,只要发出任何功能定义即可;内联函数很可能内联。但是,如果你使用外部链接的内联函数的地址,你总是得到相同的地址(参见[dcl.fct.spec] / 4)。

内联函数不是唯一需要链接器支持的结构;模板是另一个,内联变量也是如此(在C ++ 17中)。

答案 1 :(得分:3)

如果该功能实际上是内联的,则没有任何内容可以链接。只有当出于某种原因,编译器决定来扩展内联函数时才必须生成函数的外联版本。如果编译器为多个转换单元生成函数的外联版本,则最终会得到多个具有相同“内联”函数定义的目标文件。

外部定义被编译到目标文件中,并且它被标记为如果该名称的定义不止一个,链接器就不会抱怨。如果有多个,链接器只需选择一个。通常是它看到的第一个,但这不是必需的,如果定义完全相同,那也没关系。这就是为什么它具有两个或多个相同内联函数的不同定义的未定义行为:没有规则可供选择。任何事情都可能发生。

答案 2 :(得分:2)

inline或没有inline,C不允许在对同一程序或库有贡献的翻译单元中使用同名的多个外部定义。此外,它不允许在同一翻译单元中对同一名称进行多个定义,无论是内部,外部还是内联。因此,在任何给定的翻译单元中,范围内最多可以有两个给定函数的可用定义:一个内部和/或内联,一个外部。

2011,6.7.4 / 7有这样的说法:

  

任何具有内部链接的函数都可以是内联函数。用于外部功能   链接,以下限制适用:如果声明了一个函数   inline   函数说明符,那么它也应该在同一个翻译单元中定义。如果所有的   翻译单元中函数的文件范围声明包括   inline   功能   说明者没有   extern   那么翻译单元中的定义是一个   排队   定义   。 内联定义不提供函数的外部定义   并且不禁止在另一个翻译单元中使用外部定义。内联定义   提供了外部定义的替代方案,翻译人员可以使用该定义来实现   在同一翻译单元中对该功能的任何调用。 未指定是否致电   函数使用内联定义或外部定义。

(强调补充。)

具体回答你的问题,然后,因为它们与C:

有关
  

最终的可执行文件或共享库中是否包含所有定义?

内联定义不是外部定义。它们可能包含也可能不包含为实际功能,内联代码,两者或两者,具体取决于编译器和链接器的缺陷以及它们的使用细节。它们在任何情况下都不会被来自不同翻译单元的函数按名称调用,因此它们是否应被视为“包含”是一个抽象的问题。

  

对函数的所有调用都是针对相同的定义吗?

C未指定,但它允许答案为“否”,即使对于同一翻译单元内的不同呼叫也是如此。此外,内联函数不是外部函数,因此在一个转换单元中定义的内联函数不会被(在另一个转换单元中定义的函数直接调用)。

  

一个或多个C和C ++ ISO标准是否需要上述问题的答案,如果没有,大多数常见平台都会做同样的事情吗?

我的回答是基于目前的C标准来解决问题,但正如您将看到的那样,这些答案并非完全是规范性的。此外,该标准并未直接解决任何目标代码或链接问题,因此您可能已经注意到我的答案大部分都没有用这些术语表达。

在任何情况下,假设任何给定的C系统在这些方面对于不同的功能或在不同的环境中都是一致的是不安全的。在某些情况下,它可能会内联对内部或内联函数的每次调用,因此该函数根本不会显示为单独的函数。在其他时候,它可能确实发出了具有内部链接的函数,但这并不妨碍它无论如何都要内联对该函数的一些调用。在任何情况下,内部函数都没有资格链接到其他翻译单元的函数,因此链接器根本不必涉及链接它们。

答案 3 :(得分:1)

我认为你的问题的正确答案是"它取决于"。

请考虑以下代码:

文件x.c(或x.cc):

#include <stdio.h>

void otherfunction(void);

inline void inlinefunction(void) {
    printf("inline 1\n");
}

int main(void) {
    inlinefunction();
    otherfunction();
    return 0;
}

档案y.c(或y.cc)

#include <stdio.h>

inline void inlinefunction(void) {
    printf("inline 2\n");
}

void otherfunction(void) {
    printf("otherfunction\n");
    inlinefunction();
}

由于inline关键字只是一个&#34;建议&#34;对于编译内联函数,具有不同标志的不同编译器的行为有所不同。例如。看起来像C编译器总是&#34;出口&#34;内联函数,不允许多个定义:

$ gcc x.c y.c && ./a.out 
/tmp/ccy5GYHp.o: In function `inlinefunction':
y.c:(.text+0x0): multiple definition of `inlinefunction'
/tmp/ccQkn7m4.o:x.c:(.text+0x0): first defined here
collect2: ld returned 1 exit status

而C ++允许它:

$ g++ x.cc y.cc && ./a.out 
inline 1
otherfunction
inline 1

更有趣 - 让我们尝试切换文件的顺序(等等 - 切换链接的顺序):

$ g++ y.cc x.cc && ./a.out 
inline 2
otherfunction
inline 2

嗯......看起来第一个重要!但是......让我们添加一些优化标志:

$ g++ y.cc x.cc -O1 && ./a.out 
inline 1
otherfunction
inline 2

这就是我们所期望的行为。功能得到了内联。不同的文件顺序没有任何改变:

$ g++ x.cc y.cc -O1 && ./a.out 
inline 1
otherfunction
inline 2

接下来,我们可以使用void anotherfunction(void)的原型扩展我们的x.c(x.cc)源代码,并在我们的main函数中调用它。让我们将anotherfunction定义放在z.c(z.cc)文件中:

#include <stdio.h>

void inlinefunction(void);

void anotherfunction(void) {
    printf("anotherfunction\n");
    inlinefunction();
}

这次我们没有定义inlinefunction的正文。 c ++的编译/执行给出以下结果:

$ g++ x.cc y.cc z.cc && ./a.out 
inline 1
otherfunction
inline 1
anotherfunction
inline 1

不同的顺序:

$ g++ y.cc x.cc z.cc && ./a.out 
inline 2
otherfunction
inline 2
anotherfunction
inline 2

优化:

$ g++ x.cc y.cc z.cc -O1 && ./a.out 
/tmp/ccbDnQqX.o: In function `anotherfunction()':
z.cc:(.text+0xf): undefined reference to `inlinefunction()'
collect2: ld returned 1 exit status

所以结论是:最好是将inlinestatic一起声明,这缩小了函数用法的范围,因为&#34;导出&#34;我们希望在线使用的功能毫无意义。

答案 4 :(得分:1)

当内联函数最终没有被内联时,C++ 和 C 之间的行为会有所不同。 在 C++ 中,它们的行为类似于常规函数,但具有允许重复定义的附加符号标志,并且链接器可以选择其中的任何一个。 在 C 中,实际的函数体被忽略,它们的行为就像外部函数一样。

在 ELF 目标上,C++ 所需的链接器行为是用弱符号实现的。

请注意,弱符号通常与常规(强)符号结合使用,其中强符号会覆盖弱符号(这是 Wikipedia article on weak symbols 中提到的主要用例)。它们还可用于实现可选引用(如果未找到定义,链接器将为弱符号引用插入空值)。但是对于 C++ 内联函数,它们恰好提供了我们所需要的:给定多个使用相同名称定义的弱符号,链接器将选择其中一个,在我的测试中,始终是文件中出现在传递给链接器的文件列表中的第一个.

以下是一些示例,分别显示了在 C++ 和 C 中的行为:

$ cat c1.cpp
void __attribute__((weak)) func_weak() {}

void func_regular() {}

void func_external();

void inline func_inline() {}

void test() {
  func_weak();
  func_regular();
  func_external();
  func_inline();
}
                              
$ g++ -c c1.cpp
$ readelf -s c1.o | c++filt  | grep func
11: 0000000000000000    11 FUNC    WEAK   DEFAULT    2 func_weak()
12: 000000000000000b    11 FUNC    GLOBAL DEFAULT    2 func_regular()
13: 0000000000000000    11 FUNC    WEAK   DEFAULT    6 func_inline()
16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func_external()

我们在没有优化标志的情况下编译,导致内联函数没有被内联。我们看到内联函数 func_inline 作为弱符号发出,与使用 GCC 属性显式定义为弱符号的 func_weak 相同。

用C编译同样的程序,我们看到func_inline是一个普通的外部函数,和func_external一样:

$ cp c1.cpp c1.c
$ gcc -c c1.c
$ readelf -s c1.o | grep func
 9: 0000000000000000    11 FUNC    WEAK   DEFAULT    1 func_weak
10: 000000000000000b    11 FUNC    GLOBAL DEFAULT    1 func_regular
13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func_external
14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func_inline

所以在 C 中,为了解析这个外部引用,必须指定一个包含实际函数定义的文件。

当我们使用优化标志时,我们导致内联函数实际上被内联,并且根本没有发出任何符号:

$ g++ -O1 -c c1.cpp
$ readelf -s c1.o | c++filt | grep func_inline
$ gcc -O1 -c c1.c
$ readelf -s c1.o | grep func_inline
$