如何避免【宏】的这7个误区?

TXP嵌入式
关注

重复调用

我们常见的“最小”定义一个宏min,如下所示:

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

当将此宏与包含副作用的参数一起使用时,如此处所示,

next = min(x + y,foo(z));

它扩展如下:

next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));

其中x + y替换了X,而foo(z)替换了Y。

函数foo出现在程序中的语句中仅使用一次,但是表达式foo(z)已两次替换到宏扩展中。结果,执行该语句时可能会两次调用foo,所以min是一个不安全的宏。

解决此问题的最佳方法是以仅计算一次foo(z)值的方式定义min。C语言没有提供执行此操作的标准方法,但是可以使用GNU扩展来完成此操作,如下所示:

#define min(X, Y)                

({ typeof (X) x_ = (X);          

  typeof (Y) y_ = (Y);          

  (x_ < y_) ? x_ : y_; })

“({{…})”符号产生一个复合表达式,它的值是其最后一条语句的值。

如果不使用GNU C扩展,唯一的解决方案是在使用宏min时要小心。例如计算foo(z)的值时,将其保存在变量中,然后在min中使用该变量:

//假设foo返回int类型

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

 int tem = foo (z);

 next = min (x + y, tem);

自引用宏

自引用宏是其名称出现在其定义中的宏。我们知道所有宏定义都将被重新扫描以查找更多要替换的宏,如果自引用被认为是宏的使用,它将产生无限大的扩展。

为防止这种情况,自引用不被视为宏调用。它原样传递到预处理器输出中。举个例子

#define foo (4 + foo)

按照普通规则,其宏定义分析如下

对foo的每个引用都将扩展为(4 + foo);

然后将对其进行重新扫描,并将其扩展为(4 +(4 + foo));

以此类推,直到计算机内存耗尽。

自引用规则将这一过程缩短了一步,即(4 + foo),因此此宏定义可能会导致程序在引用foo的任何地方将foo的值加4。

阅读程序的人看到foo是变量,就难以记得它也是宏,真的会坑爹的。它的一种常见有用用法是创建一个可扩展为其自身的宏。如果你写

#define EPERM EPERM

然后宏EPERM扩展为EPERM。实际上,每当在运行文本中使用预处理器时,预处理器都会将其单独保留。

如果宏x扩展为使用宏y,而y的扩展引用了宏x,则这是x的间接自引用。在这种情况下,x也不展开,举个例子

#define x (4 + y)
#define y (2 * x)

然后x和y扩展如下:

x→(4 + y)
    →(4 +(2 * x))
y→(2 * x)
    →(2 *(4 + y))

当每个宏出现在另一个宏的定义中时,它们将被展开,但是当它间接出现在其自己的定义中时,则不会被展开。

参数预扫描处理

宏参数在被替换为宏主体之前必须经过完全宏扩展,替换后,将再次扫描整个宏主体,包括替换的参数,以查找要扩展的宏。

如果参数包含任何宏调用,则它们将在第一次扫描时扩展,那么结果不包含任何宏调用,因此第二次扫描不会更改它。

如果按照给定的方式替换了参数,并且没有进行预扫描,则剩余的单个扫描将找到相同的宏调用并产生相同的结果。

预扫描处理在以下三种特殊情况下有大的作用。

对宏的嵌套调用

当宏的参数包含对该宏的调用时,就会发生对宏的嵌套调用,举个例子。

如果f是期望一个参数的宏,则f(f(1))是对f的嵌套调用对。通过扩展f(1)并将其代入f的定义来进行所需的扩展。预扫描会导致发生预期的结果。

如果没有预扫描,f(1)本身将被替换为参数,并且f的内部使用将在主扫描期间作为间接自引用出现,并且不会扩展。

调用其他可进行字符串化或连接的宏的宏

如果参数是字符串化或串联的,则不会进行预扫描。

如果要扩展宏,然后对其扩展进行字符串化或串联,则可以通过使一个宏调用进行该字符串化或串联的另一宏来实现。举个例子

#define AFTERX(x) X_ ## x

#define XAFTERX(x) AFTERX(x)

#define TABLESIZE 1024

#define BUFSIZE TABLESIZE

然后AFTERX(BUFSIZE)扩展为X_BUFSIZE,而XAFTERX(BUFSIZE)扩展为X_1024而不是X_TABLESIZE,预扫描始终会进行完整的扩展。

参数中使用的宏,其扩展名包含未屏蔽的逗号。

这可能导致使用错误数量的参数调用在第二次扫描时扩展的宏。举个例子

#define foo  a,b

#define bar(x) lose(x)

#define lose(x) (1 + (x))

我们预期的结果是bar(foo)变成(1 +(foo)),然后变成(1 +(a,b))。

然而bar(foo)扩展为loss(a,b)会出错,因为Los需要一个参数。在这种情况下,该问题可以通过使用相同的括号轻松解决,该括号应用于防止算术运算的错误嵌套:

#define foo (a,b)

or

#define bar(x) lose((x))

多余的一对括号可防止foo定义中的逗号被解释为参数分隔符。

参数中的换行符

类似函数的宏的调用可以扩展到许多逻辑行,但是在本实施方式中,整个扩展是一行完成的。

因此,由编译器或调试器发出的行号是指调用在其上开始的行,这可能与包含导致问题的参数的行不同,例如:

#define ignore_second_arg(a,b,c) a; c

ignore_second_arg (foo (),
                  ignored (),
                  syntax error);

由Syntax error on tokens触发的语法错误会导致错误消息引用第三行(ignore_second_arg行),即使有问题的代码来自第五行。

image.png

声明: 本文由入驻OFweek维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存