电子工程师技术服务社区
公告
登录
|
注册
首页
技术问答
厂商活动
正点原子
板卡试用
资源库
下载
文章
社区首页
文章
避免这7个误区,才能让【宏】削铁如泥
分 享
扫描二维码分享
避免这7个误区,才能让【宏】削铁如泥
C语言
李肖遥
关注
发布时间: 2020-10-16
丨
阅读: 3322
## 语法错误 当使用参数调用宏时,会将参数替换为宏主体,并与其他输入文件一起检查结果,以进行更多的宏调用,可以将部分来自宏主体和部分自变量的宏调用组合在一起。 例如, ``` #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行),即使有问题的代码来自第五行。
原创作品,未经权利人授权禁止转载。详情见
转载须知
。
举报文章
点赞
(
0
)
李肖遥
关注
评论
(0)
登录后可评论,请
登录
或
注册
相关文章推荐
MK-米客方德推出工业级存储卡
Beetle ESP32 C3 蓝牙数据收发
Beetle ESP32 C3 wifi联网获取实时天气信息
开箱测评Beetle ESP32-C3 (RISC-V芯片)模块
正点原子数控电源DP100测评
DP100试用评测-----开箱+初体验
Beetle ESP32 C3环境搭建
【花雕体验】16 使用Beetle ESP32 C3控制8X32位WS2812硬屏之二
X
你的打赏是对原创作者最大的认可
请选择打赏IC币的数量,一经提交无法退回 !
100IC币
500IC币
1000IC币
自定义
IC币
确定
X
提交成功 ! 谢谢您的支持
返回
我要举报该内容理由
×
广告及垃圾信息
抄袭或未经授权
其它举报理由
请输入您举报的理由(50字以内)
取消
提交