欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C ++中的内联函数(inline)与宏(#defines)

程序员文章站 2022-03-01 19:50:39
...

Prefer const,enum and linline to #defines(尽量使用const enum inline替换#define).

上面这句话是Scott meyers在EffectIve C++中提到的。

一、先说说#define

先看这句代码:

#define ASPECT 1.35

知道#define的,就知道名称ASPECT在编译时并没有被编译器所看见,也许在编译器开始处理源码之前就被预处理器移走了,所以ASPECT有可能没进入到sysmbol table(记号表)中。

(1).于是当你使用该常量时可能会产生编译错误,而这个错误信息也许会提到1.35而不是ASPECT,如果你在一个公司浩大的工程中调用它,而这个宏又不是你写的,那么你会很蒙蔽这个1.35是什么东西。然而宏不像函数一样,宏是不可调试的

(2).使用宏最大的缺点其实是出错,因为宏定义是预处理器单纯的拷贝代码,不做任何判断且常常会产生意想不到的边界效应。

(3).对于C++而言,宏的另一个缺点是:不能操作类的私有数据成员。

很多人会了提高程序执行效率,会以它实现宏函数,因为不会产生函数调用带来的额外开销

再举一个老生常谈例子,宏夹带着宏实参,调用函数f():

#define MUL(a,b) f(a * b) 

我相信这句代码即使没写过也看过,即使没看过也听人说过。如果调用该宏时是这样的:MUL(1+2,3)那么结果是7,而你期望的结果是9。 正确的写法是:

#define MUL(a,b) f((a) * (b)) 

而无论写什么都加上括号就正确了吗?看下面这句代码:

#define MAX(a,b) f((a) > (b) ? (a) : (b)) 

乍一看没什么毛病,一个三目运算,但你如此调用:

#define MAX(a,b) f((a) > (b) ? (a) : (b)) 

int a =5,b=0;
MUL(++a,b);
MUL(++a,b+10);

发现问题了吗?

当a与b比较的时候++一次,当a大于b的时候又++了一次,而a小于b的时候没++, a的递增次数竟然取决于它被拿来和谁比较

上面粉色加粗字体提到了函数调用的开销,那我也来提一提:

函数调用的开销:

当程序执行函数调用指令时,CPU将存储该函数调用之后的指令的内存地址,将函数的参数复制到堆栈上,最后将控制权转移到指定的函数。然后,CPU执行功能代码,将功能返回值存储在预定义的存储位置/寄存器中,并将控制权返回给调用函数。如果函数的执行时间少于从调用者函数到被调用函数(被调用者)的切换时间,则这可能会成为开销。对于大型函数和/或执行复杂任务的函数,与函数运行所花费的时间相比,函数调用的开销通常微不足道。但是,对于小型的常用功能,进行函数调用所需的时间通常比实际执行函数代码所需的时间多得多。对于小型功能,会发生这种开销,因为小型功能的执行时间小于切换时间。

由此引出接下来要说的内联函数:

内联函数

内联函数是C ++增强功能,可以增加或减少程序的执行时间。可以指示函数编译器使其内联,以便编译器可以在调用它们的任何地方替换这些函数定义。编译器在编译时替换内联函数的定义,而不是在运行时引用函数定义。

C ++提供了一个内联函数,以减少函数调用的开销。内联函数是在调用时在行中扩展的函数。调用内联函数时,将在内联函数调用时插入或替换内联函数的整个代码。此替换由C ++编译器在编译时执行。如果内联函数很小,则可以提高效率。

内联的作用归纳出来就是:

1.通过避免函数调用开销来加速程序。
2.当函数调用发生时,它节省了堆栈上变量push / pop的开销。
3.节省了从函数返回调用的开销。
4.通过利用指令缓存增加了引用的局部性。
5.通过将其标记为内联,可以将函数定义放在头文件中(即,可以将其包含在多个编译单元中,而无需链接程序抱怨)

内联的使用:

Class A
{
 Public:
    inline int add(int a, int b)
    {
       return (a + b);
    };
}

Class A
{
 Public:
    int add(int a, int b);
};

inline int A::add(int a, int b)
{
   return (a + b);
}

内联函数放入头文件内

关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

//非内联
inline void Foo(int x, int y);   // inline 仅与函数声明放在一起   
void Foo(int x, int y)          
{
    //...
} 
//内联
void Foo(int x, int y);   
inline void Foo(int x, int y)   // inline 与函数定义体放在一起

所以说,C++ inline函数是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了 inline 关键字,但我认为 inline 不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

但是编译器是否将它真正内联则要看 Foo函数如何定义

内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline 函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。

当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。

之所以上面红字加注的(增加或减少)就是有人会误认为什么时候都可以用inline

注意:

内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:

(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

    类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。”

Google C++编码规范中则规定得更加明确和详细:

内联函数:

Tip: 只有当函数只有 10 行甚至更少时才将其定义为内联函数.

定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用. 
优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联. 
缺点: 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。 
结论: 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用! 
另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行). 
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.