为什么在C语言中要用其他宏递归定义宏?

5

我想了解arduino函数digitalWrite的实际工作原理。但是当我查找该函数的源代码时,发现它充满了宏定义,而这些宏定义本身又是基于其他宏定义。为什么会以这种方式构建代码,而不是使用一个函数?这是编程风格不佳还是在C语言中正确的做法?

例如,digitalWrite包含宏定义digitalPinToPort

#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )

pgm_read_byte 是一个宏:

#define pgm_read_byte(address_short)    pgm_read_byte_near(address_short)

pgm_read_byte_near是一个宏:

#define pgm_read_byte_near(address_short) __LPM((uint16_t)(address_short))

__LPM 是一个宏:

#define __LPM(addr)         __LPM_classic__(addr)

__LPM_classic__ 是一个宏:

#define __LPM_classic__(addr)   \
(__extension__({                \
    uint16_t __addr16 = (uint16_t)(addr); \
    uint8_t __result;           \
    __asm__ __volatile__        \
    (                           \
        "lpm" "\n\t"            \
        "mov %0, r0" "\n\t"     \
        : "=r" (__result)       \
        : "z" (__addr16)        \
        : "r0"                  \
    );                          \
    __result;                   \
}))

虽然不是直接相关的内容,但我认为双下划线应该只由编译器使用。在 LPM 前缀加上 __ 是否妥当?


5
当你只有一把锤子,即C语言时,所有事情都开始看起来像你的大拇指。 - ti7
你所描述的代码似乎使用了异常密集的宏,但是你的问题实质上是关于一般编码风格的问题,这主要是一个观点问题。因此,这个问题不适合在SO上提问,我已经投票关闭了它。 - John Bollinger
1
@JohnBollinger 在哪个论坛上合适让 Legion 询问这种“风格”背后的动机和编码哲学? - Patrick Trentin
@PatrickTrentin,这个问题不适合在Code Review SE上讨论,但那里的问题通常会引发对代码风格的讨论。也许在Software Engineering SE上提出这样的问题会受到欢迎。无论如何,无论是这两个网站还是其他一些网站,是否适合该问题都与它是否适合场所无关。 - John Bollinger
你可以使用Eclipse来完全展开Arduino使用的宏...事实上,有这个插件(http://playground.arduino.cc/Code/Eclipse),可以让你做到这一点...只需将键盘悬停在digitalWrite上方,就会弹出一个窗口,其中包含完全展开的宏。 - Luiz Menezes
@JohnBollinger 我并不质疑您的判断,我同意您的看法,我只是想给Legion指出正确的方向去发表这个问题,因为我对答案很感兴趣。我认为您比我更清楚应该把他送到哪里。谢谢。 - Patrick Trentin
2个回答

6
如果你的问题是“为什么要使用多层宏?”,那么:
  • 为什么不呢?特别是在没有inline的C99之前。一个经典的例子是20世纪80年代的getc函数,在那个时候(SunOS3.2,1987)被记录为宏定义,man页面中有一条注意告诉我们(我忘记了详情),如果有一些FILE* filearr[];,那么getc(filearr[i++])是错误的(我记得当时还没有未定义行为这个术语)。当你查看一些系统头文件(例如<stdio.h>或者包含它的头文件)时,你会找到这样的宏定义。而且在那个时候(计算机只运行几兆赫兹,比今天慢上千倍),getc 必须是一个宏定义来提高效率(因为没有inline,编译器也没有像现在这样的过程间优化)。当然,你可以在自己的宏定义中使用getc
  • 即使在今天,一些标准也定义了宏。特别是今天的waitpid(2)系统调用文档将WIFEXITEDWEXITSTATUS定义为宏,混合使用这两个宏是明智的。
  • 主要的问题是要理解C预处理器的工作原理及其非常文本化(因此非常脆弱)的本质。这在所有关于C语言的教科书中都有解释。因此,你需要了解发生了什么。
  • 现代C(即C99和C11)的经验法则是系统地优先使用一些静态内联函数(在某些头文件中定义)而不是等效的宏定义。换句话说,只有当无法避免时才使用#define一些宏定义。并明确记录下这一点。
  • 多层宏可能会(有时)提高代码可读性。
  • 宏可以用#ifdef macroname进行测试,这有时很有用。
当你敢于定义几层宏(我不会称之为“递归”,请阅读自引用宏的链接)时,当然需要非常小心并且理解它们(组合和单独使用)的情况。查看预处理形式是有帮助的。
顺便说一句,为了调试复杂的宏,有时我会这样做:
gcc -C -E -I/some/directory mysource.c | sed 's:^#://#:' > mysource.i

然后我查看mysource.i,有时甚至需要编译它,例如gcc -c -Wall mysource.i,以获取位于预处理形式mysource.i中的警告(我可以在我的emacs编辑器中检查)。 "注释"以#开头的行,这些行设置源位置(类似于#line)的sed命令.... 有时我甚至会执行indent mysource.i。(实际上,在我的Makefile中有一个特殊的规则)。

使用LPM前缀为__是正确的吗?

顺便说一下,以_开头的名称(按照标准和传统惯例)保留给实现。原则上,您不允许使用以_开头的名称,因此不可能发生任何冲突。

注意,__LPM_classic__ 宏使用 statement-expr extension (来自 GCCClang)。
另外看看其他编程语言。Common Lisp 有一个非常不同的宏模型(并且更有趣)。阅读关于 卫生宏 的文章。我个人的遗憾是 C 预处理器从未发展成为更强大的工具(类似 Scheme)。这种情况之所以没有发生(想象一下能够调用 Guile 代码进行宏扩展的 C 预处理器!)可能是出于社会和经济的原因。

你仍然应该有时考虑使用其他预处理器(如m4GPP)来生成C代码。请参见autoconf


4
目的很可能是为了优化使用不支持内联函数的早期C99编译器(通过inline关键字)的函数调用。这样,函数宏的整个堆栈实际上被预处理器合并成单个代码块。
每次在C中调用函数时,都会有一点开销,因为要跳转程序代码、管理堆栈帧和传递参数。在大多数应用程序中,这种成本可以忽略不计,但如果函数被频繁调用,则可能成为性能瓶颈。
很难给出明确的答案,因为编码风格是主观的话题。我建议考虑使用内联函数,甚至更好的是让编译器自动内联它们。它们是类型安全的,更易读,更可预测,并且在编译器的适当帮助下,净结果基本相同。
相关参考(它是针对C++的,但对于C来说,思路基本相同):

这讲述了一般使用宏的一个原因,但它并没有涉及到OP主要关注的问题,即使用多层宏。 - John Bollinger
另一个原因可能是,由于有许多不同类型的AVR微控制器,因此使用宏可以在编译时基于设备类型选择某些操作的正确实现,或者至少为未来留下这个选项。 - Unimportant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接