为了账号安全,请及时绑定邮箱和手机立即绑定

为什么预处理宏是邪恶的,还有什么可供选择的?

为什么预处理宏是邪恶的,还有什么可供选择的?

C++
一只萌萌小番薯 2019-07-02 16:50:53
为什么预处理宏是邪恶的,还有什么可供选择的?我一直问这个问题,但我从来没有收到过一个很好的答案,我想几乎所有程序员在写第一个“HelloWorld”之前都会遇到“宏不应该被使用”、“宏是邪恶的”等短语,我的问题是:为什么?有了新的C+11,在这么多年之后,还有真正的替代品吗?简单的部分是关于宏的,比如#pragma,它们都是特定于平台的和编译器特定的,而且大多数情况下它们都有严重的缺陷,例如#pragma once这在至少2种重要情况下是容易出错的:相同的名称在不同的路径以及一些网络设置和文件系统中。但总的来说,宏和其使用的替代方案又如何呢?
查看完整描述

3 回答

?
有只小跳蛙

TA贡献1824条经验 获得超8个赞

宏就像任何其他工具一样-用于谋杀的锤子不是邪恶的,因为它是锤子。以这种方式使用它是邪恶的。如果你想钉子,锤子是一个完美的工具。

宏有几个方面使它们变得“糟糕”(稍后我将对每个方面进行详细讨论,并提出备选方案):

  1. 无法调试宏。
  2. 宏观扩张会导致奇怪的副作用。
  3. 宏没有“命名空间”,因此如果宏与其他地方使用的名称冲突,则会得到不需要的宏替换,这通常会导致奇怪的错误消息。
  4. 宏可能会影响你没有意识到的事情。

让我们在这里展开一点:

1)宏不能调试。当您有一个转换为数字或字符串的宏时,源代码将具有宏名称,并且有许多调试器,您无法“看到”宏转换为什么。所以你不知道到底发生了什么。

更换*使用enumconst T

对于“类似函数”的宏,因为调试器工作在“您所在的每个源行”级别上,因此无论是一个语句还是100个语句,宏都将充当单个语句。很难弄清楚到底是怎么回事。

更换:使用函数-如果需要“快速”,则使用内联(但请注意,过多的内联不是一件好事)

2)宏观扩张会产生奇怪的副作用。

著名的是#define SQUARE(x) ((x) * (x))以及使用x2 = SQUARE(x++)..这导致了x2 = (x++) * (x++);,即使它是有效的代码[1],几乎肯定不是程序员想要的。如果它是一个函数,那么做x+就可以了,而x只会增加一次。

另一个例子是宏中的“如果其他”,比如我们有:

#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;

现在,您认为您得到了什么错误,您在哪里查找错误(假设您已经完全忘记-或者甚至不知道-其他人编写的某个头文件中的开始宏?[更有趣的是,如果您在包含之前包含了这个宏-当您查看代码本身时,您将陷入完全没有意义的奇怪错误中。

更换好吧,与其说是替换,不如说是“规则”-只对宏使用大写名称,而从不将所有大写名称用于其他内容。

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也是。


查看完整回答
反对 回复 2019-07-02
?
慕标琳琳

TA贡献1830条经验 获得超9个赞

“宏是邪恶的”这句谚语通常指的是#Definition,而不是#实用主义。

具体而言,这一表述指的是以下两种情况:

  • 将幻数定义为宏

  • 使用宏替换表达式

随着新的C+11有一个真正的选择,经过这么多年?

是的,对于上面列表中的项目(神奇的数字应该用const/conexpr来定义,表达式应该用[Normal/inlineTemplate/inlineTemplate]函数来定义。

以下是将魔术数字定义为宏和用宏替换表达式(而不是定义用于计算这些表达式的函数)所带来的一些问题:

  • 当为魔术数字定义宏时,编译器不保留定义值的类型信息。这可能导致编译警告(和错误),并混淆调试代码的人员。

  • 当定义宏而不是函数时,使用该代码的程序员期望它们像函数一样工作,而它们没有。

考虑以下代码:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )int a = 5;int b = 4;int c = max(++a, b);

在分配给c之后,您将期望a和c为6(使用std:max而不是宏)。相反,代码执行:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

此外,宏不支持命名空间,这意味着在代码中定义宏将限制客户端代码的名称。

这意味着,如果您定义了上面的宏(对于max),您将不再能够#include <algorithm>在下面的任何代码中,除非您显式地编写:

#ifdef max#undef max#endif#include <algorithm>

拥有宏而不是变量/函数也意味着您无法获取它们的地址:

  • 如果宏作为常量计算为魔术数字,则不能按地址传递它。

  • 对于宏作为函数,您不能使用它作为谓词,或接受该函数的地址或将其视为函式。

编辑:例如,#define max上文:

template<typename T>inline T max(const T& a, const T& b){
    return a > b ? a : b;}

这将执行宏所做的一切,但有一个限制:如果参数的类型不同,模板版本将强制您显式(这实际上会导致更安全、更显式的代码):

int a = 0;double b = 1.;max(a, b);

如果此max被定义为宏,则代码将编译(带有警告)。

如果这个max被定义为一个模板函数,编译器将指出歧义,您必须说max<int>(a, b)max<double>(a, b)(从而明确说明您的意图)。


查看完整回答
反对 回复 2019-07-02
?
拉风的咖菲猫

TA贡献1995条经验 获得超2个赞

一个常见的问题是:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

它将打印10,而不是5,因为预处理器会这样扩展它:

printf("25 / (3+2) = %d", 25 / 3 + 2);

这个版本更安全:

#define DIV(a,b) (a) / (b)


查看完整回答
反对 回复 2019-07-02
  • 3 回答
  • 0 关注
  • 237 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信