# 工欲善其事必先利其器-C语言拓展--嵌入式C语言(十一)

简介: # 工欲善其事必先利其器-C语言拓展--嵌入式C语言(十一)

工欲善其事必先利其器-C语言拓展–嵌入式C语言(十一)

这里来到了最后的一个part–>可变参数宏

文章内容全部来自–>《嵌入式C语言自我修养——从芯片、编译器到操作系统》 王利涛前辈的,超级推荐

在之前内容有变参函数的定义和使用,如果你不知道可以先看看,这里基本的套路就是使用va_list、va_start、va_end等宏,去解析那些可变参数列表。

找到这些参数的存储地址后,就可以对这些参数进行处理了。要么自己动手,亲自处理;要么继续调用其他函数来处理。

整个例子:

GNU C还助一臂之力:干脆宏定义也支持可变参数吧!

什么是可变参数宏

C99标准已经支持了这个特性,但是其他编译器不太给力,对C99标准的支持不是很好,只有GNU C标准支持这个功能,所以有时候我们也把这个可变参数宏看作GNU C标准的一个语法扩展。

上面实现的LOG()变参函数,如果我们想使用一个可变参数宏实现,就可以直接这样定义。

可变参数宏的实现形式其实和变参函数差不多:用…表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开。可变参数宏使用C99标准新增加的一个__VA_ARGS__预定义标识符来表示前面的变参列表,而不是像变参函数一样,使用va_list、va_start、va_end这些宏去解析变参列表。预处理器在将宏展开时,会用变参列表替换掉宏定义中的所有__VA_ARGS__标识符。

使用宏定义实现一个打印功能的变参宏,你会发现,它的实现甚至比变参函数还简单!Linux内核中的很多打印宏,经常使用可变参数宏来实现,宏定义一般为下面这个格式。

在这个宏定义中,有一个固定参数,通常为一个格式字符串,后面的变参用来打印各种格式的数据,与前面的格式字符串相匹配。这种定义方式比较容易理解,但是有一个漏洞:当变参为空时,宏展开时就会产生一个语法错误。

上面这个程序在编译时就会报错,产生一个语法错误。这是因为,我们只给LOG宏传递了一个参数,而变参为空。当宏展开后,就变成了下面的样子。

宏展开后,在第一个字符串参数的后面还有一个逗号,不符合语法规则,所以就产生了一个语法错误。我们需要继续对这个宏进行改进,使用宏连接符##,可以避免这个语法错误。

继续改进我们的宏

接下来,我们使用宏连接符##来改进上面的宏。宏连接符##的主要作用就是连接两个字符串。我们在宏定义中可以使用##来连接两个字符,预处理器在预处理阶段对宏展开时,会将##两边的字符合并,并删除##这个连接符。

在上面的程序中,我们定义一个宏。

这个宏的功能就是连接字符a和x。在程序中,A(1)展开后就是a1,A()展开后就是a。我们使用printf()函数可以直接打印变量a1、a的值,因为宏展开后,就相当于使用int关键字定义了两个整型变量a1和a。上面的程序可以编译通过,运行结果如下。

知道了宏连接符##的使用方法,我们就可以对LOG宏做一些修改。

我们在标识符__VA_ARGS__前面加上了宏连接符##,这样做的好处是:当变参列表非空时,##的作用是连接fmt和变参列表,各个参数之间用逗号隔开,宏可以正常使用;当变参列表为空时,##还有一个特殊的用处,它会将固定参数fmt后面的逗号删除掉,这样宏就可以正常使用了。

可变参数宏的另一种写法

当我们定义一个变参宏时,除了使用预定义标识符__VA_ARGS__表示变参列表,还可以使用下面这种写法。

使用预定义标识符__VA_ARGS__来定义一个变参宏,是C99标准规定的写法。而上面这种格式是GNU C扩展的一个新写法:可以不使用__VA_ARGS__,而是直接使用args…来表示一个变参列表,然后在后面的宏定义中,直接使用args代表变参列表就可以了。

和上面一样,为了避免变参列表为空时的语法错误,我们也需要在参数之间添加一个连接符##。

使用这种宏定义方式,你会发现比使用__VA_ARGS__看起来更加直观,更加容易理解。

内核中的可变参数宏

可变参数宏在内核中主要用于日志打印。一些驱动模块或子系统有时候会定义自己的打印宏,支持打印开关、打印格式、优先级控制等功能。如在printk.h头文件中,我们可以看到pr_debug宏的定义。

