Effective C++笔记之十五:inline函数的里里外外

1.inline函数简介
inline函数是由inline关键字来定义,引入inline函数的主要原因是用它替代C中复杂易错不易维护的宏函数。

2.编译器对inline函数的处理办法
inline对于编译器而言,在编译阶段完成对inline函数的处理。将调用动作替换为函数的本体。但是它只是一种建议,编译器可以去做,也可以不去做。编译器对inline函数的处理步骤一般如下: 
(1)将inline函数体复制到inline函数调用点处; 
(2)为所用inline函数中的局部变量分配内存空间; 
(3)将inline函数的的输入参数和返回值映射到调用方法的局部变量空间中; 
(4)如果inline函数有多个返回点,将其转变为inline函数代码块末尾的分支(使用GOTO)。

//求0-9的平方
inline int inlineFunc(int num)
{
    if(num>9||num<0)
        return -1;
    return num*num;
}

int main(int argc,char* argv[])
{
    int a=8;
    int res=inlineFunc(a);
    cout<<"res:"<<res<<endl;
}

//inline之后的main函数代码类似于如下形式:
int main(int argc,char* argv[])
{
    int a=8;
    {
        int _temp_b=8;
        int _temp;
        if (_temp_q >9||_temp_q<0) _temp = -1;
        else _temp =_temp*_temp;
        b = _temp;
    }
}

经过以上处理,可消除所有与调用相关的痕迹以及性能的损失。inline通过消除调用开销来提升性能。

3.inline函数使用的一般方法
函数定义时,在返回类型前加上关键字inline即把函数指定为内联,函数申明时可加也可不加。但是建议函数申明的时候,也加上inline,这样能够达到”代码即注释”的作用。

inline如果只修饰函数的申明的部分,不能成为内联函数
4.inline函数的优点与缺点

有点:

(1)内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。

(2)内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。 
(3)在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。

(4)内联函数在运行时可调试,而宏定义不可以。

缺点: 
(1)代码膨胀。 
inline函数带来的运行效率是典型的以空间换时间的做法。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

(2)inline函数无法随着函数库升级而升级。 
如果f是函数库中的一个inline函数,使用它的用户会将f函数实体编译到他们的程序中。一旦函数库实现者改变f,所有用到f的程序都必须重新编译。如果f是non-inline的,用户程序只需重新连接即可。如果函数库采用的是动态连接,那这一升级的f函数可以不知不觉的被程序使用。

(3)是否内联,程序员不可控。 
inline函数只是对编译器的建议,是否对函数内联,决定权在于编译器。编译器认为调用某函数的开销相对该函数本身的开销而言微不足道或者不足以为之承担代码膨胀的后果则没必要内联该函数,若函数出现递归,有些编译器则不支持将其内联。

5.inline函数的注意事项
(1)使用函数指针调用内联函数将会导致内联失败。 
也就是说,如果使用函数指针来调用内联函数,那么就需要获取inline函数的地址。如果要取得一个inline函数的地址,编译器就必须为此函数产生一个函数实体,那么就内联失败。

(2)如果函数体代码过长或者有多重循环语句,if或witch分支语句或递归时,不宜用内联。

(3)类的constructors、destructors和虚函数往往不是inline函数的最佳选择。 
类的构造函数(constructors)可能需要调用父类的构造函数,析构函数同样可能需要调用父类的析构函数,二者背后隐藏着大量的代码,不适合作为inline函数。虚函数(destructors)往往是运行时确定的,而inline是在编译时进行的,所以内联虚函数往往无效。如果直接用类的对象来使用虚函数,那么对有的编译器而言,也可起到优化作用。

(4)内联函数是定义在头文件还是源文件的建议。 

内联展开是在编译时进行的,只有链接的时候源文件之间才有关系。所以内联要想跨源文件必须把实现写在头文件里。如果一个inline函数会在多个源文件中被用到,那么必须把它定义在头文件中。参考如下示例:

// base.h
class Base{protected:void fun();};
 
// base.cpp
#include base.h
inline void Base::fun(){}
 
//derived.h
#include base.h
class Derived: public Base{public:void g();};
 
// derived.cpp
void Derived::g(){fun();} //VC2010: error LNK2019: unresolved external symbol
上面这种错误,就是因为内联函数fun()定义在编译单元base.cpp中,那么其他编译单元中调用fun()的地方将无法解析该符号,因为在编译单元base.cpp生成目标文件base.obj后,内联函数fun()已经被替换掉,编译器不会为fun()生成函数实体,链接器自然无法解析。所以如果一个inline函数会在多个源文件中被用到,那么必须把它定义在头文件中。

这里有个问题,当在头文件中定义内联函数,那么被多个源文件包含时,如果编译器因为inline函数不适合被内联时,拒绝将inline函数进行内联处理,那么多个源文件在编译生成目标文件后都将各自保留一份inline函数的实体,这个时候程序在连接阶段就会出现重定义错误。解决办法是在需要inline的函数使用static。

//test.h
static inline int max(int a,int b)
{
    return a>b?a:b;
}
事实上,inline函数具有内部链接特性,所以如果实际上没有被内联处理,也不会报重定义错误,因此使用static修饰inline函数有点多余。

(5)能否强制编译器进行内联操作? 
不能强制编译器进行函数内联,如果使用的是MSVC++, 注意__forceinline如同inine一样,也是一个用词不当的表现,它只是对编译器的建议比inline更加强烈,并不能强制编译器进行inline操作。

(6)如何查看函数是否被内联处理了? 
实际在VS2012中预处理了一下,查看预处理后的.i文件,inline函数的内联处理不是在预处理阶段,而是在编译阶段。编译源文件为汇编代码或者反汇编查看有没有相关的函数调用call,如果没有就是被inline了。

(7)C++类成员函数定义在类体内为什么不会报重定义错误? 
类成员函数定义在类体内,并随着类的定义放在头文件中,当被不同的源文件包含,那么每个源文件都应该包含了类成员函数的实体,为何在链接的过程中不会报函数的重定义错误呢?

原因是:在类里定义时,这种函数会被编译器编译成内联函数,在类外定义的函数则不会。

可能存在的疑问:类体内的成员函数被编译器内联处理,但并不是所有的成员函数都会被内联处理,比如包含递归的成员函数。但是实际测试,将包含递归的成员函数定义在类体内,被不同的源文件包含并不会报重定义错误,为什么会这样呢?

如果编译器发现被定义在类体内的成员函数无法被内联处理,也不会出现重定义的错误,因为C++中存在5种作用域的级别,分别是文件域(全局作用域)、命名空间域、类域、函数作用域和代码块作用域(局部域)。当类成员函数被定义在类体内,那么其作用域也就被限制在类域,当然定义在类体外的函数作用域也是属于类域的。显然并不是因为作用域的原因而不会产生重定义的错误。

那么原因究竟是什么呢?其实很简单,类体内定义的成员函数就是inline函数,即使不被内联处理,inline函数的特性就是不具有外部连接性。所以并不会与其他源文件中的同名类域中的成员函数发生冲突,也就不会造成重定义的错误。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值