C++内联函数(Inline)

来源:互联网 发布:agv导航算法 编辑:程序博客网 时间:2024/05/21 11:19

介绍内联函数之前,有必要介绍一下预处理宏。内联函数的功能和预处理宏的功 
能相似。相信大家都用过预处理宏,我们会经常定义一些宏,如 
 
#define TABLE_COMP(x) ((x)>0?(x):0)  
   
就定义了一个宏。 
 
  为什么要使用宏呢?因为函数的调用必须要将程序执行的顺序转移到函数所存放 
在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地 
方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现 
场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销 
,于是将影响其效率。而宏只是在预处理的地方把代码展开,不需要额外的空间和时 
间方面的开销,所以调用一个宏比调用一个函数更有效率。  
 
  但是宏也有很多的不尽人意的地方。 
 
  1、.宏不能访问对象的私有成员。 
 
  2、.宏的定义很容易产生二意性。 
 
  我们举个例子: 
 
#define TABLE_MULTI(x) (x*x)  
 
  我们用一个数字去调用它,TABLE_MULTI(10),这样看上去没有什么错误,结果返 
回100,是正确的,但是如果我们用TABLE_MULTI(10+10)去调用的话,我们期望的结果 
是400,而宏的调用结果是(10+10*10+10),结果是120,这显然不是我们要得到的 
结果。避免这些错误的方法,一是给宏的参数都加上括号。 
 
#define TABLE_MULTI(x) ((x)*(x))  
 
  这样可以确保不会出错,但是,即使使用了这种定义,这个宏依然有可能出错, 
例如使用TABLE_MULTI(a++)调用它,他们本意是希望得到(a+1)*(a+1)的结果,而实 
际上呢?我们可以看看宏的展开结果: (a++)*(a++),如果a的值是4,我们得到的结 
果是5*6=30。而我们期望的结果是5*5=25,这又出现了问题。事实上,在一些C的库函 
数中也有这些问题。例如: Toupper(*pChar++)就会对pChar执行两次++操作,因为Toupper 
实际上也是一个宏。  
 
  我们可以看到宏有一些难以避免的问题,怎么解决呢? 
 
  下面就是用我要介绍的内联函数来解决这些问题,我们可以使用内联函数来取代 
宏的定义。而且事实上我们可以用内联函数完全取代预处理宏。 
 
  内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编 
译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数 
像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函 
数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。 
 
  我们可以用Inline来定义内联函数,不过,任何在类的说明部分定义的函数都会 
被自动的认为是内联函数。 
 
  下面我们来介绍一下内联函数的用法。 
 
  内联函数必须是和函数体申明在一起,才有效。像这样的申明Inline Tablefunction 
(int I)是没有效果的,编译器只是把函数作为普通的函数申明,我们必须定义函数 
体。 
 
Inline tablefunction(int I) {return I*I};  
 
  这样我们才算定义了一个内联函数。我们可以把它作为一般的函数一样调用。但 
是执行速度确比一般函数的执行速度要快。 
 
  我们也可以将定义把在类的外部的函数定义为内联函数(这里有个问题就是在子类中访问的时候出现连接错误!),比如: 
 
Class TableClass{ 
 Private: 
  Int I,j; 
 Public:  
  Int add() return I+j;}; 
  Inline int dec() return I-j;} 
  Int GetNum(); 

