1、当你的函数的参数个数不确定时,就可以使用上述宏进行动态处理,这无疑为你的程序增加了灵活性。Example:CString AppendString(CString str1,.)/一个连接字符串的函数,参数个数可以动态变化LPCTSTR str=str1;/str 需为指针类型,因为 va_arg 宏返回的是你的参数的指针,但是如果你的参数为 int 等简单类型,则不必为指针,因为变量名实际上即是指针。CString res;va_list marker; /你的类型链表va_start(marker,str1);/初始化你的 marker 链表while(str!=“ListEnd“)/Li
2、stEnd:参数的结束标志,十分重要,在实际中需自行指定res+=str;str=va_arg(marker,CString);/取得下一个指针va_end(marker);/结束,与 va_start 合用return res;int main()CString str=AppendString(“xu“,“zhi“,“hong“,“ListEnd“);coutstr.GetBuffer(str.GetLength()endl;return 0;输出 xuzhihongCString AppendString(CString str1,.),因为连接字符串的参数可以动态变化,你不知用户要进行
3、连接的字符串个数是多少,所以你可以用来代替。但是要注意的是你的函数要有一个参数作为标志来表示结束,否则会出错。在上例中用 ListEnd 作为结束符。还有 va_arg 返回的是你参数内容的指针。上例在支持 MFC 程序的 console 下运行通过。可变参数函数的原型声明格式为:type VAFunction(type arg1, type arg2, );参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用“表示。固定参数和可选参数公同构成一个函数的参数列表。借助上面这个简单的例 2,来看看各个
4、 va_xxx 的作用。va_list arg_ptr:定义一个指向个数可变的参数列表指针;va_start(arg_ptr, argN):使参数列表指针 arg_ptr 指向函数参数列表中的第一个可选参数,说明:argN 是位于第一个可选参数之前的固定参数,(或者说,最后一个 固定参数;之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。如果有一 va 函数的声明是 void va_test(char a, char b, char c, ),则它的固定参数依次是 a,b,c,最后一个固定参数 argN 为 c,因此就是 va_start(arg_ptr, c)。v
5、a_arg(arg_ptr, type):返回参数列表中指针 arg_ptr 所指的参数,返回类型为 type,并使指针 arg_ptr 指向参数列表中下一个参数。va_copy(dest, src):dest,src 的类型都是 va_list,va_copy()用于复制参数列表指针,将 dest 初始化为 src。va_end(arg_ptr):清空参数列表,并置参数指针 arg_ptr 无效。说明:指针arg_ptr 被置无效后,可以通过调用 va_start ()、va_copy()恢复 arg_ptr。每次调用 va_start() / va_copy()后,必须得有相应的 va_e
6、nd()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在 va_start() va_end()之内。va 函数的实现就是对参数指针的使用和控制。typedef char * va_list; / x86 平台下 va_list 的定义函数的固定参数部分,可以直接从函数定义时的参数名获得;对于可选参数部分,先将指针指向第一个可选参数,然后依次后移指针,根据与结束标志的比较来判断是否已经获得全部参数。因此,va 函数中结束标志必须事先约定好,否则,指针会指向无效的内存地址,导致出错。这里,移动指针使其指向下一个参数,那么移动指针时的偏移量是多少呢,没有具体答案,因为这里涉及到内存对齐(
7、alignment)问题,内存对齐跟具体 使用的硬件平台有密切关系,比如大家熟知的 32 位 x86 平台规定所有的变量地址必须是 4 的倍数(sizeof(int) = 4)。va 机制中用宏_INTSIZEOF(n)来解决这个问题,没有这些宏,va 的可移植性无从谈起。首先介绍宏_INTSIZEOF(n),它求出变量占用内存空间的大小,是 va 的实现的基础。#define _INTSIZEOF(n) (sizeof(n)+sizeof(int)-1) 其中 e 为结束标志。从上图中可以很清楚地看出 va_xxx 宏如此编写的原因。1 va_start。为了得到第一个可选参数的地址,我们有
8、三种办法可以做到:A) = &n3 + _INTSIZEOF(n3)/ 最后一个固定参数的地址 + 该参数占用内存的大小B) = &n2 + _INTSIZEOF(n3) + _INTSIZEOF(n2)/ 中间某个固定参数的地址 + 该参数之后所有固定参数占用的内存大小之和C) = &n1 + _INTSIZEOF(n3) + _INTSIZEOF(n2) + _INTSIZEOF(n1)/ 第一个固定参数的地址 + 所有固定参数占用的内存大小之和从编译器实现角度来看,方法 B),方法 C)为了求出地址,编译器还需知道有多少个固定参数,以及它们的大小,没有把问题分解到最简单,所以不是很聪明的
9、途 径,不予采纳;相对来说,方法 A)中运算的两个值则完全可以确定。va_start()正是采用 A)方法,接受最后一个固定参数。调用 va_start ()的结果总是使指针指向下一个参数的地址,并把它作为第一个可选参数。在含多个固定参数的函数中,调用 va_start()时,如果不是用最后一个固定 参数,对于编译器来说,可选参数的个数已经增加,将给程序带来一些意想不到的错误。(当然如果你认为自己对指针已经知根知底,游刃有余,那么,怎么用就随 你,你甚至可以用它完成一些很优秀(高效)的代码,但是,这样会大大降低代码的可读性。)注意:宏 va_start 是对参数的地址进行操作的,要求参数地址必
10、须是有效的。一些地址无效的类型不能当作固定参数类型。比如:寄存器类型,它的地址不是有效的内存地址值;数组和函数也不允许,他们的长度是个问题。因此,这些类型时不能作为 va 函数的参数的。2 va_arg 身兼二职:返回当前参数,并使参数指针指向下一个参数。初看 va_arg 宏定义很别扭,如果把它拆成两个语句,可以很清楚地看出它完成的两个职责。#define va_arg(ap,t) ( *(t *)(ap += _INTSIZEOF(t) - _INTSIZEOF(t) ) /下一个参数地址/ 将( *(t *)(ap += _INTSIZEOF(t) - _INTSIZEOF(t) )拆成
11、:/* 指针 ap 指向下一个参数的地址 */1 ap += _INTSIZEOF(t); / 当前,ap 已经指向下一个参数了/* ap 减去当前参数的大小得到当前参数的地址,再强制类型转换后返回它的值 */2 return *(t *)( ap - _INTSIZEOF(t) 回想到 printf/scanf 系列函数的%d %s 之类的格式化指令,我们不难理解这些它们的用途了- 明示参数强制转换的类型。(注:printf/scanf 没有使用 va_xxx 来实现,但原理是一致的。)3va_end 很简单,仅仅是把指针作废而已。#define va_end(ap) (ap = (va_l
12、ist)0) / x86 平台四、 简洁、灵活,也有危险从 va 的实现可以看出,指针的合理运用,把 C 语言简洁、灵活的特性表现得淋漓尽致,叫人不得不佩服 C 的强大和高效。不可否认的是,给编程人员太多自由空间必然使程序的安全性降低。va 中,为了得到所有传递给函数的参数,需要用 va_arg 依次遍历。其中存在两个隐患:1)如何确定参数的类型。va_arg 在类型检查方面与其说非常灵活,不如说是很不负责,因为是强制类型转换,va_arg 都把当前指针所指向的内容强制转换到指定类型;2)结束标志。如果没有结束标志的判断,va 将按默认类型依次返回内存中的内容,直到访问到非法内存而出错退出。例 2 中 SqSum()求的是自然数的平方 和,所以我把负数和 0 作为它的结束标志。例如 scanf 把接收到的回车符作为结束标志,大家熟知的 printf()对字符串的处理用0作为结束标 志,无法想象 C 中的字符串如果没有0, 代码将会是怎样一番情景,估计那时最流行的可能是字符数组,或者是 malloc/free。允许对内存的随意访问,会留给不怀好意者留下攻击的可能。当处理 cracker精心设计好的一串字符串后,程序将跳转到一些恶意代码区域执行,以使cracker 达到其攻击目的。(常见的 exploit 攻击)所以,必需禁止对内存的随意访问和严格控制内存访问边界。