《C++之那些年踩过的坑(二)》

本系列文章针对我在写C++代码的过程中,尤其是做自己的项目时,踩过的各种坑。以此作为给自己的警惕。

今天讲一个小点,虽然小,但如果没有真正理解它,没有真正熟悉它的里里外外,是很容易出错的 —— inline。

关于一些简单的介绍和使用,可以先看我 这篇笔记。接下来进入正题。

一、如何使用 inline?

你知道,inline 函数可以减小函数调用的开销,你可能会想,嗯,我这个函数那么短,我把它声明为 inline,可以提高程序运行的效率!考虑这样一个例子:

// A.h
// A.cc
// main.cc
#include "A.h"
int main
{
  A a;
  a.foo(1);
  a.bar(1);
}

首先,你知道,①inline 需要看到函数实体,所以要跟定义放在一起。于是你想在 A.cc 中在为 foo 的定义加上一个 inline :

inline void A::foo(int i)

然后开心的编译运行,WTF!!!编译器居然报错了?!!不就加了个 inline 吗!仔细观察编译器给的出错信息,如果你用的是VS,那么你大概会看到这样的信息: error LNK2019: 无法解析的外部符号……如果你用的是GCC,你会发现当你使用

g++ -c main.cc

时(即编译),是不会产生任何错误的,然后当你使用

g++ main.o -o a.out

时(即链接),就报错了。说明,这是链接的时候出错了。在这里要说明一下,大多数的建置环境都是在编译过程进行 inlining(为了替换函数调用,编译器需要知道函数的实体长什么样,这解释了①),某些可以在连接期完成,少数的可以在运行期完成。我们只考虑绝大部分情况:Inlining 在大多数C++程序中是编译期行为。

如果你想学习C/C++可以来这个群,首先是三三零,中间是八五九,最后是七六六,里面有大量的学习资料可以下载。

大部分函数默认的就是外部链接,也就是外部可以访问,而 inline 函数默认具有内部链接,也就是对本文件可见,对其它文件不可见。那么自然我们在 main.cc 中调用它,没法看到它的定义,于是就出现了连接错误。OK,你学到了 ②一般 inline 需要放在头文件中

那 bar 函数确实是定义在头文件中了,这下你想声明为 inline 总没问题了吧!确实,这没有问题,不过,它如你所想提高效率了吗?我们可以探究一下。在vs下可以用调试看反汇编,现在用GCC分别运行以下命令:

g++ -E main.cc -o main.i
g++ -S main.i -o main.s
g++ -O2 -S main.i -o main2.s

我们来看一下 main.s 中的主要部分:

    call    ___main
    leal    -9(%ebp), %eax
    movl    $1, (%esp)
    movl    %eax, %ecx
    call    __ZN1A3fooEi
    subl    $4, %esp
    leal    -9(%ebp), %eax
    movl    $1, (%esp)
    movl    %eax, %ecx
    call    __ZN1A3barEi
    subl    $4, %esp
    movl    $0, %eax
    movl    -4(%ebp), %ecx

我们再看一下 main2.s 中的这个部分:

    call    ___main
    leal    -9(%ebp), %ecx
    movl    $1, (%esp)
    call    __ZN1A3fooEi
    subl    $4, %esp
    movl    $2, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    movl    -4(%ebp), %ecx

在不开优化的情况下,程序诚实的执行,开O2优化的情况下,我们已经看不到 bar 函数的调用了。不过这真的是拜你加的 inline 所赐的吗?为了验证,我们去掉 inline,再次重复上面的过程,然后你就会发现,重新生成的两份汇编代码还是 一模一样 。定义在类内的函数,是自动 inline 的,LLVM CodingStandards也告诉我们:③不要给定义在类中的函数使用 inline

你可能已经急坏头了,这里不给用,那里不给用,行行行,头文件是吧,不在类内是吧,我在类外!于是你改成了这样的代码(虽然很少人这样写,但万一呢?还别说我就干过= =!):

// A.h