inline int tableclass::GetNum(){ 
return I; 
 
 
  上面申明的三个函数都是内联函数。在C++中,在类的内部定义了函数体的函数 
,被默认为是内联函数。而不管你是否有inline关键字。
 
 
  内联函数在C++类中,应用最广的,应该是用来定义存取函数。我们定义的类中 
一般会把数据成员定义成私有的或者保护的,这样,外界就不能直接读写我们类成员 
的数据了。对于私有或者保护成员的读写就必须使用成员接口函数来进行。如果我们 
把这些读写成员函数定义成内联函数的话,将会获得比较好的效率。 
 
Class sample{ 
 Private: 
  Int nTest; 
 Public: 
  Int readtest(){ return nTest;} 
 Void settest(int I) {nTest=I;} 
 
 
  当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果, 
内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数 
。这样,内联函数就和普通函数执行效率一样了。
 

 

在C++中,为了解决一些频繁调用的小涵数大量消耗栈空间或者是叫栈内存的问题,特别的引入了inline修饰符,表示为内联涵数。

  可能说到这里,很多人还不明白什么是栈空间,其实栈空间就是指放置程序的局部数据也就是函数内数据的内存空间,在系统下,栈空间是有限的,如果频繁大量的使用就会造成因栈空间不足所造成的程序出错的问题,涵数的死循环递归调用的最终结果就是导致栈内存空间枯竭。

  下面我们来看一个例子  
 
#include <iostream> 
#include <string> 
using namespace std; 
 
inline string dbtest(int a); //函数原形声明为inline即:内联涵数 
 
 
void main() 
{ 
 for (int i=1;i<=10;i++) 
 { 
  cout << i << ":" << dbtest(i) << endl; 
 } 
 cin.get(); 
} 
 
string dbtest(int a)
//这里不用再次inline,当然加上inline也是不会出错的 
{ 
 return (a%2>0)?"0":"1"; 
}  

上面的例子就是标准的内联涵数的用法,使用inline修饰带来的好处我们表面看不出来,其实在内部的工作就是在每个for循环的内部所有调用dbtest(i)的地方都换成了(i%2>0)?"0":"1"这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。

  说到这里很多人可能会问,既然inline这么好,还不如把所谓的函数都声明成inline,嗯,这个问题是要注意的,inline的使用是有所限制的,inline只适合涵数体内代码简单的涵数使用,不能包含复杂的结构控制语句例如while、switch,并且内联函数本身不能直接调用递归函数(自己内部还调用自己的函数)。

  说到这里我们不得不说一下在c语言中广泛被使用的#define语句,是的define的确也可以做到inline的这些工作,但是define是会产生副作用的,尤其是不同类型参数所导致的错误,由此可见inline有更强的约束性和能够让编译器检查出更多错误的特性,在c++中是不推荐使用define的。 
 在VC import  dll 的时候,发现tli文件里面有很多的inline 修饰符.,查了一下,原来是这样用的啊

其他的一些解释

(.tlh),它包含了列举的类型和对类型库中对象的定义;

(.tli)对类型库对象模型中的方法产生封装。

Namespace(名字空间)用来定义一个名字空间,使用using就可以将当前的类型上下文转换名字空间所定地.

#import  "c:\xx.dll" 应该放在

{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately 之后

end if  之前 

条款33: 明智地使用内联

内联函数------多妙的主意啊!它们看起来象函数,运作起来象函数,比宏(macro)要好得多(参见条款1),使用时还不需要承担函数调用的开销。你还能对它们要求更多吗?

然而,你从它们得到的确实比你想象的要多,因为避免函数调用的开销仅仅是问题的一个方面。为了处理那些没有函数调用的代码,编译器优化程序本身进行了专门的设计。所以当内联一个函数时,编译器可以对函数体执行特定环境下的优化工作。这样的优化对"正常"的函数调用是不可能的。

我们还是不要扯得太远。程序世界和现实生活一样,从来就没有免费的午餐,内联函数也不例外。内联函数的基本思想在于将每个函数调用以它的代码体来替换。用不着统计专家出面就可以看出,这种做法很可能会增加整个目标代码的体积。在一台内存有限的计算机里,过分地使用内联所产生的程序会因为有太大的体积而导致可用空间不够。即使可以使用虚拟内存,内联造成的代码膨胀也可能会导致不合理的页面调度行为(系统颠簸),这将使你的程序运行慢得象在爬。(当然,它也为磁盘控制器提供了一个极好的锻炼方式:))过多的内联还会降低指令高速缓存的命中率,从而使取指令的速度降低,因为从主存取指令当然比从缓存要慢。

另一方面,如果内联函数体非常短,编译器为这个函数体生成的代码就会真的比为函数调用生成的代码要小许多。如果是这种情况,内联这个函数将会确实带来更小的目标代码和更高的缓存命中率!

要牢记在心的一条是,inline指令就象register,它只是对编译器的一种提示,而不是命令。也就是说,只要编译器愿意,它就可以随意地忽略掉你的指令,事实上编译器常常会这么做。例如,大多数编译器拒绝内联"复杂"的函数(例如,包含循环和递归的函数);还有,即使是最简单的虚函数调用,编译器的内联处理程序对它也爱莫能助。(这一点也不奇怪。virtual的意思是"等到运行时再决定调用哪个函数",inline的意思是"在编译期间将调用之处用被调函数来代替",如果编译器甚至还不知道哪个函数将被调用,当然就不能责怪它拒绝生成内联调用了)。以上可以归结为:一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现。幸运的是,大多数编译器都可以设置诊断级,当声明为内联的函数实际上没有被内联时,编译器就会为你发出警告信息(参见条款48)。

假设写了某个函数f并声明为inline,如果出于什么原因,编译器决定不对它内联,那将会发生些什么呢?最明显的一个回答是将f作为一个非内联函数来处理:为f生成代码时就象它是一个普通的"外联"函数一样, 对f的调用也象对普通函数调用那样进行。

理论上来说确实应该这样发生,但理论和现实往往会偏离,现在就属于这种情况。因为,这个方案对解决"被外联的内联"(outlined inline)这一问题确实非常理想,但它加入到C++标准中的时间相对较晚。较早的C++规范(比如ARM------参见条款50)告诉编译器制造商去实现的是另外不同的行为,而且这一旧的行为在现在的编译器中还很普遍,所以必须理解它是怎么一回事。

稍微想一想你就可以记起,内联函数的定义实际上都是放在头文件中。这使得多个要编译的单元(源文件)可以包含同一个头文件,共享头文件内定义的内联函数所带来的益处。下面给出了一个例子,例子中的源文件名以常规的".cpp"结尾,这应该是C++世界最普遍的命名习惯了:


 // 文件example.h
 inline void f() { ... }          // f的定义

 ...


 // 文件source1.cpp
 #include "example.h"             // 包含f的定义

 ...                              // 包含对f的调用


 // 文件source2.cpp
 #include "example.h"             // 也包含f的定义
  
 ...                              // 也调用f

假设现在采用旧的"被外联的内联"规则,而且假设f没有被内联,那么,当source1.cpp被编译时,生成的目标文件中将包含一个称为f的函数,就象f没有被声明为inline一样。同样地,当source2.cpp被编译时,产生的目标文件也将包含一个称为f的函数。当想把两个目标文件链接在一起时,编译器会因为程序中有两个f的定义而报错。

为了防止这一问题,旧规则规定,对于未被内联的内联函数,编译器把它当成被声明为static那样处理,即,使它局限于当前被编译的文件。具体到刚才看到的例子中,遵循旧规则的编译器处理source1.cpp中的f时,就象f在source1.cpp中是静态的一样;处理source2.cpp中的f时,也把它当成在source2.cpp中是静态的一样。这一策略消除了链接时的错误,但带来了开销:每个包含f的定义(以及调用f)的被编译单元都包含自己的f的静态拷贝。如果f自身定义了局部静态变量,那么,每个f的拷贝都有此局部变量的一份拷贝,这必然会让程序员大吃一惊,因为一般来说,函数中的"static"意味着"只有一份拷贝"。

具体实现起来也会令人吃惊。无论新规则还是旧规则,如果内联函数没被内联,每个调用内联函数的地方还是得承担函数调用的开销;如果是旧规则,还得忍受代码体积的增加,因为每个包含(或调用) f的被编译单元都有一份f的代码及其静态变量的拷贝!(更糟糕的是,每个f的拷贝以及每个f的静态变量的拷贝往往处于不同的虚拟内存页面,所以两个对f的不同拷贝进行调用有可能导致多个页面错误。)

还有呢!有时,可怜的随时准备为您效劳的编译器即使很想内联一个函数,却不得不为这个内联函数生成一个函数体。特别是,如果程序中要取一个内联函数的地址,编译器就必须为此生成一个函数体。编译器怎么能产生一个指向不存在的函数的指针呢?


inline void f() {...}            // 同上

void (*pf)() = f;                // pf指向f

int main()
{
  f();                           // 对f的内联调用

  pf();                          // 通过pf对f的非内联调用
  ...
}

这种情况似乎很荒谬:f的调用被内联了,但在旧的规则下,每个取f地址的被编译单元还是各自生成了此函数的静态拷贝。(新规则下,不管涉及的被编译单元有多少,将只生成唯一一个f的外部拷贝)

即使你从来不使用函数指针,这类"没被内联的内联函数"也会找上你的门,因为不只是程序员会使用函数指针,有时编译器也这么做。特别是,编译器有时会生成构造函数和析构函数的外部拷贝,这样就可以通过得到那些函数的指针,方便地构造和析构类的对象数组(参见条款M8)。

实际上,随便一个测试就可以证明构造函数和析构函数常常不适合内联;甚至,情况比测试结果还糟。例如,看下面这个类Derived的构造函数:


class Base {
public:
  ...
private:
  string bm1, bm2;   // 基类成员1和2
};
class Derived: public Base {
public:
  Derived() {}                  // Derived的构造函数是空的,
  ...                           // ------但,真的是空的吗?


private:
  string dm1, dm2, dm3;         // 派生类成员1-3
};

这个构造函数看起来的确象个内联的好材料,因为它没有代码。但外表常常欺骗人!仅仅因为它没有代码并不能说明它真的不含代码。实际上,它含有相当多的代码。

C++就对象创建和销毁时发生的事件有多方面的规定。条款5和M8介绍了当使用new时,动态创建的对象怎样自动地被它们的构造函数初始化,以及当使用delete时析构函数怎样被调用。条款13说明了当创建一个对象时,对象的每个基类以及对象的每个数据成员会被自动地创建;当对象被销毁时,会自动地执行相反的过程(即析构)。这些条款告诉你,C++规定了哪些必须发生,但没规定"怎么"发生。"怎么发生"取决于编译器的实现者,但要弄清楚的是,这些事件不是凭空自己发生的。程序中必然有什么代码使得它们发生,特别是那些由编译器的实现者写的、在编译其间插入到你的程序中的代码,必然也藏身于某个地方------有时,它们就藏身于你的构造函数和析构函数。所以,对于上面那个号称为空的Derived的构造函数,有些编译器会为它产生相当于下面的代码:

// 一个Derived构造函数的可能的实现
 Derived::Derived()
{
  // 如果在堆上创建对象,为其分配堆内存;
  // operator new的介绍参见条款8
  if (本对象在堆上)
    this = ::operator new(sizeof(Derived));


  Base::Base();                  // 初始化Base部分

  dm1.string();            // 构造dm1
  dm2.string();            // 构造dm2
  dm3.string();            // 构造dm3
}

别指望上面这样的代码可以通过编译,因为它在C++中是不合法的。首先,在构造函数内无法知道对象是不是在堆上。(想知道如何可靠地确定一个对象是否在堆上,请参见条款M27)另外,对this赋值是非法的。还有,通过函数调用访问构造函数也是不允许的。然而,编译器工作起来没这些限制,它可以随心所欲。但代码的合法性不是现在要讨论的主题。问题的要点在于,调用operator new(如果需要的话)的代码、构造基类部分的代码、构造数据成员的代码都会神不知鬼不觉地添加到你的构造函数中,从而增加构造函数的体积,使得构造函数不再适合内联。当然,同样的分析也适用于Base的构造函数,如果Base的构造函数被内联,添加到它里面的所有代码也会被添加到Derived的构造函数(Derived的构造函数会调用Base的构造函数)。如果string的构造函数恰巧也被内联,Derived的构造函数将得到其代码的5个拷贝,每个拷贝对应于Derived对象中5个string中的一个(2个继承而来,3个自己声明)。现在你应该明白,内联Derived的构造函数并非可以很简单就决定的!当然,类似的情况也适用于Derived的析构函数,无论如何都要清楚这一点:被Derived的构造函数初始化的所有对象都要被完全销毁。刚被销毁的对象以前可能占用了动态分配的内存,那么这些内存还需要释放。

程序库的设计者必须预先估计到声明内联函数带来的负面影响。因为想对程序库中的内联函数进行二进制代码升级是不可能的。换句话说,如果f是库中的一个内联函数,用户会将f的函数体编译到自己的程序中。如果程序库的设计者后来要修改f,所有使用f的用户程序必须重新编译。这会很令人讨厌(参见条款34)。相反,如果f是非内联函数,对f的修改仅需要用户重新链接,这就比需要重新编译大大减轻了负担;如果包含这个函数的程序库是被动态链接的,程序库的修改对用户来说完全是透明的。

内联函数中的静态对象常常表现出违反直觉的行为。所以,如果函数中包含静态对象,通常要避免将它声明为内联函数。具体介绍参见条款M26。

为了提高程序开发质量,以上诸项一定要牢记在心。但在具体编程时,从纯实际的角度来看,有一个事实比其余的因素都重要:大多数调试器遇上内联函数都会无能为力。

这不是什么新鲜事。你想,怎么在一个不存在的函数里设置断点呢?怎么单步执行到这样一个函数呢?怎么俘获对它的调用呢?除非你是个百年一遇的怪才,或者用了暗渡陈仓之类的伎俩,否则是不可能做到的。让人高兴的是,这一点倒是可以作为决定该不该对函数声明inline的决策依据之一。

一般来说,实际编程时最初的原则是不要内联任何函数,除非函数确实很小很简单,象下面这个age函数:

class Person {
public:
  int age() const { return personAge; }

  ...

private:
  int personAge;

  ...

};

慎重地使用内联,不但给了调试器更多发挥作用的机会,还将内联的作用定位到了正确的位置:它是一个根据需要而使用的优化工具。不要忘了从无数经验得到的这条80-20定律(参见条款M16):一个程序往往花80%的时间来执行程序中20%的代码。这是一条很重要的定律,因为它提醒你,作为程序员的一个很重要的目标,就是找出这20%能够真正提高整个程序性能的代码。你可以选择内联你的函数,或者没必要就不内联,但这些选择只有作用在"正确"的函数上才有意义。

一旦找出了程序中那些重要的函数,以及那些内联后可以确实提高程序性能的函数(这些函数本身依赖于所在系统的体系结构),就要毫不犹豫地声明为inline。同时,要注意代码膨胀带来的问题,并监视编译器的警告信息(参见条款48),看看是否有内联函数没有被编译器内联。

若能做到明智地使用,内联函数将是每个C++程序员百宝箱中的一件无价之宝。当然,正如前面的讨论所揭示的,它们并不象所想象的那样简单和直接。

原创粉丝点击