20. #define
- 原则
对于单纯常量,最好以 const 对象或 enum 替换 #define。
对于形似函数的宏(macros),最好改用 inline 函数替换 #define。
20.1. const
“宁可以编译器替换 预编译器 ”。
#define ASPECT_RATIO 1.653
const double AspectRation = 1.653;
也许名称 ASPECT_RATIO
从未被编译器看见,也许在编译器开始处理源码之前它已经被预处理器移走了。于是,记号名称 ASPECT_RATIO
可能没有进入记号表(symbol table)内。当运用此常量获得编译错误时,这个错误也许会提到 1.653
而不是 ASPECT_RATIO
,导致对其追踪
变得困难。
作为一个语言常量, AspectRation
一定会被编译器看到并记入记号表。
此外,对浮点常量而言,使用常量可能比使用 #define 导致较小的代码量,因为预处理器将宏名称 ASPECT_RATIO
替换为 1.653
,可能导致
目标码(object code)出现多份 1.653
,使用常量则不会。
还可以在类内声明 static const 成员。
1class Player
2{
3private:
4 static const int NumTurns = 5; // 常量声明(不是定义)
5 int scores[NumTurns]; // 使用该常量
6}
20.2. enum
如果编译器不允许 static 成员在声明式上获得初始值,一方面,可以在头文件定义类,在源文件中初始化它;另一方面,如果该类在编译期间 必须使用一个常量值,例如上例中数组 scores 的大小必须在编译期间知道,此时可以使用 enum。一个属于枚举类型的数值可以权当 int 被使用。
1class Player
2{
3private:
4 enum {NumTurns = 5}; // NumTurns 成为数值 5 的一个记号。
5 int scores[NumTurns]; // 使用该常量
6}
Note
enum 的行为类似于 #define,取一个 enum 的地址或 #define 的地址通常不合法,而取一个 const 的地址是合法的。
20.3. inline
使用宏(macros)不会有函数调用带来的额外开销。宏中的所有实参必须添加括号,但是仍然可能出现问题。
此时,可以定义内联函数(inline),它与普通函数一样遵守作用域(scope)和访问规则。内联函数的特点:
在调用处直接展开该函数的内容
运行速度快,但占用更多内存
适用于规模小、流程直接(无递归和众多判断)、频繁调用的函数
- 普通函数的调用:
执行到函数调用指令时,程序将立即存储该指令的内存地址,并将函数参数复制到栈(为此保留的内存块);
跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入寄存器中);
然后跳回到地址被保存的指令处。
1#include <iostream>
2using namespace std;
3
4#define CALL_WITH_MAX(a, b) f((a)>(b)? (a):(b))
5
6#define MAX_COMP_1(a, b) (a)>(b)? (a):(b)
7
8#define MAX_COMP_2(a, b) ((a)>(b)? (a):(b)) // 有外层括号
9
10template<class T>
11void f(T elem)
12{
13 cout << "max out: " << elem << endl;
14}
15
16template<class T>
17inline void CallWithMax(const T& a, const T& b) // 形参使用常量引用,因为不知道 T 的具体类型,比较安全
18{
19 f(a > b ? a : b);
20}
21
22int main(int argc, char** argv)
23{
24 int a = 5, b = 0;
25 CALL_WITH_MAX(++a, b); // a 自增2次,变为7(++a > b => ++a)
26 cout << a << endl;
27 CALL_WITH_MAX(++a, b+10); // a 自增1次,变为8(++a < b+10 => b)
28 cout << a << endl;
29
30 f(-10 + MAX_COMP_1(a, b)); // -10 + a > b ? a : b; 结果为 0
31 f(-10 + MAX_COMP_2(a, b));// -10 + (a > b ? a : b); 结果为 -10 + 8 = -2
32
33 CallWithMax(a, b); // 8
34
35 return 0;
36}
20.4. 附:C/C++ 编译过程(简)
编译过程 :
1.(分离式)编译 :每个文件独立编译
预处理:处理伪指令(#开头)和特殊符号。
宏定义:#define,#undef
条件编译:#ifdef,#ifndef,#endif
头文件包含:#include
特殊符号:__LINE__,__FILE__
编译:词法分析、语法分析,确认所有指令符合语法规则,将其翻译成等价的中间代码表示或汇编代码。
汇编:把汇编代码翻译成目标机器指令,得到目标文件(obj)。
2. 链接 :将相关的目标文件进行连接(头文件包含关系、符号引用等),使这些目标文件能够成为一个被执行的同一整体。
20.5. 参考资料
《Effective C++》条款02。
《C++ Primer 第5版 中文版》 Page 213–214。
C++内联函数详解