嗯,这没问题。把这份代码用刚才的方法,得到的汇编代码还是一样的。若你现在想尝试把 inline 去掉,看看会有什么变化,你经过刚刚的事情,大概会预测,应该还是会得到相同的汇编代码吧!编译器应该会帮你去优化!如果你不想尝试,好的,我帮你。WTF!!!编译器又报错了??发生了什么??

二、什么时候应该使用 inline?

嗯,终于我们来到了第二个问题,我们发现,在这种情况下,去掉 inline ,居然无法通过编译了!它给出来的错误信息是:重定义的符号。让我们冷静下来,想一想,然后你就会恍然大悟:一个函数可以有多次声明,但只能有一次定义,而我们定义在 A.h 的 bar 函数的定义,被 A.cc 和 main.cc 都包含了一遍!所以就出现了重定义的错误!Ok,类成员函数我们也许不会这样写,要么放到类内,要么放到源文件中,当然模板类的情况我们稍后再谈。不过对于普通的函数,我们就很容易忽略了。例如,我们想在 A.h 中加一个函数:

// A.h

它很短,要不要使用 inline 呢?经过刚刚的问题,你应该会谨慎的想到,这里,要使用 inline ,不然就会出错。非常好,你的想法是对的。原因跟上面提到的是一样的。当然,它不是非得使用 inline 不可,使用 static 事实上也可以解决这个问题,在这里只是为了说明 inline 可以使用的情况。

但当你用上模板时,情况发生了改变。若你把这个 max 函数改成一个模板函数:

template 
T max(const T& a, const T& b)
{
  return a < b ? b : a;
}

这个时候,无论你有没有使用 inline,它都是可以运行的。这是因为,模板是具有“内联”语义的。所以,类模板,函数模板,类函数模板,都不需要加 inline 。回到正题,什么时候可以使用 inline 呢?④使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时

三、 inline 的真正意义?

现在你该好好的思考,什么是 inline,是内联吗?inline 的意义是什么,是发起一个内联请求吗?

你认为加了 inline 能提高程序的运行效率,但是并不会有这样的变化。有的时候,你不加 inline,却会出错,这是为什么呢?

好好的想想。

inline,跟 static , extern 一样,都是链接指令,它在很久很久以前,是作为给编译器优化的提示符。而 inline 的含义是非绑定的,编译器可以自由的选择、决定是否 inline 一个函数。如今,编译器根本不需要这样的提示,如果它认为一个函数值得 inline,它会自动 inline,否则,即使你 inline 了,它也会拒绝。有很多地方,都会隐式 inline 。在这篇SO中,有一段话:

It is said that inline

  • static

  • extern

  • inline

看完你应该差不多能理解了。现在的编译器,并不需要你用 inline 提醒,所以 ⑤当且仅当你认为使用 inline 会加快程序运行效率时,不要使用 inline 。inline 这个关键字,在C++里就是一个骗局。它真正的意义并不是去内联一个函数,而是表示别怕!无论你看到了多少个定义,但实体就我一个! 在Reference中有有这样一句话:

Because the meaning of the keyword inline for functions came to mean "multiple definitions are permitted" rather than "inlining is preferred", that meaning was extended to variables.

翻译过来就是 ⑥ inline 的含义是 “允许多重定义” 而不是 “去内联函数” 。全文基于 C++17 及以前的讨论。

四、总结

1、inline 需要看到函数实体,所以要跟定义放在一起

2、一般 inline 需要放在头文件中

3、不要给定义在类中的函数使用 inline

4、使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时

5、当且仅当你认为使用 inline 会加快程序运行效率时,不要使用 inline

6、inline 的含义是 “允许多重定义” 而不是 “去内联函数”

7、模板不需要声明 inline,也具有 inline 的语义

※注:以上总结建立在不熟悉、不够了解 inline 的情况下。若在使用 inline 的时候,很明白很清楚在做什么,会发生什么,那怎么用都可以啦,有时候用了就是好看!!

你可能感兴趣的:(C++)