c 和 c++ 中的宏块(macro)

    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++ 程序)的源代码。你甚至可以进行显式的逾编程,即由一个程序生成另一个程序:

●  编译生成器代码。

●  运行生成器,输出最终代码。

●  编译最终代码。

●  运行最终代码。

这种复杂性在实际系统中是很常见的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值