跳转至

inline错误解决

C/C++中的inline关键字

https://blog.shengbin.me/posts/inline-keyword-in-c-c++

https://zhuanlan.zhihu.com/p/28673806

https://www.zhihu.com/question/270847649/answer/358472568

在C/C++中,內联(inline)指的是在使用函数的地方不进行函数调用,而是将函数的实现代码插入到此处。 这样能够以增加代码大小为代价,省下函数调用过程产生的开销,加快程序执行速度。 內联属于编译器的一个优化措施,而inline关键字就是用来告诉编译器,希望对指定的函数做內联优化。

所谓“希望”,意思就是这仅仅是程序员对编译器的优化建议,并不能强制编译器必须将指定的函数內联。 因此,如果一定要将一个函数內联,用inline关键字是不行的,需要使用编译器扩展或配合编译器优化选项。

早期版本的C语言标准,例如C89,并没有inline关键字。C++语言先引入了这个关键字,后来的C语言标准,如C99,将其借鉴了进来。 在inline关键字进入C语言标准之前,很多C编译器,例如GCC,已经把它作为一项编译器扩展支持了。 但问题是,这些编译器扩展中inline关键字的意义与后来的C99标准并不一致。这就导致了代码兼容性的问题。

Clang是一个符合C99标准的编译器。在它的语言兼容性页面,专门针对inline关键字做了说明。 在C99中,被标为inline的函数定义只是用来內联使用的,并不提供该函数的一个外部定义。也就是说,如果在使用函数的地方编译器没有內联,那么程序就必须在别处提供一个该函数没inline标记的定义以供调用。 否则,链接时会报“找不到符号”的错误。例如,下面的C程序用Clang编译时就会报错:

Text Only
inline int add(int i, int j) { return i + j; }

int main() {
  int i = add(4, 5);
  return i;
}

GCC(在5.0版本之前)默认采用GNU89模式,也就是C89标准加上它自己的一些扩展。inline关键字在GNU89并没有C99那样“只是用来內联使用”的含义,因此用GCC编译上述程序就没有问题。

要让上述程序被Clang编译通过,有以下几个方法:

  1. 给add函数添加static关键字(前提是这个函数只在当前文件使用,这样的话编译器就知道它不需要一个外部定义了);
  2. 去掉inline关键字;
  3. 另写一个没有inline关键字的add函数(为避免重复定义,另写的是函数声明);
  4. 给Clang加上–std=gnu89的选项。

维基百科上列出了使用inline关键字可能带来的一些问题,并指出它并没有那么值得用。 我自己没有使用过这个关键字,而且以后也不会用。是否內联优化一个函数还是交给编译器自己决定吧,程序员可以做些更有意义的事情。

我们先来思考一个问题:既然 inline 只是对编译器作出的「指导性意见」,那么假如编译器拒绝内联任何东西,外部链接(external linkage)的内联函数会在每个包含它的翻译单元中生成一份定义。换作普通函数,多份定义会导致链接时报符号重复,为什么内联函数就不会呢?

C++ 给出的答案是:如果出现重复的内联函数定义,禁止链接器报符号重复,任选一个就好了。

这种做法有那么一点点坏味道:

  • 把烂摊子扔给了链接器
  • 如果不同翻译单元中给出了不同的定义,没有要求给出错误提示

而 C 语言则走的是另一条路:允许函数拥有「外部定义」(external definition)和「内联定义」(inline definition)两种定义。

  • 如果翻译单元中所有对于某个内联函数的声明都只有 inline(没有 static 和 extern),那么该翻译单元中的定义就是内联定义
  • 外部定义就是我们认为的普通的函数定义
  • 内联定义仅在当前翻译单元有效;不同翻译单元之间,允许同时存在外部定义和内联定义
  • 遇到函数调用时,如果对应的内联定义可见,编译器自行决定使用外部定义还是内联定义

也就是说,C 语言中 inline 的正确打开方式是:

  • 将 inline 版本的函数写在 xxx.h 文件里
  • 在 xxx.c 中提供对应的普通版本,或者包含 xxx.h 后将其声明为 extern inline(这样在 xxx.c 这个翻译单元中,就存在 extern inline 的声明,从头文件中包含进来的定义就不再是内联定义,而是外部定义。)

(当然你也可以 static inline,不过那就是另一回事了。)

C
// main.c
#include <stdio.h>
#include "foo.h"

int main()
{
    printf("foo: %d\n", foo());
}


// foo.h
#ifndef FOO_H_
#define FOO_H_

inline int foo()
{
    return 42;
}

#endif  // FOO_H_


// foo.c
#if 1    // 方法一:直接写个 extern inline 完事

#include "foo.h"
extern inline int foo();

#else    // 方法二:自己写个完全不同的外部定义

extern inline int foo()
{
    return 2333;
}

#endif

个人感觉,C 语言这种通过修改游戏规则解决问题的做法,比 C++ 打补丁似的特殊处理要舒服一点。同时也带来了更多的可能性,比如,你肯定已经注意到了,内联定义完全可以和外部定义不同。那么打个比方,在编写函数库时,你就可以对外提供非 inline 版本,库内部使用 inline 版本。对外的非 inline 版本还可以额外加入参数检查之类的功能。


回到题目,由于示例代码中的 f 只用了 inline 修饰,它是「内联定义」。

  • 在较低优化等级下,由于未启用内联,所以编译器决定 main 函数中应该调用 f 的「外部定义」版本,而代码中实际未提供,所以报错
  • 在 -O2 下,由于开启了内联,所以编译器决定 main 函数中应该使用 f 的「内联定义」版本,所以可以通过编译和链接
  • 在 C++ 模式下,一切都按照 C++ 的规则来,没有那么多事,所以可以通过编译和链接

至于 gcc 里各种开关对结果的影响,针对 inline,gcc 有三套不同的语义:

  1. 开启了 -std=gnu89 / 开启了 -fgnu89-inline / 使用了 gnu_inline 属性:使用 gcc 自己的 C 扩展规则(大致就是和 C99 的选择相反,更像 C++ 一点,不过不建议自己没事去了解这种)
  2. 开启了 -std=c99 / 开启了 -std=gnu99 / 使用了更靠后的 C 版本:使用 C99+ 标准中的规则
  3. C++ 模式:不解释