1. 背景
函数是一个可以重复使用的代码块,CPU 会一条一条地执行其中的代码。CPU 在执行主调函数代码时如果遇到了被调函数,主调函数就会暂停,转而执行被调函数的代码;被调函数执行完毕后再返回到主调函数,主调函数根据刚才的状态继续往下执行。
一个 C/C++ 程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条,这个链条的起点是main(),终点也是main()。当main()调用完了所有的函数,它会返回一个值(例如 return 0;
)来结束自己的生命,从而结束整个程序。
函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
栈空间就是指放置程式的局部数据也就是函数内数据的内存空间,在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足所造成的程式出错的问题,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。
如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视。
为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(inline function),又称内嵌函数或者内置函数。
2. 内联函数的定义
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline
,在调用函数之前需要对函数进行定义。
在类定义中的定义的函数都是内联函数,即使没有使用 inline
说明符。
3. 内联函数的注意事项
关键字 inline
必须与函数定义体放在一起才能使函数成为内联。在函数声明处添加 inline
关键字虽然没有错,但这种做法是无效的,编译器会忽略函数声明处的 inline
关键字。
如下风格的函数 Foo
不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{
}
而如下风格的函数 Foo
则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{
}
所以说,inline
是一种 “用于实现的关键字” ,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了 inline
关键字,但 inline
不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量 C/C++ 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
4. 内联函数的优点
- 内联通过避免函数调用所带来的开销来提高程序的运行速度。
- 当发生函数调用时,它节省了变量弹栈、压栈的开销。
- 避免了一个函数执行完返回原现场的开销。
- 通过将函数声明为内联函数,可以把函数定义放在头文件内。
5. 内联函数的缺点
- 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。
- 另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大。
- C++ 内联函数的展开是中编译阶段,这就意味着如果你的内联函数发生了改动,那么就需要重新编译代码。
- 当内联函数放在头文件中时,这将会使头文件信息变多,不过头文件的使用者不用在意这些。
- 有时候内联函数并不受到青睐,比如在嵌入式系统中,嵌入式系统的存储约束可能不允许体积很大的可执行程序。
「注意」:一般只将那些短小的、频繁调用的函数声明为内联函数。
6. 内联函数的使用场景
使用:
- 当对程序执行性能有要求时,那么就可以适当使用内联函数。
- 当你想宏定义一个函数时,使用内联函数。
- 写一些功能专一且性能关键的函数,这些函数的函数体不大,包含了很少的执行语句。通过
inline
声明,编译器不需要跳转到内存其他地址去执行函数调用,也不需要保留函数调用时的现场数据。 - 在类内部定义的函数会默认声明为
inline
函数,这有利于类实现细节的隐藏。(但也需要斟酌,如果不需要隐藏的时候,其实大部分是不推荐默认inline
的)。
不使用:
- 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
- 如果函数体内出现循环或者开关语句,那么执行函数体内代码的时间要比函数调用的开销大。
7. 编译器对内联函数的处理
对函数作 inline
声明只是程序员对编译器提出的一个建议,而不是强制性的,并非一经指定为 inline
编译器就必须这样做。编译器有自己的判断能力,它会根据具体情况决定是否这样做。一个好的编译器将会根据函数的定义体,自动取消不值得的内联(这进一步说明了 inline
不应该出现在函数的声明中)。具体是否会被编译器优化为内联也要看优化级别。
有些函数即使声明为内联的也不一定会被编译器内联,这点很重要。比如虚函数和递归函数就不会被正常内联。通常,递归函数不应该声明成内联函数。(递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数)。虚函数内联的主要原因则是想把它的函数体放在类定义内,为图方便,或是当作文档描述其行为,比如精短的存取函数。
将内联函数放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。而之所以声明跟定义要一致,其实是指,如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为,也就是说,如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中。