inline多次定义的用法

作者:Tim Shen
链接:zhihu
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

能定义不止一次的好处是方便你放到头文件里,放到头文件里的好处是每个include这个头文件的.c文件都能看到函数体,看到函数体的好处是编译器可以内联。内联的好处是代码变快了。另外,所有函数体定义必须一模一样,不然出了问题概不负责。constexpr自带inline属性。当你下决心在.c文件中定义函数体的时候,自然不需要inline关键字了。而这时候也必须link相应的.o来确保找得到定义。

首先来几个前置知识:

  1. C和C++都有translation unit(或compilation unit)的概念:基本上编译器会一次编译一个文件,然后忘记一切,然后再编译下一个文件。哪怕你写gcc -c a.c b.c,其实和gcc -c a.c && gcc -c b.c大体上是没区别的。在最后,所有的.o文件都被linker汇总link成一个大的可执行文件。
  2. static function。你可以把一个函数标记为static(也称为internal linkage),这样该函数的symbol就会被隐藏,从而该函数只存在在当前translation unit。换了下一个translation unit之后,该函数被忘得一干二净。linker也从来不知道这函数存在过。这时候你就算再定义一次上次translation已经定义过的static函数,linker也不会报redefinition错误。当然,这样代码在binary中就出现了多次。
  3. 当然你也肯定知道C和C++的include的意思:在A中#include 就是把B的内容复制粘贴到A中#include对应的位置。
  4. 编译器的内联优化就是看到你在Bar里调用Foo的时候,帮你复制一遍Foo的函数体,内嵌到Bar里去,同时消除栈操作的开销(因为代码已经被复制到“本地”了嘛,不需要跳来跳去了)。内联优化有个缺陷,就是在同一个translation unit里一定要看到函数体,所以光看到declaration是没用的。

现在考虑这么个问题:

传统的在头文件中声明,在一个文件(.c)中实现函数体的方式有时执行太慢了。为什么慢呢,假设我这个函数就一行,但是函数调用的压栈传参数弹栈跳转等指令占了大部分开销,真是太不合算了。这时候在传统C里面有两个解决方案:
1) “宏函数”。就是把本来藏在.c文件里的函数体放到一个宏里面去,当然宏也在头文件里。然后大家include头文件的时候就把宏也include走了,使用宏的时候就把这段代码一遍遍展开到所有使用的地方,消除了函数调用的开销。
2) 在编译器支持内联优化的情况下,在头文件里定义static function。任何别的.c文件,只要include了你的头文件,都对你的头文件做了一次复制粘贴,自动获得了该static function的函数体。所以在不同的translation unit里面,这些函数并不冲突,因为它们是static的。值得一提的是,这些函数体不一定一模一样。举例来说:

 // a.h    
#define FOO 3
static int Foo() { return FOO; }

// a.c
#include "a.h"

// b.c
#undef FOO
#define FOO 2
#include "a.h"

在不同的translation unit里面一个Foo返回3一个返回2。
1) 的坏处很明显,宏不能解决类型检查的问题,宏是dynamic scope(变量检查环境都是调用端而非定义端的)的,宏是textual substitution,搞不好有迷之编译不通过,宏很丑,定义不带语法高亮(雾),等等。
2) 看上去很好诶,写的是真正的函数,编译器还有能力内联。其缺陷是在编译器决定不内联的时候(通常这时候函数很大),每个translation unit中都定义了一个很大的函数,造成了不必要的binary size bloat。
这时候C++之父Bjarne Stroustrup站出来了,说我们在C++里搞个inline关键字吧!这个关键字不仅编译器认识,而且编译器在没有真正内联该函数时,会通过某种方式提示linker说这个函数被标记为“可重复定义”耶 - 根据我用gcc的实验,生成的是一个weak symbol。当linker看到一个weak symbol,会把函数名写在一个小本本上。在linker最后把所有文件link到一起的时候,它会把小本本扫一遍,对于同名函数只留一个,别的函数连带函数体统统删掉。这样就解决了binary size bloat的问题。当然这只是一种典型实现方式,并非一定如此。
另外,在编译器真正内联了该函数的时候,效果就和static一样了,这也是为什么你的代码里找不到定义 - 因为linker根本看不到static函数。话虽这么说,但是他们不管这个叫internal linkage(inline specifier),因为此时linkage是个implementation detail。语言只是强调:The definition of an inline function must be present in the translation unit where it is called (not necessarily before the point of call)
在前面提到,用include static functions的方式中include进来的函数体可能不完全一样。inline此处也提到,你要是同名函数的函数体长得不一样,我才不告诉你我要留哪一份删哪几份呢。你要是敢这么做,我不保证我的输出有意义。这个在C++里叫做ODR violation (Definitions and ODR)。编译器一次只看一个translation unit,所以通常是没法检测ODR violation的(不排除LTO还是能查的),而linker也不查,我并不清楚为什么,大概是太昂贵吧。
另外,可以感受下当年inline关键字的marketing口号:“An Inline Function is As Fast As a Macro”(Inline - Using the GNU Compiler Collection (GCC))
顺带建议一下刷ACM-ICPC的各种坑爹oj的同学,想要速度就用宏,因为oj可能不开内联优化。。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值