00. 目录

文章目录

  • 00. 目录
  • 01. 可变参数宏概述
  • 02. ##符号
  • 03. 可变参宏另外一种写法
  • 04. 内核中的可变参数宏
  • 05. 附录

01. 可变参数宏概述

#include <stdarg.h>void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);

Linux平台可能的实现

/** Storage alignment properties*/
#define  _AUPBND                (sizeof (acpi_native_int) - 1)
#define  _ADNBND                (sizeof (acpi_native_int) - 1)/** Variable argument list macro definitions*/
#define _bnd(X, bnd)            (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)              (void) 0
#define va_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

其实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", );

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

02. ##符号

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

#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( ) 函数可以直接打印变量 a1、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 后面的逗号删除掉,这样宏也就可以正常使用了。

03. 可变参宏另外一种写法

当我们定义一个变参宏时,除了使用预定义标识符 __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__ 看起来更加直观和方便。

04. 内核中的可变参数宏

可变参数宏在内核中主要用于日志打印。一些驱动模块或子系统有时候会定义自己的打印宏,可以支持打印开关、打印格式、优先级控制等。如在 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");elseDEBUG();return 0;
}

运行结果

hello if
else

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

int main(void){if(1)printf("hello if\n");elseprintf("hello ");printf("else\n");return 0;}

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

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

05. 附录

参考:C语言嵌入式Linux高级编程

【嵌入式】C语言高级编程-可变参数宏(12)相关推荐

  1. c语言宏定义可变参数,C语言可变参数宏定义方法

    http://blog.csdn.net/skyflying2012/article/details/38436711 2014 在GNU C中,宏可以接受可变数目的参数,就象函数一样,例如: 1 2 ...

  2. c语言宏不能传递参数,关于c ++:可变参数宏:不能通过’…’传递非平凡复制类型的对象...

    我正在尝试为日志记录机制编写宏. 我编写了一个可变参数宏,但它不适用于std::string. 该代码如下所示: #include #include #define LOG_NOTE(m, ...) ...

  3. c语言高级程序设计第五版PDF,C语言高级编程.pdf

    C语言高级编程 概述 由几个测试程序说开去 预编译与宏 高级预编译介绍 宏的高级用法 变量 变量分类详细解析 我的变量去哪儿了? 大小端对变量的影响 内存与指针 常见内存使用错误大观 指针,又是指针! ...

  4. 鼠标绘图 c语言,c语言高级编程技术教程 图形显示方式与鼠标输入.doc

    c语言高级编程技术教程 图形显示方式与鼠标输入 c语言高级编程技术教程 图形显示方式和鼠标输入 图形显示方式和鼠标输入 问题的提出编写程序,使用鼠标进行如下操作:按住鼠标器的任意键并移动,十字光 标将 ...

  5. c语言理解参数,c语言中对可变参数列表的简单理解

    函数原型中一般情况下参数的数目是固定的,但是如果想在不同的时候接收不定数目的参数时该怎么办呢?c语言提供了可变参数列表来实现. 可变参数列表是通过宏来实现的,这些宏定义在stdarg.h的头文件中.头 ...

  6. 高级编程中C语言属于,c语言高级编程

    c语言高级编程 C高级编程 责任编辑:admin 更新日期:2005-8-6 深入了解C语言(函数的参数传递和函数使用参数的方法) tangl_99(原作) 关键字 C语言,汇编,代码生成,编译器 C ...

  7. #{}不自动改参数类型_【Just For Fun】C - 可变参数函数、可变参数宏 __VA_ARGS__、额外的逗号

    [Just For Fun] 本系列纯粹娱乐.研究用.一些旁门左道的东西. 事实上可能完全没用. (๑•̀ω•́๑) 對於可变参数函数.可变参数宏 __VA_ARGS__ , 我曾經有在另一些地方寫過 ...

  8. Go 学习推荐 —(Go by example 中文版、Go 构建 Web 应用、Go 学习笔记、Golang常见错误、Go 语言四十二章经、Go 语言高级编程)

    Go by example 中文版 Go 构建 Web 应用 Go 学习笔记:无痕 Go 标准库中文文档 Golang开发新手常犯的50个错误 50 Shades of Go: Traps, Gotc ...

  9. 《go语言圣经》+《Mastering.GO-cn》+《go语言高级编程》PDF下载

    公众号[爱吃橙子的搬砖小徐]开通啦,后续将会同步更新,欢迎订阅 回复[java面试]获得两套面试宝典 回复[golang]获得go语言学习三部曲 <go语言圣经>+<Masterin ...

最新文章

  1. linux查看nginx运行状态,如何查看nginx运行状态及相关属性说明
  2. sql advantage 执行sql_Sql 的执行顺序是怎样的?
  3. 你好,面试官 | 你拿Java Map考验老干部?
  4. 服务器找不到硬盘如何解决方案,Linux云服务器磁盘不见了?解决方案在这里
  5. spring cloud构建互联网分布式微服务云平台- Netflix
  6. elk日志收集系统 linux_ELK 日志分析系统
  7. 郝斌java教程下载_郝斌Java自学教程全集打包,视频教程下载
  8. 几大经典算法c语言cnds,浮点数据有损压缩算法 附完整C代码
  9. 如何将汇编语言转换为c语言,如何把汇编语言转换成C语言
  10. 制作温馨浪漫爱心表白动画特效HTML5+jQuery【附源码】
  11. cmake 历史版本
  12. 布料仿真中常用积分方法
  13. 开平方算法的C++实现
  14. 微波射频学习笔记9--------品质因数Q值的意义
  15. oracle 与plc通信,cim系统(com系统和plc的通讯)
  16. Python爬虫 | Python爬虫获取女友图片
  17. mysql数据库 数据查询闯关(头哥)
  18. 常见的图片比例有哪些?App中不同图片比例适用场景
  19. bzoj 3007 拯救小云公主
  20. 更改bootstrap工具提示-tooltip

热门文章

  1. 在source insight 中添加系统字体
  2. asp:UpdatePanel中使用下拉时不得不注意的问题
  3. html页面取js里面的值,如何在javascript中获取HTML元素的样式值?
  4. python电话号码对应的字符组合_Python3 在字符串中提取字母+数字组合微信账号、电话等 - pytorch中文网...
  5. 2013年海康威视校园招聘笔试题
  6. git命令详解( 八)
  7. VMware与Centos7系统安装
  8. Aiiage Camp Day4 A Board game
  9. 【BZOJ1899】[Zjoi2004]Lunch 午餐 贪心+DP
  10. linux 设置mysql 数据库编码utf8