C++编程思想08.pdf
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_05.gif)
《C++编程思想08.pdf》由会员分享,可在线阅读,更多相关《C++编程思想08.pdf(16页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、下载下载第8章内 联 函 数C+继承C的一个重要特性是效率。假如 C+的效率显著地比 C低,程序设计者不会使用它。在C中,保护效率的一个方法是使用宏(m a c r o)。宏可以不用普通函数调用就使之看起来像函数调用。宏的实现是用预处理器而不是编译器。预处理器直接用宏代码代替宏调用,所以就没有了参数压栈、生成汇编语言的 C A L L、返回参数、执行汇编语言的 R E T U R N的时间花费。所有的工作由预处理器完成,因此,不用花费什么就具有了程序调用的便利和可读性。C+中,使用预处理器宏存在两个问题。第一个问题在 C中也存在:宏看起来像一个函数调用,但并不总是这样。这就隐藏了难以发现的错误
2、。第二个问题是 C+特有的:预处理器不容许存取私有(p r i v a t e)数据。这意味着预处理器宏在用作成员函数时变得非常无用。为了既保持预处理器宏的效率又增加安全性,而且还能像一般成员函数一样可以在类里访问自如,C+用了内联函数(inline function)。本章我们将研究C+预处理器宏存在的问题、C+中如何用内联函数解决这些问题以及使用内联函数的方针。8.1 预处理器的缺陷预处理器宏存在的关键问题是我们可能认为预处理器的行为和编译器的行为一样。当然,有意使宏在外观上和行为上与函数调用一样,因此容易被混淆。当微妙的差异出现时,问题就出现了。考虑下面这个简单例子:#define f(
3、x)(x+1)现在假如有一个像下面的f的调用f(1)预处理器展开它,出现下面不希望的情况:(x)(x+1)(1)出现这个问题是因为在宏定义中f和括号之间存在空格缝隙。当定义中的这个空格取消后,实际上调用宏时可以有空格空隙。像下面的调用:f(1)依然可以正确地展开为:(1+1)上面的例子虽然微不足道但问题非常明显。在宏调用中使用表达式作为参数时,问题就出现了。存在两个问题。第一个问题是表达式在宏内展开,所以它们的优先级不同于我们所期望的优先级。例如:#define floor(x,b)x=b?0:1现在假如对参数使用表达式if(floor(a&0 x0f,0 x07)/.宏将展开成:if(a&0
4、 x0f=0 x07?0:1)因为&的优先级比=的低,所以宏的展开结果将会使我们惊讶。一旦发现这个问题,可以通过在宏定义内使用括弧来解决。上面的定义可改写如下:#define floor(x,b)(x)=(b)?0:1)发现问题可能很难,我们可能一直认为宏的行为是正确的。在前面没有加括号的版本的例子中,大多数表达式将正确工作,因为 =的优先级比像+、/、-甚至位移动操作符的优先级都低。因此,很容易想到它对于所有的表达式都正确,包括那些位逻辑操作符。前面的问题可以通过谨慎地编程来解决:在宏中将所有的内容都用括号括起来。第二个问题则更加微妙。不像普通函数,每次在宏中使用一个参数,都对这个参数求值。
5、只要宏仅用普通变量调用,这个求值就开始了。但假如参数求值有副作用,那么结果可能出乎预料,并肯定不能模仿函数行为。例如,下面这个宏决定它的参数是否在一定范围:#define band(x)(x)5&(x)10)?(x):0)只要使用一个“普通”参数,宏和真的函数工作得非常相像。但只要我们松懈并开始相信它是一个真的函数时,问题就开始出现了。下面是这个程序的输出,它完全不是我们想从真正的函数期望得到的结果:第8章 内 联 函 数143下载当a等于4时,测试了条件表达式第一部分,但它不满足条件,而表达式只求值一次,所以宏调用的副作用是a等于5,这是在相同的情况下普通函数调用得到的结果。但当数字在范围之
6、内时,两个表达式都测试,产生两次自增操作。产生这个结果是由于再次对参数操作。一旦数字出了范围,两个条件仍然测试,所以也产生两次自增操作。根据参数不同产生的副作用也不同。很清楚,这不是我们想从看起来像函数调用的宏中所希望的。在这种情况下,明显有效的解决方法是设计真正的函数。当然,如果多次调用函数将会增加额外的开销并可能降低效率。不幸的是,问题可能并不总是如此明显。我们可能不知不觉地得到一个包含混合函数和宏在一起的库函数,所以像这样的问题可能隐藏了一些难以发现的缺陷。例如,在S T D I O.H中的putc()宏可能对它的第二个参数求值两次。这在标准 C中作了详细说明。宏toupper()不谨慎
7、地执行也会对第二个参数求值超过两次。如在使用 toupper(*p+)1 时就会产生不希望的结果。宏和访问当然,对于C需要对预处理器宏谨慎地编码和使用。即使不是因为宏不是成员函数所需要的范围概念这一原因,我们也会在C+中避免使用它所带来的麻烦。预处理器简单地执行原文替代,所以不可能用下面这样或近似的形式写:class X int i;p u b l i c:#define val(X:i)/Error另外,这里没有指明我们正在涉及哪个对象。在宏里简直没有办法表示类的范围。没有能取代预处理器宏的方法,程序设计者出于效率考虑,不得不让一些数据成员成为 p u b l i c类型,这样就会暴露内部的
8、实现并妨碍在这个实现中的改变。8.2 内联函数在解决C+中宏存取私有的类成员的问题过程中,所有和预处理器宏有关的问题也随着消失了。这是通过使宏被编译器控制来实现的。在 C+中,宏的概念是作为内联函数来实现的,而内联函数无论在任何意义上都是真正的函数。唯一不同之处是内联函数在适当时像宏一样展开,所以函数调用的开销被取消。因此,应该永远不使用宏,只使用内联函数。144C+编程思想下载1 在Andraw Koenig所著的书C的陷阱和缺陷(A d d i s i o n-We s l e y,1 9 8 9)中将更详细地阐述。任何在类中定义的函数自动地成为内联函数,但也可以使用 i n l i n
9、e关键字放在类外定义的函数前面使之成为内联函数。但为了使之有效,必须使函数体和声明结合在一起,否则,编译器将它作为普通函数对待。因此inline int PlusOne(int x);没有任何效果,仅仅只是声明函数(这不一定能够在稍后某个时候得到一个内联定义)。成功的方法如下:inline int PlusOne(int x)return +x;注意,编译器将检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些事情是预处理器无法完成的。假如对于上面的内联函数,我们写成一个预处理器宏的话,将有不想要的副作用。一般应该把内联定义放在头文件里。当编译器看到这个定义时,它把函数类型(函数名+
10、返回值)和函数体放到符号表里。当使用函数时,编译器检查以确保调用是正确的且返回值被正确使用,然后将函数调用替换为函数体,因而消除了开销。内联代码的确占用空间,但假如函数较小,这实际上比为了一个普通函数调用而产生的代码(参数压栈和执行 C A L L)占用的空间还少。在头文件里,内联函数默认为内部连接即它是 static,并且只能在它被包含的编译单元看到。因而,只要它们不在相同的编译单元中声明,在内联函数和全局函数之间用同样的名字也不会在连接时产生冲突。8.2.1 类内部的内联函数为了定义内联函数,通常必须在函数定义前面放一个 i n l i n e关键字。但这在类内部定义内联函数时并不是必须的
11、。任何在类内部定义的函数自动地为内联函数。如下例:第8章 内 联 函 数145下载当然,因为类内部的内联函数节省了在外部定义成员函数的额外步骤,所以我们一定想在类声明内每一处都使用内联函数。但应记住,内联的目的是减少函数调用的开销。假如函数较大,那么花费在函数体内的时间相对于进出函数的时间的比例就会较大,所以收获会较小。而且内联一个大函数将会使该函数所有被调用的地方都做代码复制,结果代码膨胀而在速度方面获得的好处却很少或者没有。8.2.2 存取函数在类中内联函数的最重要的用处之一是用于一种叫存取函数的函数。这是一个小函数,它容许读或修改对象状态即一个或几个内部变量。类内存取函数使用内联方式重要
12、的原因在下面的例子中可以看到。这里,在类的设计者控制下,将类里面状态变量设计为私有(p r i v a t e),类的使用者就永远不会直接和它们发生联系了。对私有(p r i v a t e)数据成员的所有存取只可以通过成员函数接口进行。而且,这种存取是相当有效的。例如对于函数 read()。若没用内联函数,对read()调用产生的代码将包括对t h i s压栈和执行汇编语言C A L L。对于大多数机器,产生的代码将比内联函数产生的代码大一些,执行的时间肯定要长一些。不用内联函数,考虑效率的类设计者将忍不住简单地使 i为公共(p u b l i c)成员,从而通过让用户直接存取i 而节约开销
13、。从设计的角度看,这是很不好的。因为 i将成为公共界面的一部分,所以意味着类设计者决不能修改它。我们将和称为 i的一个i n t类型变量打交道。这是一个问题,因为我们可能在稍后觉得用一个 f l o a t变量比用一个int 变量代表状态信息更有用一些,但因为int i是公共接口的一部分,所以我们不能改变它。另一方面,假如我们总是使用成员函数读和修改一个对象的状态信息,那么就可以满意地修改对象内部一些描述(应该永远打消在编码和测试之前能使我们的设计完善的念头)。存取器(a c c e s s o r s)和修改器(m u t a t o r s)一些人进一步把存取函数的概念分成存取器(从一个对
14、象读状态信息)和修改器(修改状146C+编程思想下载态信息)。而且,可以用重载函数对存取器和修改器提供相同名字的函数,如何调用函数决定了我们是读还是修改状态信息。构造函数使用构造函数初始表达式表(在第 7章中作了简介,在1 3中章将详细介绍)来初始化Wi d t h和H e i g h t值(对于内部数据类型使用伪编译器调用形式)。当然,存取器和修改器对于一个内部变量不必只是简单的传递途径。有时,它们可以执行一些计算。下面的例子使用标准的C库函数中的时间函数来生成简单的Ti m e类:第8章 内 联 函 数147下载148C+编程思想下载标准C库函数对于时间有多种表示,它们都是类 Ti m e
15、的一部分。但全部更新它们是没有必要的,所以time_t T被用作基本的表示法,tm local 和A S C I I字符表示法A s c i i都有一个标记来显示它们是否已被更新为当前的时间t i m e _ t。两个私有函数updateLocal()和 updateAscii()检查标记,并有条件地执行更新。构造函数调用mark()函数时(用户也可以调用它,强迫对象表示当前时间)也就清除了两个标记,这时当地时间和 A S C I I表示法是无效的。函数 ascii()调用updateAscii(),因为函数ascii()使用静态数据,假如它被调用,则这个静态数据被重写,所以 updateAs
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C+ 编程 思想 08
![提示](https://www.taowenge.com/images/bang_tan.gif)
限制150内