定义
内联函数从源代码层看,有函数的结构,而在编译后,却不具体函数的性质。编译时,类似宏替换地,使用函数体替换调用处的函数名。一般在代码中用inline修饰,但是否能形成内联函数,需要看编译器对该函数定义的具体处理。
实际应用
引入内联函数的目的是为了解决程序中函数调用的效率问题。
函数是一种更高级的抽象。它的引入使得编程者只关心函数的功能和使用方法,而不必关心函数功能
的具体实现;函数的引入可以减少程序的目标代码,实现程序代码和数据的共享。但是,函数调用也会带来降低效率的问题,因为调用函数实际上将程序执行顺序转
移到函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去前要保护现场并记忆执行的地址,转
回后先要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。特别是对于一些函数体代码不是很大,但
又频繁地被调用的函数来讲,解决其效率问题更为重要。引入内联函数实际上就是为了解决这一问题。
在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行替换。
显然,这种做法不会产生转去转回的问题,但是由于在编译时函数体中的代码被替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间开销上不
象函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。
在程序中,调用其函数时,该函数在编译时被替代,而不是像一般函数那样是在运行时被调用。
注意事项
使用内联函数应注意的事项
内联函数具有一般函数的特性,它与一般函数所不同之处只在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。在使用内联函数时,应注意如下几点:
1.在内联函数内不允许用循环语句和开关语句。
如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数(自己调
用自己的函数)是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,
所以也没有必要用内联函数实现。
2.内联函数的定义必须出现在内联函数第一次被调用之前。
3.本栏目讲到的类结构中所有在类说明内部定义的函数是内联函数。
深入探究内联函数 内联函数——多么振奋人心的一项发明!它们看上去与函数很相像,它们拥有与函数类似的行为,它们要比宏(参见第 2 条)好用的多,同时你在调用它们时带来的开销比一般函数小得多。可谓“内联在手,别无他求。”
你得到的远远比你想象的要多,因为节约函数调用的开销仅仅是冰山一角。编译器优化通常是针对那些没有函数调用的代码,因此当你编写内联函数时,编译器就会针对函数体的上下文进行优化工作。然而大多数编译器都不会针对“外联”函数调用进行优化。
然而,在你的编程生涯中,“没有免费的午餐”这句生活哲言同样奏效,内联函数不会幸免。内联函数背后蕴含的理念是:用代码本体来取代每次函数调用,这样做
很可能会是目标代码的体积增大不少,这一点并不是非要统计学博士才能看得清。对于内存空间有限的机器而言,过分热衷于使用内联则会造成函数占用过多的空
间。即使在虚拟内存中,那些冗余的内联代码也会带来不少无谓的分页,从而使缓存读取命中率降低,最终带来性能的牺牲。
另一方面,如果一个内联函数体非常的短,那么为函数体所生成代码的体积就会比为函数调用生成的代码小一些。此时,内联函数才真正做到了减小目标代码和提高缓存读取命中率的目的。
我们要时刻保持清醒, Inline 是对编译器的一次请求,而不是一条命令。这种请求可以显式提出也可以隐式提出。隐式请求的途径就是:在类定义的内部定义函数:
class Person { public: ... int age() const { return theAge; } // 隐式内联请求 : 年龄 age 在类定义中做出定义 ... private: int theAge; };
|
这样的函数通常是成员函数,但是类中定义的函数也可以是友元(参见第 46 条),如果函数是友元,那么也应隐式将它们定义为内联函数。
显式声明
内联函数的方法为:在函数定义之前添加 inline 关键字。比如说,下面是标准 max 模板(来自 <algorithm> )通常的定义方式:
template<typename T> // 显式内联请求:
inline const T& std::max(const T& a, const T& b)
{ return a < b ? b : a; } // 在 std::max 的前边添加 “inline”
max 是一个模板这一事实,让我们不免得出这样的推论:内联函数和模 板都应该在头文件中定义。这就使一些程序员做出“函数模板必须为内联函数”的论断。这一结论不仅不合法,而且也存在潜在的害处,所以这里我们还是要大略的了解一下。
由于大多数构建环境都是在编译过程中进行内联,因此内联函数一般情况下都应该定义在头文件中。编译器必须首先了解函数的大致情况,以便于用所调用函数体来
代替这次函数调用。(一些构建环境在连接过程中进行内联,还有个别基于 .NET 通用语言基础结构( CLI
)的托管环境甚至是在运行时进行内联。这样的环境仅仅属于例外,而不是守则。在大多数
C++ 程序中,内联是一个编译时行为。)
模板通常保存在头文件中,但是编译器还需要了解模板的大致情形,以便于在用到时进行正确的实例化。(然而,这并不是一成不变的。一些构建环境在连接时进行模板实例化。但是编译时实例化才是更通用的方式。)
模板实例化相对于内联是独立的。如果你正在编写一个模板,而你又确信由这个模板所实例化出的所有函数都应该是内联的,那么这个模板就应该添 加
inline 关键字;这也就是上文中 std::max 实
现的做法。但是如果你正在编写的模板并不需要实例化内联函数,那么就不需要声明内联模板(无论是显式还是隐式)。内联也是有开销的,不假思索就引入内联的
开销的做法并不明智。我们已经介绍过了内联是如何使代码膨胀起来的(对于模板的作者而言,还应该做更周密的考虑——参见第 44
条),但是内联还会带来其他的开销,这就是下文中我们将要讨论的问题。
inline
是对编译器的一次请求,但编译器可能会忽略它。在我们的讨论开
始之前,我们首先要弄清这一点。大多数编译器如果认为当前的函数过于复杂(比如包括循环或递归的函数),或者这个函数是虚函数(即使是最平常的虚函数调
用),就会拒绝将其内联。后一个结论很好理解。因为 virtual 意味着“等到运行时再指出要调用哪个程序,”而 inline
意味着“在执行程序之前,使用要调用的函数来代替这次调用。”如果编译器不知道要调用哪个函数,那么它们拒绝内联函数体的做法就无可厚非了。
综上所述,我们得出下面的结论:一个给定的函数是否得到内联,取决于你正在使用的构建环境——主要是编译器。幸运的是,大多数编译器拥有诊断机制,如果编译器在内联函数时失败了,那么它们将会做出警告(参见第 53 条)。
有些时候,即使编译器认为某个函数非常适合进行内联,可是还是会为它提供一个函数体。举例说,如果你的程序要取得某个
内联函数的地址,那么编译器必须用典型的方法为其创建一个外联的函数体。那么编译器又怎样让一个指针去指向一个不存在的函数呢?再加上编译器一般不会通过对函数指针的调用进行内联这一事实,更能肯定这一结论:对于一个
内联函数的调用是否应该得到内联,取决于这一调用是如何进行的:
inline void f() {……} // 假设编译器乐意于将 f 的调用进行内联
void (*pf)() = f; // pf 指向 f……
f(); // 此调用将被内联,因为这是一次“正常”的调用
pf(); // 此调用很可能不会被内联,因为它是通过一个函数指针进行的
即使你从未使用函数指针,未得到内联的函数依然“阴魂不散”,这是因为需求函数指针的不仅仅是程序员。比如,编译器在为对象的数组进行构造或析构时,也会生成构造函数和析构函数的外联副本,从而使它们可以得到这些函数的指针以便使用。
实际上,为构造函数和析构函数进行内联通常不是一个好的选择,这两者甚至不如一些随意挑选的“选手”。请看下面示例 中 Derived 类的构 造函数:
class Base { public: ... private: std::string bm1, bm2; // 基类成员1和2 }; class Derived: public Base { public: Derived() {} // 派生类的构造函数为空 — 还有别的可能 ? ... private: std::string dm1, dm2, dm3;// 派生类成员 1–3 };
|
乍看上去,将这个构造函数进行内联再适合不过了,因为它不包含任何代码。其实你的眼睛欺骗了你。
C++对于在创建和销毁对象的过程中发生的事件进行了多方面的保证。比如,当你使用 new 时,你动态创建的对象的构造函数就会自动将其初始化;当你使用
delete
时,将调用相关的析构函数。当你创建一个对象时。每个基类和该对象中的每个数据成员将自动得到构造,在销毁这个对象时,针对两者的析构过程将会自动进行。
如果在对象的构造过程中有异常抛出,那么对象中已经得到构造的部分将统统被自动销毁。在所有这些场景中, C++
告诉你什么一定会发生,但它没有说明如何发生。这一点取决于编译器的实现者,但是必须要清楚的一点是,这些事情并不是自发的。你必须要在程序中添加一些代
码来实现它们。这些代码一定存在于某处,它们由编译器代劳,用于在编译过程中插入你的程序中。一些时候它们就存在于构造函数和析构函数中,所以,对于上文
中 Derived 的空构造函数,我们可以将具体实现中生成的代码等价看作:
Derived::Derived() // Derived 空构造函数的抽象实现 { Base::Base(); // 初始化 Base 部分 try { dm1.std::string::string(); } // 尝试构造 dm1 catch (...) { // 如果抛出异常 , Base::~Base(); // 销毁基类部分 , throw; // 并且传播该异常 } try { dm2.std::string::string(); } // 尝试构造 dm2 catch(...) { // 如果抛出异常 , dm1.std::string::~string(); // 销毁 dm1, Base::~Base(); // 销毁基类部分 , throw; // 并且传播该异常 } try { dm3.std::string::string(); } // 尝试构造 dm3 catch(...) { // 如果抛出异常 , dm2.std::string::~string(); // 销毁 dm2, dm1.std::string::~string(); // 销毁 dm1, Base::~Base(); // 销毁基类部分 , throw; // 并且传播该异常 } }
|
这段代码并不能完全真实反映出编译器所做的事情,因为真实的编译器采用的做法更加复杂。然而,上面的代码可以较为精确地反映出 Derived
的“空”构造函数必须要提供的内容。无论编译器处理异常的实现方式多么复杂, Derived
的构造函数必须至少为其数据成员和基类调用构造函数,这些调用(可能就是内联的)会使 Derived 显得不那么适合进行内联。
这
一推理过程对于 Base 的构造函数同样适用,因此如果将 Base 内联,所有添加进其中的代码同样也会添加进 Derived
的构造函数中(通过 Derived 构造函数调用 Base 构造函数的过程)。同时,如果 string 的构造函数恰巧被内联了,那么
Derived 的构造函数将为其复制出五份副本,分别对应 Derived
对象中包含的五个字符串(两个继承而来,另外三个系对象本身包括)。现在,“ Derived
的构造函数是否应该内联不是一个纯机械化问题”就很容易理解了。对于 Derived 的析构函数也一样,你必须亲自关注 Derived
的构造函数初始化的对象是否全部恰当的得到销毁,这一点机器无法代替。
库设计者必须估算出将函数内联所带来的影响,因为你根本无法为库中客户端程序员可见的内联函数提供底层的升级。换句话说,如果 f
是库中的一个内联函数,那么库的客户端程序员就会将 f 的函数体编译进他们的程序中。随后,如果一个库实现者修改了 f 的内容,那么所有曾经使用过
f 的客户端程序员必须要重新编译他们的代码。这一点是我们所不希望看到的。另一个角度讲,如果 f 不是内联函数,那么修改 f
只需要客户端程序员重新连接一下就可以了。这样要比重新编译减少很多繁杂的工作,并且,如果库中需要使用的函数是动态链接的,那么它对于客户端程序员就是
完全透明的。
我们的目标是开发优质的程序,因此要将这些重要问题牢记在心。但是以编写代码实际操作的角度来说,这一个事实将淹没一切:大多数调试人员面对内联函数时会遇到麻烦。这并不会令人意外,因为你无法为一个尚不存在的函数设定一个跟踪点。一些构建环境试图支持
内联函数的调试,但是几乎都失败了,大多数环境都是在调试过程中直接禁止内联。
对于“哪个函数应该声明为 inline
而哪些不应该”这一问题,我们可以由上文中引出一个逻辑上的策略。起初,不要内联任何内容,或者仅挑选出那些不得不内联的函数(参见第 46
条)或者那些确实是很细小的程序(比如本节开篇处出现的 Person::age
)进行内联。谨慎引入内联,你就为调试工作提供了方便,但是你仍然要为内联摆正位置:它属于手工的优化操作。不要忘记 80-20
经验决定主义原则:一个典型的程序将花去 80% 的时间仅仅运行 20%
的代码。这是一个非常重要的原则,因为它时时刻刻提醒我们,软件开发者的目标是:找出你的代码中 20%
的这部分进行优化,从而从整体上提高程序的性能。你可以花费很长的时间进行内联、修改函数等等,但如果你没有锁定正确的目标,那么你做再多的努力也是徒
劳。
铭记在心 仅仅对小型的、调用频率高的程序进行内联。这将简化你的调试操作,为底层更新提供方便,降低潜在的代码膨胀发生的可能,并且可以让程序获得更高的速度。
不要将模板声明为 inline 的,因为它们一般在头文件中出现。
好多公司的笔试题都有它,我吃了两次亏,长了一智
文章(一)内联函数与宏定义
在C中,常用预处理语句#define来代替一个函数定义。例如:
#define MAX(a,b) ((a)>(b)?(a)
b))
该语句使得程序中每个出现MAX(a,b)函数调用的地方都被宏定义中后面的表达式((a)>(b)?(a)
b))所替换。
宏定义语句的书写格式有过分的讲究, MAX与括号之间不能有空格,所有的参数都要
放在括号里。尽管如此,它还是有麻烦:
int a=1,b=0;
MAX(a++,b); //a被增值2次
MAX(a++,b+10); //a被增值1次
MAX(a,"Hello"); //错误地比较int和字符串,没有参数类型检查
MAX( )函数的求值会由于两个参数值的大小不同而产生不同的副作用。
MAX(a++,b)的值为2,同时a的值为3;
MAX(a++,b+10)的值为10,同时a的值为2。
如果是普通函数,则MAX(a,"HellO")会受到函数调用的检查,但此处不会因为两个参数类型不同而被编译拒之门外。幸运的是,通过一个内联函数可以得到所有宏的替换效能和 所有可预见的状态以及常规函数的类型检查:
inline int MAX(int a,int b)
{
return a>b?a:b;
}
1.内联函数与宏的区别:
传统的宏定义函数可能会引起一些麻烦。
ex:
#define F(x) x+x
void main(){int i=1;F(i++);}
这里x将被加两次。
内联函数被编译器自动的用函数的形势添加进代码,而不会出现这种情况。
内联函数的使用提高了效率(省去了很多函数调用汇编代码如:call和ret等)。
2.
内联函数的使用:
所有在类的声明中定义的函数将被自动认为是内联函数。
class A()
{
void c();// not a inline function;
void d(){ print("d() is a inline function.");}
}
如果想将一个全局函数定义为内联函数可用,inline 关键字。
inline a(){print("a() is a inline function.");}
注意:
在内联函数中如果有复杂操作将不被内联。如:循环和递归调用。
总结:
将简单短小的函数定义为内联函数将会提高效率。
文章(二)8.5.1 用内联取代宏代码
C++ 语言支持函数内联,其目的是为了提高函数的执行效率(速度)。
在C程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言
的CALL调用、返回参数、执行return等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效
应。例如
#define MAX(a, b) (a) > (b) ? (a) : (b)
语句
result = MAX(i, j) + 2 ;
将被预处理器解释为
result = (i) > (j) ? (i) : (j) + 2 ;
由于运算符‘+’比运算符‘:’的优先级高,所以上述语句并不等价于期望的
result = ( (i) > (j) ? (i) : (j) ) + 2 ;
如果把宏代码改写为
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
则可以解决由优先级引起的错误。但是即使使用修改后的宏代码也不是万无一失的,例如语句
result = MAX(i++, j);
将被预处理器解释为
result = (i++) > (j) ? (i++) : (j);
对于C++ 而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。
让我们看看C++
的“函数内联”是如何工作的。对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错
误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数
都一样)。如果正确,
内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。
C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在C++
程序中,应该用内联函数取代所有宏代码,“断言assert”恐怕是唯一的例外。assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生
的情况。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。如果assert是函数,由于函数调用会引起内
存、代码的变动,那么将导致Debug版本与Release版本存在差异。所以assert不是函数,而是宏。(参见6.5节“使用断言”)
8.5.2
内联函数的编程风格
关键字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程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
定义在类声明之中的成员函数将自动地成为内联函数,例如
class A
{
public:
void Foo(int x, int y) { … } // 自动地成为内联函数
}
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:
// 头文件
class A
{
public:
void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y)
{
…
}
8.5.3 慎用内联
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?
如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处
内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。
一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。
8.6 一些心得体会
C++
语言中的重载、内联、缺省参数、隐式转换等机制展现了很多优点,但是这些优点的背后都隐藏着一些隐患。正如人们的饮食,少食和暴食都不可取,应当恰到好
处。我们要辨证地看待C++的新机制,应该恰如其分地使用它们。虽然这会使我们编程时多费一些心思,少了一些痛快,但这才是编程的艺术。
第9章 类的构造函数、析构函数与赋值函数
构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。
每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?
原因如下:
(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人Stroustrup的好心好意白费了。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
对于那些没有吃够苦头的C++程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。
本章以类String的设计与实现为例,深入阐述被很多教科书忽视了的道理。String的结构如下:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operate =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
文章评论(0条评论)
登录后参与讨论