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

● 编译生成器代码。

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

● 编译最终代码。

● 运行最终代码。

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

相关推荐
AA陈超11 小时前
004 T02 - 俯视角摄像机系统 设计文档
网络·c++·ue5·虚幻引擎
bu_shuo11 小时前
c与cpp中的argc和argv
c语言·c++·算法
蓝创精英团队11 小时前
VCPKG 跨平台C++ 库管理器
c++·vcpkg
Aurorar0rua11 小时前
CS50 x 2024 Notes Algorithms - 02
c语言·开发语言·学习方法
足球中国12 小时前
nuget把缓存搬离C盘
c语言·windows·缓存
Jayden_Ruan12 小时前
C++组合的输出
c++·算法·深度优先
froyoisle13 小时前
CSP 真题解析:[CSP-J 2020-T3] 表达式
c++·算法·csp·信息学·信奥赛
Darkwanderor13 小时前
动、静态库相关内容的详细介绍
linux·c语言·开发语言·c++
文祐13 小时前
C++类之虚函数表没有虚继承的菱形继承
开发语言·c++·算法