c语言宏 可变参数,嵌入式C语言自我修养 (12):有一种宏,叫可变参数宏

12.1 什么是可变参数宏

在上面的教程中,咱们学会了变参函数的定义和使用,基本套路就是使用 va_list、va_start、va_end 等宏,去解析那些可变参数列表咱们找到这些参数的存储地址后,就能够对这些参数进行处理了:要么本身动手,本身处理;要么继续调用其它函来处理。html

void print_num(int count, ...)

{

va_list args;

va_start(args,count);

for(int i = 0; i < count; i++)

{

printf("*args: %d\n",*(int *)args);

args += 4;

}

}

void __attribute__((format(printf,2,3))) LOG(int k,char *fmt,...)

{

va_list args;

va_start(args,fmt);

vprintf(fmt,args);

va_end(args);

}

GNU C 以为这样不过瘾,再来个猛锤:干脆宏定义也支持变参吧!linux

这一节咱们要学习一下可变参数宏的定义和使用。其实,C99 标准已经支持了这个特性,可是其它的编译器不太给力,对 C99 标准的支持不是很好,只有 GNU C 支持这个功能,因此有时候咱们也把这个可变参数宏看做是 GNU C 的一个语法扩展。 上面的 LOG 函数,若是咱们想使用一个变参宏实现,就能够直接这样定义。编程

#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)

#define DEBUG(...) printf(__VA_ARGS__)

int main(void)

{

LOG("Hello! I'm %s\n","Wanglitao");

DEBUG("Hello! I'm %s\n","Wanglitao");

return 0;

}

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

使用宏定义实现一个变参打印功能,你会发现,它的实现甚至比变参函数还方便!内核中的不少打印宏,常用可变参数宏来实现,宏定义通常为下面这个格式。微信

#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)

在这个宏定义中,有一个固定参数,一般为一个格式字符串,后面的变参用来打印各类格式的数据,跟前面的格式字符串相匹配。这种定义方式有一个漏洞,即当变参为空时,宏展开时就会产生一个语法错误。函数

#define LOG(fmt,...) printf(fmt,__VA_ARGS__)

int main(void)

{

LOG("hello\n");

return 0;

}

上面这个程序编译时就会通不过,产生一个语法错误。这是由于,咱们只给 LOG 宏传递了一个参数,而变参为空。当宏展开后,就变成了下面这个样子。学习

printf("hello\n", );

宏展开后,在第一个字符串参数的后面还有一个逗号,因此就产生了一个语法错误。咱们须要继续对这个宏进行改进,使用宏链接符 ##,来避免这个语法错误。.net

12.2 继续改进咱们的宏

咱们接下来,使用宏链接符 ## 来改进上面的宏。debug

宏链接符 ## 的主要做用就是链接两个字符串,咱们在宏定义中可使用 ## 来链接两个字符。预处理器在预处理阶段对宏展开时,会将 ## 两边的字符合并,并删除 ## 这两个字符。调试

#define A(x) a##x

int main(void)

{

int A(1) = 2; //int a1 = 2;

int A() = 3;  //int a=3;

printf("%d %d\n",a1,a);

return 0;

}

如上面的程序,咱们定义一个宏。

#define A(x) a##x

这个宏的功能就是链接字符 a 和 x。在程序中,A(1) 展开后就是 a1,A( ) 展开后就是 a。咱们使用 printf( ) 函数能够直接打印变量 a一、a 的值,由于宏展开后,就至关于使用 int 关键字定义了两个整型变量 a1 和 a。上面的程序能够编译经过,运行结果以下。

2 3

知道了宏链接符 ## 的使用方法,咱们接下来就能够就对 LOG 宏作一些修改。

#define LOG(fmt,...) printf(fmt, ##__VA_ARGS__)

int main(void)

{

LOG("hello\n");

return 0;

}

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

12.3 可变参数宏的另外一种写法

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

#define LOG(fmt,args...) printf(fmt, args)

使用预约义标识符 __VA_ARGS__ 来定义一个变参宏,是 C99 标准规定的写法。而上面这种格式是 GNU C 扩展的一个新写法。咱们再也不使用 __VA_ARGS__,而是直接使用 args... 来表示一个变参列表,而后在后面的宏定义中,直接使用 args 表明变参列表就能够了。

跟上面同样,为了不变参列表为空时的语法错误,咱们也须要添加一个链接符##。

#define LOG(fmt,args...) printf(fmt,##args)

int main(void)

{

LOG("hello\n");

return 0;

}

使用这种方式,你会发现这种写法比使用 __VA_ARGS__ 看起来更加直观和方便。

12.4 内核中的可变参数宏

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

#if defined(CONFIG_DYNAMIC_DEBUG)

#define pr_debug(fmt, ...) \

dynamic_pr_debug(fmt, ##__VA_ARGS__)

#elif defined(DEBUG)

#define pr_debug(fmt, ...) \

printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)

#else

#define pr_debug(fmt, ...) \

no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)

#endif

#define dynamic_pr_debug(fmt, ...)               \

do {                               \

DEFINE_DYNAMIC_DEBUG_METADATA(descriptor, fmt); \

if (unlikely(descriptor.flags       \

& _DPRINTK_FLAGS_PRINT))   \

__dynamic_pr_debug(&descriptor, pr_fmt(fmt),   \

##__VA_ARGS__);     \

} while (0)

static inline __printf(1, 2)

int no_printk(const char *fmt, ...)

{

return 0;

}

#define __printf(a, b)   \

__attribute__((format(printf, a, b)))

看到这个宏定义,不得不佩服宏的做者。一个小小的宏,综合运用各类技巧和知识点,把 C 语言发挥到极致!

这个宏定义了三个版本。若是咱们在编译内核时有动态调试选项,那么这个宏就定义为 dynamicprdebug。若是没有配置动态调试选项,那咱们还能够经过 DEBUG 这个宏,来控制这个宏的打开和关闭。

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

最有意思的是 dynamicprdebug 宏,宏定义采用 do{ ... }while(0) 结构。这看起来貌似有点多余,有它没它,咱们的宏均可以工做。反正都是执行一次,为何要用这种看似“多此一举”的循环结构呢?道理很简单,这样定义就是为了防止宏在条件、选择等分支结构的语句中展开后,产生宏歧义。

好比咱们定义一个宏,由两条打印语句构成。

#define DEBUG() \

printf("hello ");printf("else\n")

int main(void)

{

if(1)

printf("hello if\n");

else

DEBUG();

return 0;

}

程序运行结果以下。

hello if

else

理论状况下,else 分支是执行不到的。但经过运行结果能够看到,程序也执行了 else 分支的一部分代码。这是由于咱们定义的宏由多条语句组成,直接展开后,就变成了下面这样。

int main(void)

{

if(1)

printf("hello if\n");

else

printf("hello ");

printf("else\n");

return 0;

}

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

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

本文根据《C语言嵌入式Linux高级编程》部分章节改编,视频学习可访问CSDN学院:https://edu.csdn.net/combo/detail/1038

微信公众号:宅学部落(armlinuxfun)

QQ群:475504428

你可能感兴趣的:(c语言宏,可变参数)