可变参数宏标识符__VA_ARGS__当你第一次看到这种宏定义和用法的时候是不是一头雾水#defineDEBUG(...)printf(__VA_ARGS__)DEBUG(x%d\n,x);...是什么三个点也能当参数__VA_ARGS__这一串全大写、还带双下划线的东西又是什么变量宏关键字DEBUG(x%d\n, x)调用的时候传了两个参数格式串和x可宏定义里DEBUG(...)却只写了一个...这参数是怎么对上的最后展开出来的printf(__VA_ARGS__)__VA_ARGS__又是怎么摇身一变变成x%d\n, x这两个东西的如果你也有这些疑惑说明你的直觉是对的——这确实不是普通函数调用那套形参对实参的直觉能解释的因为宏定义根本不是函数它不在运行时工作而是在编译之前的预处理阶段靠纯粹的文本/记号替换来完成这一切。功能介绍__VA_ARGS____VA_ARGS__是 C99 和 C11 标准引入的一个预定义宏标识符专门用于定义可变参数宏。简单来说它让你能像创建 printf 那样写出可以接受不定数量参数的宏极大地提升了代码的灵活性。......在宏定义的参数列表里...是 C99 标准引入的一个专用语法符号术语叫 ellipsis省略号。它是预处理器语法中明确定义、有专门含义的记号作用是告诉预处理器从这个位置开始允许调用者传入数量不确定0个、1个或任意多个具体依据实现的额外实参。注意它只能出现在宏参数列表的最后。常见用法写法上有两种常见应用方式/* 全部参数都是可变参数 */#defineDEBUG(...)printf(__VA_ARGS__)/* 第一个参数固定其余可变 */#defineLOG(fmt,...)printf(fmt,__VA_ARGS__)//没有额外的可变参数传入时会出现多余的逗号#defineLOG(fmt,...)printf(fmt,##__VA_ARGS__)// GNU 扩展自动去除前导逗号过程解析整个解析过程就是预处理器对宏定义进行展开的过程。/*展开过程示例*/#defineLOG(fmt,...)printf(fmt,__VA_ARGS__)LOG(a%d b%d\n,a,b);第一步预处理器看到宏定义#defineLOG(fmt,...)printf(fmt,__VA_ARGS__)预处理器解析这行定义时会做以下事情识别出宏名LOG并确定对应的替换目标printf解析参数列表(fmt, ...)这里...是一个特殊记号预处理器识别到它时会在内部标记这个宏是变参宏变参宏替换目标中出现的__VA_ARGS__被记录为一个占位符绑定到可变参数集合这个位置最后预处理器在扫描完后会为...这个宏建立一张参数表形参位置名称1fmt2 (可变参数)VA_ARGS第二步预处理器看到调用LOG(a%d b%d\n,a,b);进行参数分割预处理器会将第一个逗号之前的内容a%d b%d\n绑定给fmt逗号之后剩余的所有参数a, b打包成一个整体统一绑定给__VA_ARGS__这个记号。第三步替换预处理器在宏体中做替换时先将整个调用语句替换为宏体文本printf(fmt, __VA_ARGS__)然后逐个对该文本进行记号扫描遇到fmt→ 替换为实参a%d, b%d\n遇到,宏体里写的字面逗号→ 原样保留遇到__VA_ARGS__→ 替换为之前打包的记号序列a, b最后得到printf(a%d b%d\n,a,b);完整替换过程LOG(a%d b%d\n, a, b); ↓ printf(fmt , __VA_ARGS__); ↓ ↓ ↓ printf( a%d b%d\n, a, b )