c或c++ 中的宏块(macro)是一种历史悠久且备受争议的源代码转换方式。争议的根源在于,就像 goto 语句一样,宏的功能过于强大且通用,以至于难以令人放心使用。
" 宏"本义是指"大",语言中的宏是指一(大)串字符,或者一(大)段代码块,在写代码时用简短的字符代替,在编译时展开。
编译器处理器具有多种不同的功能(注:foo表示变量或函数占位符):
● #include <foo> 在系统包含路径中查找文件 foo,并将其插入到当前程序中。
● #include "foo" 同上,但会先检查用户包含路径。
● #define foo bar 这种"类似常量"的宏会将代码中所有出现的 foo 替换为字符串 bar。
● #undef foo 移除宏 foo(如果它已定义)。
● #if defined(foo) ... #endif 如果已定义宏 foo,则编译其中的代码行;否则跳过它们。
● #ifdef foo ... #endif 这是上述指令的简写形式。
● #ifndef foo ... #endif "if not defined"(如果未定义)的简写形式。
#if 0 ... #endif 总是跳过其中的代码行。这常用于"注释掉"大段代码,特别是那些内部已包含 /* */ 注释的代码。
● #pragma foo 其具体作用取决于编译器。它可用于访问编译器内部机制或特定功能,但不具备可移植性。
1. 类常量宏块
该标准用法相当简单明了:
#define symbol replacement
使得预处理器在编译前将所有出现的"symbol"替换为"replacement"。因此,下述代码运行正常,并返回 17:
#define n 17
与 C++ 中的其他任何事物不同,从这一点开始,"n"定义为 17,且这一定义跨越了类或函数等作用域。如果声明 int n=3;,它会重写为 int 17=3;,导致无法通过编译!为了避免这种情况,按照惯例,宏通常使用大写字母命名 (例如 NUM_ELEMENTS),而不会仅仅使用简单的 n 。
简单的字符串替换还存在另一个问题。假设你定义了一个像这样的常量:
#define n 10+10
使用:
return n*n;
这样就会返回 10+10*10+10 = 120 。为何?
针对这个 Bug,有几种广为人知的修复方法:
● 避免:永远不要使用宏。 "enum"或"const int"提供基本相同的功能,并且不存在此问题。 有些人认为预处理器是纯 C 的不必要的遗留物,永远不应该使用。
● 变通方法 :如果使用 #define 定义内容,请将其括在括号内,例如:
#define n (10+10)
这招管用,但你必须记得每次都这么做!
2. 包含保护( Include Guards**)**
如果有人 #include 了你的头文件,同时也 #include 了另一个本身也包含了你该头文件的头文件,那么你的头文件代码就会被编译两次,从而引发关于"重复定义"的奇怪错误信息。为了避免这种情况,通常会在头文件的开头加上:
#ifndef LAWLOR_CODE_ALREADY_INCLUDED
#define LAWLOR_CODE_ALREADY_INCLUDED
...
#endif
第一次编译时,ifndef 条件成立,你的头文件会正常编译;而在随后的编译中,由于该宏已被定义,整个头文件都会被跳过。
照例,这样做存在几个隐患。首先,宏名称必须全局唯一 ;否则,当第二次包含该头文件时,预处理器会跳过它(导致宏定义失效),因此宏名中至少应包含头文件、项目及作者的名称 。其次,每个头文件都必须遵循这一规范,否则该头文件的第二次使用将会失败。
3. 可移植性标注宏( Portability**)**
可移植代码往往需要在不同的编译器上执行不同的操作。例如,对于既能由 C++ 编译器又能由 C 编译器编译的代码,可以利用以下方式为 C++ 版本添加额外功能:
#ifdef __cplusplus
class foo { ... };
#endif
**旨在跨系统(如 OS X 和 Windows)运行的可移植代码,往往需要区分这些系统,以便调用特定于操作系统的功能或规避特定系统的缺陷。**预定义的平台宏列表很长,但其中最常用的是:
● #ifdef _WIN32 对任意 Windows (不只是 32-bit)
● #ifdef __unix 对任意类UNIX系统 (Linux, BSD, IBM AIX, 甚至 Cygwin; 但由于某些原因不针对OS X )
● #ifdef __linux 针对Linux 系统(Ubuntu, Debian, Red Hat, 等等)
● #ifdef APPLE 对 OS X 或 iOS
4. 类函数宏( Function-like Macro**)**
在原生 C 语言中,内联函数出现之前,人们常使用带参数的宏来编写简短的工具函数。与其他所有宏一样,参数会在编译的最早阶段进行文本替换:
#define times3(x) x*3
使用:
return times3(10);
再次强调,问题在于这属于纯文本替换。因此,如果用求和表达式来调用同一个宏:
return times3(10+10);
这会返回 ......40 。即 10+10*3,而不是 (10+10)*3。同样,解决方法是用括号将表达式括起来。请注意,参数和整个表达式都需要加上括号,以防止宏调用内部或外部的运算符产生干扰:
#define times3(x) ((x)*3)
使用
return times3(10+10);
这种方法基本可靠,不过在这里使用宏相比使用小型函数并没有什么明显优势,而且它存在诸多局限------例如,在类函数宏内部创建临时变量并没有标准做法。
5. 改变语法宏( Syntax-changing Macros**)**
宏能够以各种有趣且任意的方式调整语法。例如,我可以利用宏来重命名花括号,就像这样:
#define begin {
#define end }
int x=5;
if (x>3)
begin
return 7;
end
习惯于全大写输入代码的 FORTRAN 程序员,可能会觉得使用 #define FOR for 用起来更顺手。
上述大多数用法顶多让人感到恼火,但情况可能会变得更糟:
#define BOOGA int x=9; return (x+
BOOGA 7);
这种扭曲语法的做法,不仅会搞乱任何具备语法高亮功能的编辑器,从根本上破坏代码的可读性,甚至严重到让人认真讨论是否应该全面禁用宏。
6. 复杂的 C++ 宏特征
● FILE 会展开为包含当前源代码文件名的字符串,这在调试时非常有用。
● LINE 会展开为表示当前源代码行号的整数,这对于生成在每次宏调用时都各不相同的名称非常有用。
● #b 会将参数 b 转换为带引号的字符串形式,这在打印输出时很方便;这种操作称为"字符串化"(stringification)。
● a##b 会将常量或参数 a 与 b 拼接在一起,中间不留空格。这种"记号粘贴"(token pasting)操作常用于生成新名称,例如 "myClass_##name" 或 "myClass_##LINE"。
可以使用反斜杠(\)将宏定义跨行书写。这种写法仅为了方便编写宏,反斜杠本身不会出现在传递给编译器的代码中。在某些编译器中,宏内部的 // 注释会导致宏剩余的所有内容都被视为注释而失效!
C++ 宏可能会变得相当复杂(这通常被视为弊端),但它们也提供了非常有用的"逾编程"能力,尤其是在大型程序中。所谓"逾编程",是指第一个程序(即预处理器)的输出作为另一个程序(此处指 C++ 程序)的源代码。你甚至可以进行显式的逾编程,即由一个程序生成另一个程序:
● 编译生成器代码。
● 运行生成器,输出最终代码。
● 编译最终代码。
● 运行最终代码。
这种复杂性在实际系统中是很常见的!