当前位置:首页 > > 嵌入式微处理器
[导读]当使用参数调用宏时,会将参数替换为宏主体,并与其他输入文件一起检查结果,以进行更多的宏调用,可以将部分来自宏主体和部分自变量的宏调用组合在一起。


语法错误

当使用参数调用宏时,会将参数替换为宏主体,并与其他输入文件一起检查结果,以进行更多的宏调用,可以将部分来自宏主体和部分自变量的宏调用组合在一起。例如,

#define twice(x) (2*(x))
#define call_with_1(x) x(1)
call_with_1 (twice)

//x=1
 → twice(1)
 → (2*(1))

宏定义不必带有括号,通过在宏主体中编写不平衡的开放括号,可以创建一个从宏主体内部开始但在宏主体外部结束的宏调用。例如,

#define strange(file) fprintf (file, "%s %d",

strange(stderr) p, 35)
     → fprintf (stderr, "%s %d", p, 35)

组合宏调用的功能可能会很有用,但是在宏主体中使用不平衡的开放括号只会造成混淆,应该避免。

运算符优先级问题

在大多数宏定义示例中,每次出现的宏参数名称都带有括号,并且另一对括号通常会包围整个宏定义,这是编写宏最好的方式。举个例子

#define ceil_div(x, y) (x + y - 1) / y

假定其用法如下:

a = ceil_div(b&c,sizeof(int));

拓展开是

a =(b&c + sizeof(int)-1)/ sizeof(int);

这没有达到我们的预期,C的运算符优先级规则使其等效于此,而我们想要的是:

a =(((b&c)+ sizeof(int)-1))/ sizeof(int);

如果我们将宏定义为

#define ceil_div(x,y)((x)+(y)-1)/(y)

可能导致另一种情况,sizeof ceil_div(1,2)是一个C表达式,可以计算ceil_div(1,2)类型的大小,它扩展为:

sizeof((1)+(2)-1)/(2)

这将采用整数的大小并将其除以2,而除法包含在内部的sizeof之外。所以整个宏定义的括号可防止此类问题。那么,下面是定义ceil_div的正确方法如下

#define ceil_div(x,y)((((x)+(y)-1)/(y))

吞噬分号

通常需要定义一个扩展为复合语句的宏。例如,考虑以下宏,该宏跨空格字符前进一个指针(参数p表示在何处查找):

#define SKIP_SPACES(p, limit)  \
{ char *lim = (limit);         \
  while (p < lim) {            \
    if (*p++ != ' ') {         \
      p--; break; }}}

该宏定义必须是单个逻辑行,严格来说,该调用扩展为复合语句,这是一个完整的语句,不需要用分号结束。

但是,由于它看起来像函数调用,因此,如果可以像使用函数调用一样使用它,则可以最大程度地减少混乱,然后再写一个分号,就像在SKIP_SPACES(p,lim)中一样。

这可能会在else语句之前出问题,因为分号实际上是空语句。假设你写

if (*p != 0)
  SKIP_SPACES (p, lim);
else …

在if条件和else条件之间存在两个语句(复合语句和null语句)使C代码无效。

怎么解决?我们可以使用do…while语句更改宏SKIP_SPACES的定义以解决此问题。方法如下:

#define SKIP_SPACES(p, limit)     \
do { char *lim = (limit);         \
     while (p < lim) {            \
       if (*p++ != ' ') {         \
         p--; break; }}}          \
while (0)

SKIP_SPACES (p, lim);扩展为

do {…} while (0);

这是一个陈述,循环仅执行一次,而且大多数编译器不会为此生成任何额外的代码。

重复调用

我们常见的“最小”定义一个宏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)

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

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

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

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

自引用规则将这一过程缩短了一步,即(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行),即使有问题的代码来自第五行。

参考资料:

http://gcc.gnu.org/onlinedocs/cpp/Macros.html



-END-


来源 | 技术让梦想更伟大

作者 | 李肖遥


整理文章为传播相关技术,版权归原作者所有 |

如有侵权,请联系删除 |


【1】嵌入式研发10多年,工程师悟出这些道理

【2】当谈起嵌入式工程师,究竟在谈些什么

【3】嵌入式工程师出路之我见:就业,技术,行业...

【4】为什么嵌入式工程师会对8位MCU有误解?

【5】嵌入式工程师结合经历聊硬件工程师和软件工程师哪个更有前途?



免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

嵌入式ARM

扫描二维码,关注更多精彩内容

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