文章目录
- 概述
- va_ 系列定义
- va_list 类型
- va_start 宏
- 从变参函数的强制参数谈起
- 宏 va_start 对 char 和 short 类型编译告警
- 宏 va_start 源码分析
- 猜测 __va_start 函数实现
- va_arg 宏
- 宏 va_arg 无法接受 char 和 short
- 为啥va_arg可解析int却不能解析float类型?
- 宏 va_arg 源码解析
- 变参数类型: sizeof(结构体) > sizeof(_int64)
- va_end 宏
- 小结
概述
本文基于丰富的可变参函数设计、实现、使用实践经验,进一步结合对 va_list、va_start 、va_arg、va_end 源码分析,剖析了变参列表的处理过程,重新回答和解释了变参函数实现和使用过程中遇到的诸多问题和陷阱,如,
为什么 va_start 宏函数不愿接受 char或short 类型的参数变量?
为什么 va_arg 宏函数不可接受 char或short 等类型名?
实践过 int、long是可以做变参类型的,那么, float、double 可以做变参类型吗?
为什么 va_arg 宏函数可以正确解析 int 类型,却不肯接受 float 类型?
实践过结构体指针类型做变参类型,那么,结构体对象类型可做变参类型吗?
…
@History
上述问题已埋在CSDN草稿中太久太久。
@关联
@《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?》
@关联
@《语言基础 /C&C++ 可变参函数设计与实践,变参函数的实现、使用、替代方法》
转载请标明原文链接,
https://blog.csdn.net/quguanxin/category_6223029.html
va_ 系列定义
在 C/C++ 语言中,提供了一组以 va_ 为前缀的宏和类型定义,用以在函数内部迭代访问可变数量的参数,可称它们为 “va_list系列” ,它们的定义位于stdarg.h头文件中。一个简短的介绍如下:
va_list类型:
va_list 是一个用于存储可变参数信息的类型,通常是一个指向内部结构的指针,在可变参数函数中使用va_list类型的变量来迭代访问参数列表。
va_start宏:
va_start 宏用于初始化 va_list 类型的变量,以开始对参数列表的访问。它接受两个参数,第一个是 va_list 类型的变量,第二个是可变参数函数中最后一个具名参数的名称,它会根据具名参数的位置和大小来计算参数列表的起始位置。
va_arg宏:
va_arg 宏用于获取参数列表中的下一个参数值。它接受两个参数,第一个是 va_list 类型的变量,第二个是要获取的参数的类型,它会返回指定类型的下一个参数值,并将 va_list 变量 ap 更新为指向下一个参数的位置。
va_end宏:
va_end 宏用于结束对参数列表的访问。它接受一个va_list 类型的变量,它会执行一些清理操作,以确保参数列表的访问结束。
早些年定义和实现可变参函数时,使用 mingW 编译环境,
如上,并不能进行更深层次的源码跳转,更没去研究过mingW工具集相关组件的源码。而在 MSVC 下,可查看详细定义,
在 Microsoft Visual Studio 14.0\VC\include\stdarg.h 其顶层定义为,
相关的_crt_va_ 宏可进一步跳转代码到 vadefs.h 查看详细定义,
-/-/-
-/-/-
如上,除了 _M_X64 模式下的 va_start 宏定义,不能查阅全源码,其他情况下的定义,算是完整的。后文将逐一展开讲解。
va_list 类型
在可变参函数实践过程中,已经认识到,类型 va_list 定义如下,
如上,分别是在VS2017、MingW7.3、百科给出的 va_list 类型定义,它们定义在各自版本的 vadefs.h 这个C语言标准库头文件中。故,在大部分情况下,va_list 类型就是一个char指针类型,这是下文中理解一些现象的关键因素。
还要纠正之前的一个错误,
int __CRTDECL printf(char const* const format, ...);
int __CRTDECL vprintf(const char* format, va_list argptr);
函数 printf 是变参函数,但是 vprintf 不是变参函数,因为 va_list 是一个具体的类型,argptr 是具名参数。
va_start 宏
在变参函数的实践中,最早遇到的问题是 va_start 相关的告警,本节便从它开始谈起。先了解强制参数的定义,再细细研究为什么强制参数是 char 或 short 类型时,会有编译警告。
从变参函数的强制参数谈起
在代表变参列表的语法符号… 前,可以存在多个确定的参数,其中,紧挨着…的那个确定参数,也即最后一个确定(具名)参数,一般称为变参函数的强制参数,它是必须存在的,或者说是至少要存在的。在《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?YES》 文中,我们实际验证了 va_start 宏函数是不接收 NULL参数的,至少在QtCreator+MSVC环境下那会导致编译器崩溃,也证明了把 ‘变参数个数’ 作为变参函数强制参数是不错的选择。
void print_Integers2(int param_count, ...) {
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
int value = va_arg(argptr, int);
//do something .., or save first..
qDebug("test2_Param%d:%d ", i+1, value);
}
va_end(argptr);
}
宏 va_start 对 char 和 short 类型编译告警
宏函数 va_start 不仅不接受 null,它也不愿意接受short 和 char 类型的参数。以前的草稿记录中有写下过:char和short等类型不可以作为可变参函数的强制参数类型,注意不是说指针类型哈。这种说法倒是没有什么很大的错误,只是不太确切,我们应该更加直接将此类问题安置在 va_start 和 va_arg 宏函数本身上来继续讨论。
最早的相关记录,是在某嵌入式项目中,Keil下C编程,遇到编译告警如下,
其他类似告警如下,只有简单记录,已找不到出处,未再设法复现,
//warning: #1256-D: "MR_U8" would have been promoted to "int" when passed through the ellipsis parameter; use the latter type instead
还有一次类似的记录,当时没写IDE类型,后来在 QtCreator+MSVC2015 下复现,如下,
va_start(argptr, count);
//warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior
//note: parameter of type 'short' is declared here`
要说明的一点是,虽然上述函数在 va_start 处存在编译告警,但上述函数是运行正常的。在VS集成开发环境下,同样测试上述 print_Integers3 函数实现,没有告警。
再后来,在一些机缘巧合之下,我发现:QtCreator + MSVC2015 集成开发环境下,va_start 告警提示,以及下文中提到的 va_arg 参数类型告警提示、对 va_arg 取地址操作时的错误提示,竟然是可以控制打开和关闭的。在开启 QtCreator 插件配置 ClangCodeModel 的情况下,通过 Clang 的检测机制,上述异常会报告,否则,没有任何提示。虽然上述告警并不会产生实质性的错误,但我还是深追了下来,毕竟前期被它吊了很久。
宏 va_start 源码分析
宏函数 va_start 可以接受 int、long 类型的参数,但却不太愿接受 unsigned char、char、short类型,这是为啥?所以,总想着找点理论上的支撑。接下来的文章中,我们将在QtCreator + MSVC2015_64 环境下,完成相关探究过程。
一开始我的研究方向有点跑偏,
由于上文告警中的,default argument promotion,其含义是默认参数提升。它是C语言中的一种隐式类型转换规则,用于将函数调用中的较小的整数类型和浮点类型参数提升为较大的整数类型和浮点类型。在《C语言程序设计》第2版 2.7 类型转换一节中,有提到,
但总觉得有些说不通,
以上文 print_Integers2(short param_count, …) 函数为例,这里的 param_count 是一个具参数,它不属于变参列表,它是明确声明的,这里也没有多元操作符,也不是什么无函数原型的情况,它怎么能被提升呢?
后来,得知,
根据C语言标准,可变参数的传递方式要求参数按照一定的字节对齐方式进行压栈。这种对齐方式要求参数的大小必须是特定字节的倍数。对于 va_start 宏的第二个参数,该具名参数的类型决定了可变参数列表的起始位置和对齐方式。由于 char和short类型的大小不能直接满足对齐要求,因此会告警。
再后来,
透过 _M_IX86 下 __crt_va_start_a 中使用的 _INTSIZEOF(n) 定义,也可以证明了上述猜测,
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
计算过程1,sizeof(int) - 1:
这样做是为了确保后续的对齐操作不会超过 int 类型的大小。
计算过程2,(sizeof(n) + sizeof(int) - 1):
在上一过程基础上,得到一个比类型 n 大的值。
计算过程3,~(sizeof(int) - 1):
用于生成一个掩码,将最后 sizeof(int) 个比特位设置为 1,其余比特位设置为 0。
((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)):
将过程2和过程3的结果进行按位与操作,得到 sizeof(n) 的倍数并对齐到 int 类型的边界。
最后,
这里倒是可以借用上文《C语言程序设计》中的那句话。对于可变参数函数中的强制参数(即形参类表中最后一个确定的参数),即使你的实参为char或short等,也请把它声明为int或long类型。这可能是个好习惯。
另外要注意的是,在C语言中,va_start 宏并不会直接申请可变参数列表的空间,当然 va_arg 也不会。宏 va_start 的主要作用是初始化一个 va_list 类型的变量,以便在函数内部访问可变参数。实际上可变参数列表的空间是由编译器自动管理的,在函数调用时,编译器会根据函数原型中的参数信息和函数调用时提供的实际参数,来为可变参数列表分配合适的内存空间。
猜测 __va_start 函数实现
//大河qu // vadefs.h
#define __crt_va_start(ap, x) ((void)(__vcrt_va_start_verify_argument_type<decltype(x)>(), __crt_va_start_a(ap, x)))
//其中__vcrt_va_start_verify_argument_type其内容是静态断言,旨在强制"va_start参数不能具有引用类型,也不能用括号括起来"
//大河qu
#elif defined _M_X64
void __cdecl __va_start(va_list* , ...);
#define __crt_va_start_a(ap, x) ((void)(__va_start(&ap, x)))
...
前文提到过,在 _M_X64 模式下的 va_start 宏定义,不能查阅全源码,那么,就来猜测下它的实现过程。(纯属以前的自己闲的蛋疼)
void __cdecl __va_start(va_list* ap, ...) {
// 获取可变参数函数中最后一个具名参数的地址
void* last_arg = ≈
// 根据具名参数的地址计算可变参数列表的起始地址
*ap = (va_list)last_arg + sizeof(void*);
// 将起始地址按照平台相关的规则对齐 //如8字节
*ap = (va_list)(((uintptr_t)(*ap) + 7) & ~7);
}
为此还设计了一个测试用例,
void print_Integers7(char param_count, /*real param is int */...) {
qDebug("test_int7_forcedparam_addr:0x%.16llx, value:%d ", (int64_t)¶m_count, param_count);
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
int *pvalue = &va_arg(argptr, int);
qDebug("test_int7_param%d_addr:0x%.16llx, value:%d ", i+1, (int64_t)pvalue, *pvalue);
}
va_end(argptr);
}
如上结果显示,即使传递了 char 类型给 va_start 宏,变参函数的地址也会被设置为8字节对齐。这里还有个插曲:在 QtCreator 软件,开启 ClangCodeModel 插件选项时, &va_arg(argptr, int) 操作并标识为错误代码,提示,
类似异常的分析,已经记录在其他文章中,此处不再讨论。只了解到,某些编译器可能提供一些扩展或非标准功能,允许对右值进行取地址操作,如未开启 ClangCodeModel 的QtCreator,如 未特殊配置过的 Visual Studio…
va_arg 宏
前文我们已经贴出了,在 MSVC 2015 下的完整的 va_arg 宏函数定义。对 va_arg 宏的研究始于,那段 “想方设法不不给变参函数传递参数个数信息” 的尴尬历程,再后来,读懂了 va_arg 在 MSVC 源码定义,一切才迎刃而解。
宏 va_arg 无法接受 char 和 short
在Keil中,
在 QtCreator + MSVC2015 中,
int value = va_arg(argptr, short);
//warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
//stdarg.h:35:50: note: expanded from macro 'va_arg'
与 va_start 差不多,va_arg 宏函数也不待见 char 和 short 类型。不同的是,前者针对这种情况,会执行默认提升,不会造成大问题,而后者,诚不欺你,发生了无法预计的行为,可能会在 va_arg 执行时奔溃掉。所以还是那个建议,请直接使用 int 或 long…
为啥va_arg可解析int却不能解析float类型?
首先验证,va_arg 是否可以解析浮点数,float 类型 或 double 类型,
//执行失败/结果不符合预期
void print_float1(int param_count, /*real param is float*/...) {
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
float value = va_arg(argptr, float); //note float
qDebug("test_float1_param%d:%2.3f ", i+1, value);
}
va_end(argptr);
}
//执行成功/结果符合预期
void print_float2(int param_count, /*real param is float*/...) {
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
double value = va_arg(argptr, double); //note double
qDebug("test_float2_param%d:%2.3f ", i+1, value);
}
va_end(argptr);
}
int main() {
//8, 4, 4, 8
qDebug("sizeof(__int64):%d, sizeof(int):%d, sizeof(float):%d, sizeof(double):%d", sizeof(__int64), sizeof(int), sizeof(float), sizeof(double));
//
print_float1(3, 1.234, 1.345, 1.567);
//
print_float2(3, 2.234, 2.345, 2.567);
...
}
如上,使用 va_arg 解析 float 类型的操作,失败了。那么,就产生了一个新问题,结合在 x64下 va_arg 定义,无论是int或float类型,它们都是4字节,实际运行中,其都将执行定义中的第二个分支。但,va_arg 可以正确解析 int 类型,却不能正确解析 float 类型,这是什么鬼?
//__crt_va_arg(ap, t) //in defined _M_X64
*(t* )((ap += sizeof(__int64)) - sizeof(__int64)))
结合上文,我们设计并调试查看 va_arg 宏返回值 value 的地址信息,
//
void print_Integers7(int param_count, /*real param is int */...) {
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
int *pvalue = &va_arg(argptr, int);
qDebug("test_int7_param%d_addr:0x%.16llx, value:%d ", i+1, (int64_t)pvalue, *pvalue);
}
va_end(argptr);
}
//
void print_float7(int param_count, /*real param is float*/...) {
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
float *pvalue = &va_arg(argptr, float);
qDebug("test_float7_param%d_addr:0x%.16llx, value:%2.3f ", i+1, (int64_t)pvalue, *pvalue);
}
va_end(argptr);
}
再细细品鉴 va_arg 的源码定义,无论是int还是float类型,指针偏移都按照8字节的sizeof(int64)来执行,也可以想到,int和float数据类型在调用变参函数的那一刻,已经分别被默认的提升为 int64和double类型。再回到float为啥不行的问题上,其实这是一个常规错误,常见于新手代码。即"指针类型强转并解引用"非常危险。函数 print_float7 实际上执行了,用 float* 类型 去强转 double* 类型,然后在接下来的解引用操作中,用float去解析double的内存,这肯定是错误的。一个干净的例子如下,
int main() {
//
double dvalue = 1.2345;
float fResult = *((float*)&dvalue);
//float value from double by float pointer:0.00000
qDebug("float value from double by float pointer:%2.5f ", fResult);
int64_t i64value = 12345;
int iResult = *((int*)&i64value);
//int value from int64 by int pointer:12345
qDebug("int value from int64 by int pointer:%d ", iResult);
return 0;
}
解引用操作具有一定的风险,如果解引用的指针无效或未初始化,或者指针指向的内存位置不符合指针类型的要求,那么解引用操作可能会导致未定义的行为。在 IEEE 754 标准中,double 类型的内存布局按照以下顺序排列:
而单精度浮点数float,按照上述布局,分别占用1-8-23bit位。由于 float和double 数据类型的内存构造的上述差异,按照float类型去解引用一个实际存储double数值的内存地址,自然是不符合规则的。对于一个正整数,int指针强转int64指针类型,由于整数数据的内存构造,在数值32bit大小以内的情况下,碰巧不会有问题,但这也是应该被禁止的操作。
宏 va_arg 源码解析
宏 va_arg 的主要作用是使得我们能够在函数中按照参数的类型和顺序获取变长参数列表中的参数值,从而实现对可变数量参数的处理。下文枚举了 _M_IX86 和 _M_X64 两种模式下的 va_arg 定义,前者容易理解,后者可能要费点小心思。
_M_IX86
//Microsoft Visual Studio 14.0\VC\include\vadefs.h
#elif defined _M_IX86
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
理解它的关键是,+=操作是对ap自身进行修改的,而减法操作是不会的。故,+=操作的目的是为了改变ap指针,使得其指向下一个参数,而(变更后的)ap 减 _INTSIZEOF(t) 是为了访问当前参数的指针。
(ap += _INTSIZEOF(t)):
ap 指针会根据参数类型的大小 _INTSIZEOF(t) 进行偏移,以便指向下一个参数的内存位置。
(ap - _INTSIZEOF(t)):
ap 指针会减去参数类型 t 的大小,以便回到当前参数的内存位置。如果你没有理解+=会改变自身的特性,那么这可能看上去很诡异。因为ap 已经被 va_start 初始化过,已经指向了第一个变参的地址,因此下一步直接解引用就可以得到第一个参数,但是大神程序员为了少写一行递增指针的代码…
((t)((ap - _INTSIZEOF(t))):
通过将指针转换为指定类型 t 的指针,并通过解引用操作 * 来获取参数的值。
_M_X64
#elif defined _M_X64
#define __crt_va_arg(ap, t) \
((sizeof(t) > sizeof(__int64) || (sizeof(t) & (sizeof(t) - 1)) != 0) \
? **(t**)((ap += sizeof(__int64)) - sizeof(__int64)) \
: *(t* )((ap += sizeof(__int64)) - sizeof(__int64)))
...
条件 (sizeof(t) & (sizeof(t) - 1)) != 0 的含义是检查参数类型 t 的大小是否是2的幂次方。
补充,
在计算机中,2的幂次方的二进制表示形式是只有一个位为1,其他位都为0。因此,如果 sizeof(t) 是2的幂次方,那么 sizeof(t) - 1 的二进制表示形式将会是所有位都为1。而按位与运算符 & 将两个操作数的对应位进行逻辑与操作,只有在对应的位都为1时,结果位才为1。
因此,如果 (sizeof(t) & (sizeof(t) - 1)) 的结果等于0,那么说明 sizeof(t) 是2的幂次方,即参数类型 t 的大小是2的幂次方。
回到源码分析上,
(sizeof(t) > sizeof(__int64) || (sizeof(t) & (sizeof(t) - 1)) != 0
不等于 0,即表示为 true。即条件1或条件2,其中之一为true即可。
也就是说,如果参数类型的大小大于 sizeof(__int64) 或者 参数类型的大小不是2的幂次方值,也即不是1、2、4、8 等2的幂次方值,不是char、short、int、long 等,如一些单字节对齐的结构体。此时执行分支1。
否则,也就是,参数类型的大小 <= sizeof(__int64) 且大小是2的幂次方值,也即是 char、short、int、long 等单数据类型。将执行第2个条件分支。
对于第一个分支,其使用双指针将 ap 指针转换为指向指针类型 t** 的指针,然后通过两次解引用操作 ** 来获取指针指向的值,即参数的值,其应该是主要针对结构体类型的。对于非结构体类型,包含指针类型,统一的以 sizeof(__int64) 字节长度来处理。结构体类型的参数可能具有不同的大小和内存布局,因此需要特殊处理,相当于是把一个不确定的长度转换成了一个确定的长度,否则没有信息来执行地址的偏移。通过对指针进行一次解引用操作 ,我们得到指向 t* 类型的指针,以此可访问结构体参数的地址。再次对指针进行解引用操作 ,可以获取指针指向的值,即结构体类型的参数。
其他特别需要注意的是,
实践和理论上都可表明,va_arg 并不能按照预期识别出实参列表的结尾。这也是《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?YES》 重点要表达的。
变参数类型: sizeof(结构体) > sizeof(_int64)
基于对 va_arg 宏函数的理解,结构体对象参数理论上是可以被变参函数解析过程 va_arg 接受的,至少在 x64 平台下是有可能的。先在 QtCreator + MSVC2015_64 环境下测试,
//测试1
typedef struct tagParamA {
short iSeg1;
short iSeg2;
} TParamA;
//测试2
typedef struct tagParamB {
short iSeg1;
short iSeg2;
int a;
} TParamB;
//结构体尺寸必须要大于U64
typedef struct tagParamC {
short iSeg1;
short iSeg2;
int a;
short b;
} TParamC;
//
void TestStructParamaB(int param_count, /* param is TParamA/B */...) {
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
TParamB value = va_arg(argptr, TParamB); //note TParamB
qDebug("test_struct_param%d:(%d,%d,%d,%d) ", i+1, value.iSeg1, value.iSeg2, value.a);
}
va_end(argptr);
}
//
void TestStructParamaC(int param_count, /* param is TParamC */...) {
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
TParamC value = va_arg(argptr, TParamC); //note TParamC
qDebug("test_struct_param%d:(%d,%d,%d,%d) ", i+1, value.iSeg1, value.iSeg2, value.a, value.b);
}
va_end(argptr);
}
int main() {
//sizeof(TParamA)---4, sizeof(TParamB)---8, sizeof(TParamC) ---12
//
//TParamB tArray[10] = {{100, 101, 102}, {200, 201, 202}, 0};
//TestStructParamaB(2, &tArray[0], &tArray[1]);
//
TParamC tArray[10] = {{100, 101, 102, 103}, {200, 201, 202, 203}, 0};
TestStructParamaC(2, &tArray[0], &tArray[1]);
/*system("pause");*/ return 0;
}
Test B
Test C
如上在X64平台下,经过测试 TParamA 和 TParamB 对象直接作为变参函数的解析类型和实参类型时,变参函数不能输出正确的结果,只有当结构体的尺寸大小大于 sizeof(__int64) 字节后,相关函数实现和运行过程才是正确的。
将同样的程序配置成x86平台,编译运行(VS 2017 x86配置),
如上,在Windows X86平台配置下,结构体对象是不能直接被 va_arg 解析的。
以 TParamA/B为例,其实现函数中,执行的是 __crt_va_arg 宏定义的第二个分支,代入,
*(TParamA *)((ap += 8) - 8)) //这个错误很好理解
*(TParamB *)((ap += 8) - 8)) //这个我没有理解透彻
TParamA的情况下,会导致指针偏移错误,进而解析混乱,这并不难理解。但是 TParamB 的测试结果,我并没有理解清楚,不明白它为啥会输出错误结果。
//VS2017 X64
void TestStructParamaC(int param_count, /* param is TParamC */...)
{
printf("test_struct_fixparam:(Addr:0x%.16llx)\r\n ", (int64_t)(¶m_count));
va_list argptr;
va_start(argptr, param_count);
for (int i = 0; i < param_count; i++) {
TParamC *pValue = &va_arg(argptr, TParamC); //note TParamC
printf("test_struct_param%d(Addr:0x%.16llx, &Addr:0x%.16llx) \r\n", i + 1, (int64_t)pValue, (int64_t)(&pValue));
printf("test_struct_param%d:(%d,%d,%d,%d) \r\n", i + 1, pValue->iSeg1, pValue->iSeg2, pValue->a, pValue->b);
}
va_end(argptr);
}
通过如上结果,可分析得到,当使用结构体对象直接做可变参数时,其初始地址并不是具名参数偏移sizeof(_int64),这应该与 __va_start(va_list* ap, …) 函数的实际行为有关。我没有多余时间继续研究下去,不过至此,我们最初的目的都已经达到,就这样吧。
va_end 宏
#define __crt_va_end(ap) ((void)(ap = (va_list)0))
略。
小结
先这样吧!不总结了。有问题请留言。