看到这个宏定义,估计有两个字已经在很多人心中来回荡漾,差点忍不住冲破喉咙,脱口而出,但同时又不得不佩服宏的作者:一个小小的宏,却能综合运用各种技巧和知识点,把C语言的潜能发挥得淋漓尽致。

这个宏定义了三个版本:如果我们在编译内核时有动态调试选项,那么这个宏就定义为dynamic_pr_debug。如果没有配置动态调试选项,则我们可以通过DEBUG这个宏,来控制这个宏的打开和关闭。

no_printk()作为一个内联函数,定义在printk.h头文件中,而且通过format属性声明,指示编译器按照printf标准去做参数格式检查。

最有意思的是dynamic_pr_debug这个宏,宏定义采用do{…}while(0)结构。这看起来貌似有点多余:有它没它,我们的宏都可以工作。反正都是执行一次,为什么要用这种看似“画蛇添足”的循环结构呢?

道理其实很简单,这样定义是为了防止宏在条件、选择等分支结构的语句中展开后,产生宏歧义。例如我们定义一个宏,由两条打印语句构成。

例如我们定义一个宏,由两条打印语句构成。

理论情况下,else分支是执行不到的,但通过打印结果可以看到,程序也执行了else分支的一部分代码。这是因为我们定义的宏由多条语句组成,经过预处理展开后,就变成了下面这样。

多条语句在宏调用处直接展开,就破坏了程序原来的if/else分支结构,导致程序逻辑发生了变化,所以你才会看到else分支的非正常打印。而采用do{…}while(0)这种结构,可以将我们宏定义中的复合语句包起来。宏展开后,是一个代码块,避免了这种逻辑错误。

一个小小的宏,暗藏各个知识点,综合使用各种技巧,仔细分析下来,能学到很多知识。大家在以后的工作和学习中,可能会接触到各种各样、形形色色的宏。只要有牢固的C语言基础,熟悉GNU C的常用扩展语法,再遇到这样类似的宏,我们都可以自己尝试慢慢去分析了。不用怕,只有自己真正分析过,才算真正掌握,才能转化为自己的知识和能力,才能领略它的精妙之处。

目录
相关文章
|
21天前
|
编译器 Linux C语言
嵌入式C语言(八)
嵌入式C语言(八)
22 0
|
21天前
|
存储 编译器 C语言
嵌入式C语言(六)
嵌入式C语言(六)
28 0
|
21天前
|
存储 编译器 程序员
嵌入式C语言(七)
嵌入式C语言(七)
24 0
|
21天前
|
编译器 C语言 芯片
嵌入式C语言(九)
嵌入式C语言(九)
19 0
|
21天前
|
缓存 小程序 编译器
嵌入式C语言(十)
嵌入式C语言(十)
30 0
|
6天前
|
数据处理 调度 C语言
C语言:嵌入式硬件利器
C语言:嵌入式硬件利器
|
12天前
|
人工智能 物联网 数据处理
C语言在嵌入式系统中的应用
该文探讨了C语言在嵌入式系统中的应用,强调其优势,如可移植性、高效性、灵活性及社区支持,并列举了在RTOS开发、驱动程序、通信协议实现和简单GUI开发中的应用场景。文中通过LED闪烁程序示例展示了C语言如何控制硬件。结论指出,C语言在嵌入式系统中扮演重要角色,随着技术发展,开发者需不断学习以适应新需求。
|
21天前
|
安全 算法 开发工具
【C 言专栏】基于 C 语言的嵌入式系统开发
【5月更文挑战第1天】本文探讨了C语言在嵌入式系统开发中的核心作用。嵌入式系统作为专用计算机系统广泛应用于家电、汽车、医疗等领域,具备实时性、低功耗等特点。C语言因其高效性、可移植性和灵活性成为开发首选。文章介绍了开发流程,包括需求分析、硬件选型、软件设计至部署维护,并强调中断处理、内存管理等关键技术。C语言在智能家居、汽车电子和医疗设备等领域的应用实例展示了其广泛影响力。面对硬件限制、实时性要求和安全挑战,开发者需不断优化和适应新技术趋势,以推动嵌入式系统创新发展。
【C 言专栏】基于 C 语言的嵌入式系统开发
|
21天前
|
传感器 算法 C语言
C语言在嵌入式系统开发中的优化策略与代码实现
C语言在嵌入式系统开发中的优化策略与代码实现
33 1
|
21天前
|
存储 编译器 Linux
嵌入式C语言(五)
嵌入式C语言(五)
17 0