宏就像其他任何工具一样-用于谋杀的锤子不是邪恶的,因为它是锤子。人以这种方式使用它是邪恶的。如果您想用钉子锤一下,锤子是一个完美的工具。
宏有一些方面使它们“不好”(我将在后面进行扩展,并提出替代方案):
- 您不能调试宏。
- 宏扩展会导致奇怪的副作用。
- 宏没有“命名空间”,因此,如果您的宏与其他地方使用的名称发生冲突,则会在不需要的地方获得宏替换,这通常会导致奇怪的错误消息。
- 宏可能会影响您未意识到的事情。
因此,让我们在这里扩展一下:
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
并且我们在C ++中有一些使用begin的代码:
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宏?[并且如果您在包含之前包含该宏,则会更加有趣-您将陷入奇怪的错误中,这在您查看代码本身时完全没有意义。
替换:嗯,没有什么比“规则”更可替换的了-只对宏使用大写名称,而对其他东西则不使用所有大写名称。
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
。
#pragma
不是宏。