Macro Routines and Inline Routines
宏子程序和内联子程序
用预处理器的宏语言(preprocessor macros)编写子程序还需要一些特别的考虑。下面的这些规则和示例适用于在C++中使用预处理器的情形。如果你用的是其他编程语言或预处理器,请对这些规则因地制宜地加以调整。
把宏表达式整个包含在括号内 由于宏和其参数会被最终展开到代码中,因此请多加小心,确保代码是按照你所预期的方式被展开的。下面这个宏中包含了一个常见的错误:
C++示例:一个不能正确展开的宏
#define Cube( a ) a*a*a
如果传给这个宏的a不是不可分割的值,那它就不能正确地进行这一乘法计算了。比如说你写的表达式是Cube(x+1),那么它会展开成x+1*x+1*x+1,而由于乘法运算符的优先级高于加法运算符,这显然不是你所预期的结果。这个宏的下面这种写法要好一些,但也不完美:
C++示例:仍不能正确展开的宏
#define Cube( a ) (a)*(a)*(a)
这一次要好一些,但还是存在问题。如果在使用Cube()的表达式里含有比乘法运算符优先级更高的运算符,那么(a)*(a)*(a)也会再次失效。为了防止这种情况的发生,你应该给整个表达式加上括号:
C++示例:可以正确展开的宏
#define Cube( a ) ((a)*(a)*(a))
把含有多条语句的宏用大括号括起来 一个宏可以含有多条语句,如果你把它当做一条语句使用就会出错。下面这个例子就是一个会带来麻烦的宏:
C++示例:一个无法工作的含有多条语句的宏
#define LookupEntry( key, index ) \
index = (key - 10) / 5; \
index = min( index, MAX_INDEX ); \
index = max( index, MIN_INDEX );
...
for ( entryCount = 0; entryCount < numEntries; entryCount++ )
LookupEntry( entryCount, tableIndex[ entryCount ] );
这个宏之所以会带来麻烦,是因为它和常规函数的执行方式是不同的。按照例中所示的形式,在for循环语句中只有宏的第一部分代码被执行:
index = (key - 10) / 5;
要避免这一问题,请把宏用大括号括起来:
C++语言示例:可以正确工作的含有多条语句的宏
#define LookupEntry( key, index ) { \
index = (key - 10) / 5; \
index = min( index, MAX_INDEX ); \
index = max( index, MIN_INDEX ); \
}
通常认为,用宏来代替函数调用的做法具有风险,而且不易理解——这是一种很糟糕的编程实践——因此,除非必要,否则还是应该避免使用这种技术。
用给子程序命名的方法来给展开后代码形同子程序的宏命名,以便在需要时可以用子程序来替换宏 C++语言中给宏命名的方式是全部使用大写字母。如果能用子程序来代替宏,那在给宏命名的时候就应该采用给子程序命名的规则。这样当你想用子程序代替宏的时候,除了需要修改相关的子程序之外,无需做任何其他改动,反之亦然。
遵循这一建议也会带来一些风险。如果你常常利用到++和--运算符的副作用(side effects)(作为其他语句的一部分),那么当你误把宏当做子程序使用时就会遇上麻烦。考虑到副作用还可能引发其他问题,这也可以成为避免使用副作用的另一个原因。
Limitations on the Use of Macro Routines
宏子程序在使用上的限制
像C++这样的现代编程语言都提供了大量可以取代宏的方案:
■ const可以用于定义常量
■ inline可以用于定义可被编译为内嵌的代码(inline code)的函数
■ template可以用于以类型安全的方式定义各种标准操作,如min、max等
■ enum可以用于定义枚举类型
■ typedef可以用于定义简单的类型替换
正如C++的设计师Bjarne Stroustrup所指出的,“几乎每个宏都表明在编程语言、程序或程序员身上存在问题……当你使用宏的时候,就甭指望调试器、交叉引用工具和剖测 器(profiler)等工具能好好工作。”(Stroustrup 1997)。宏对于支持条件编译——见第8.6节“调试辅助工具”——非常有用,但对于细心的程序员来说,除非万不得已,否则是不会用宏来代替子程序的。
Inline Routines
内联子程序
C++支持inline关键字。inline子程序允许程序员在编写代码时把代码当成子程序,但编译器在编译期间通常会把每一处调用inline子程序的地方都转换为插入内嵌的代码(inline code)。因为避免了子程序调用的开销,因此inline机制可以产生非常高效的代码。
节制使用inline子程序 inline子程序违反了封装原则,因为C++要求程序员把inline子程序的实现代码写在头文件里,从而也就把这些实现细节暴露给了所有使用该头文件的程序员。
inline子程序要求在调用子程序的每个地方都生成该子程序的全部代码,这样无论inline子程序是长是短,都会增加整体代码的长度。这也会带来其自身的问题。
和为追求性能而使用的其他编程技巧一样,为性能原因而使用inline子程序的底线是:剖测(profile)代码并衡量性能上的改进。如果预期获得的性能收益不能说明为“剖测代码以验证性能改进”操心是值得的,那也就没有理由再牺牲代码质量而使用inline子程序了。
|
CHECKLIST: High-Quality Routines 核对表:高质量的子程序 大局事项 q 创建子程序的理由充分吗? q 一个子程序中所有适于单独提出的部分是不是已经被提出到单独的子程序中了? q 过程的名字中是否用了强烈、清晰的“动词+宾语”词组?函数的名字是否描述了其返回值? q 子程序的名字是否描述了它所做的全部事情? q 是否给常用的操作建立了命名规则? q 子程序是否具有强烈的功能上的内聚性?即它是否做且只做一件事,并且把它做得很好? q 子程序之间是否有较松的耦合?子程序与其他子程序之间的连接是否是小的(small)、明确(intimate)、可见(viaible)且灵活(flexible)的? q 子程序的长度是否是由其功能和逻辑自然确定,而非遵循任何人为的编码标准? 参数传递事宜 q 整体来看,子程序的参数表是否表现出一种具有整体性且一致的接口抽象? q 子程序参数的排列顺序是否合理?是否与类似的子程序的参数排列顺序相符? q 接口假定是否已在文档中说明? q 子程序的参数个数是否没超过7个? q 是否用到了每一个输入参数? q 是否用到了每一个输出参数? q 子程序是否避免了把输入参数用为工作变量? q 如果子程序是一个函数,那么它是否在所有可能的情况下都能返回一个合法的值? |
Key Points
要点
■ 创建子程序最主要的目的是提高程序的可管理性,当然也有其他一些好的理由。其中,节省代码空间只是一个次要原因;提高可读性、可靠性和可修改性等原因都更重要一些。
■ 有时候,把一些简单的操作写成独立的子程序也非常有价值。
■ 子程序可以按照其内聚性分为很多类,而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性。
■ 子程序的名字是它的质量的指示器。如果名字糟糕但恰如其分,那就说明这个子程序设计得很差劲。如果名字糟糕而且又不准确,那么它就反映不出程序是干什么的。不管怎样,糟糕的名字都意味着程序需要修改。
■ 只有在某个子程序的主要目的是返回由其名字所描述的特定结果时,才应该使用函数。
■ 细心的程序员会非常谨慎地使用宏,而且只在万不得已时才用。







