宏就像任何其他工具一样 - 用于谋杀的锤子并不邪恶,因为它是一把锤子。人们以这种方式使用它的方式是邪恶的。如果你想钉钉子,锤子是一个完美的工具。
宏有几个方面使它们“不好”(我稍后会详细介绍每个方面,并提出替代方案):
- 您不能调试宏。
- 宏扩展会导致奇怪的副作用。
- 宏没有“命名空间”,因此如果您的宏与其他地方使用的名称发生冲突,您会在不需要的地方得到宏替换,这通常会导致奇怪的错误消息。
- 宏可能会影响您没有意识到的事情。
所以让我们在这里稍微扩展一下:
1) 无法调试宏。
当您有一个转换为数字或字符串的宏时,源代码将具有宏名称,并且许多调试器无法“看到”宏转换为什么。所以你实际上并不知道发生了什么。
替换:使用enum或const T
对于“类似函数”的宏,因为调试器在“每个源代码行”级别上工作,所以无论是一条语句还是一百条语句,您的宏都会像一条语句一样运行。很难弄清楚发生了什么。
替换:使用函数 - 如果需要“快速”,则使用内联(但要注意内联过多不是一件好事)
2) 宏扩展可能会产生奇怪的副作用。
著名的是#define SQUARE(x) ((x) * (x)) 和使用x2 = SQUARE(x++)。这导致x2 = (x++) * (x++);,即使它是有效代码 [1],也几乎肯定不是程序员想要的。如果是函数,做x++就好了,x只会增加一次。
另一个例子是宏中的“if else”,假设我们有这个:
#define safe_divide(res, x, y) if (y != 0) res = x/y;
然后
if (something) safe_divide(b, a, x);
else printf("Something is not set...");
它实际上变成了完全错误的事情......
替换:真正的功能。
3) 宏没有命名空间
如果我们有一个宏:
#define begin() x = 0
我们有一些使用 begin 的 C++ 代码:
std::vector<int> v;
... stuff is loaded into v ...
for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
std::cout << ' ' << *it;
现在,你认为你得到了什么错误信息,你在哪里寻找错误[假设你完全忘记了——或者甚至不知道——存在于其他人编写的某个头文件中的 begin 宏? [如果你在 include 之前包含那个宏,那就更有趣了——你会陷入奇怪的错误中,当你查看代码本身时,这完全没有意义。
替换:嗯,与其说是替换,不如说是“规则” - 只对宏使用大写名称,而从不将所有大写名称用于其他事物。
4) 宏有你没有意识到的效果
取这个函数:
#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ...
void dostuff()
{
int x = 7;
begin();
... more code using x ...
printf("x=%d\n", x);
end();
}
现在,不看宏,你会认为 begin 是一个函数,它不应该影响 x。
这种事情,而且我见过更复杂的例子,真的会搞砸你的一天!
替换:要么不使用宏来设置 x,要么将 x 作为参数传入。
有时使用宏绝对是有益的。一个例子是用宏包装一个函数来传递文件/行信息:
#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x) my_debug_free(x, __FILE__, __LINE__)
现在我们可以在代码中使用my_debug_malloc作为常规的malloc,但是它有额外的参数,所以当我们扫描“哪些内存元素还没有被释放”时,我们可以打印哪里分配是为了让程序员可以追踪泄漏。
[1] “在一个序列点”多次更新一个变量是未定义的行为。序列点与语句并不完全相同,但对于大多数意图和目的而言,我们应该将其视为。所以x++ * x++ 将更新x 两次,这是未定义的,可能会导致不同系统上的不同值,以及x 中的不同结果值。