C/C++语言有一个不同于其它语言的特性,即其支持可变参数,典型的函数如printf、scanf等可以接受数量不定的参数。如:
printf ( "I love you" );
printf ( "%d", a );
printf ( "%d,%d", a, b );
第一、二、三个printf分别接受1、2、3个参数,printf函数的原型:
int printf ( const char *format, ... ); 想必大家都注意到了后面的...参数,这个就是不定参数了。
从函数原型可以看出,其除了接收一个固定的参数format以外,后面的参数用"…"表示。在C/C++语言中,"…"表示可以接受不定数量的参数,理论上来讲,可以是0或0以上的n个参数。
本文,潇汀为大家讲解一些C/C++不定参数的一些相关知识。
C/C++不定参数实现原理
函数参数传递的时候,参数是线性的存储在内存中的,因此,如果知道参数存放的起始位置和结束位置,和参数的类型,那么就可以得到需要的所有参数.关于不定参数头文件stdarg.h中的几个宏定义(每一个颜色板块为一个宏定义及其解释):
va_list:
#ifdef _M_ALPHA
typedef struct {
char *a0; /* pointer to first homed integer argument */
int offset; /* byte offset of next parameter */
} va_list;
#else
typedef char * va_list;
#endif
其中a0或者va_list是第一个参数的地址,offset是从第一个参数到第二个参数的内存地址偏移量,由参数的类型占用内存的空间决定.
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
得到参数n的尺寸,相当与sizeof(n),为了兼容性写成了如上的表达式.
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
其中ap是一个va_list型的参数列表指针变量,v是参数表中的第一个参数的名字.作用是初始化参数表,并让参数表指针ap指向参数列表的第二个参数开始地址(根据以上宏定义的表达式得知).
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
根据参数类型移动参数表指针ap,使之返回下一个参数的值,参数的类型由第二个参数t指定(分析以上宏定义表达式).
#define va_end(ap) ( ap = (va_list)0 )
结束参数传递,做清理工作,即让参数表指针清空.
======================================================================================================
我们来看下C++标准库里的文件宣言
stdarg.h
#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end
vadefs.h
#if defined(_M_CEE)
extern void __cdecl __va_start(va_list*, ...);
extern void * __cdecl __va_arg(va_list*, ...);
extern void __cdecl __va_end(va_list*);
#define _crt_va_start(ap,v) ( __va_start(&ap, _ADDRESSOF(v), _SLOTSIZEOF(v), \
__alignof(v), _ADDRESSOF(v)) )
#define _crt_va_arg(ap,t) ( *(t *)__va_arg(&ap, _SLOTSIZEOF(t), \
_APALIGN(t,ap), (t *)0) )
#define _crt_va_end(ap) ( __va_end(&ap) )
#elif defined(_M_IX86)
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
#elif defined(_M_IA64)
#ifdef __cplusplus
extern void __cdecl __va_start(va_list*, ...);
#define _crt_va_start(ap,v) ( __va_start(&ap, _ADDRESSOF(v), _SLOTSIZEOF(v), \
_ADDRESSOF(v)) )
#else
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _SLOTSIZEOF(v) )
#endif
#define _crt_va_arg(ap,t) (*(t *)((ap += _SLOTSIZEOF(t)+ _APALIGN(t,ap)) \
-_SLOTSIZEOF(t)))
#define _crt_va_end(ap) ( ap = (va_list)0 )
#elif defined(_M_AMD64)
extern void __cdecl __va_start(va_list *, ...);
#define _crt_va_start(ap, x) ( __va_start(&ap, x) )
#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) ) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
#else
/* A guess at the proper definitions for other platforms */
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
#endif
其中有篇文章写得也很不错,摘录下
==============================================================================================================================
http://blog.youkuaiyun.com/doudou745/article/details/7237972
不定参数当年做为C/C++语言一个特长被很多人推崇,但是实际上这种技术并没有应用很多。除了格式化输出之外,我实在没看到多少应用。主要原因是这种技术比较麻烦,副作用也比较多,而一般情况下重载函数也足以替换它。尽管如此,既然大家对它比较感兴趣,我就简单总结一下它的使用和需要注意的常见问题。
原理
刚学C语言的时候,一般人都会首先接触printf函数。通过这个函数,你可以打印不定个数的变量到屏幕,如:
printf("%d", 3);
printf("%d,%d",3,4);
上述代码看似简单,实际上却需要我们解决许多问题。在我们设计printf的时候,我们是不知道到底会传入几个参数的。在这种未知的情况下,我们需要解决下面几个问题:
- 怎么告诉printf我们会传入几个参数
- printf怎么去访问这些参数
- 函数调用完成后,系统怎么把参数从传递用的堆栈中释放
为了解决这些问题,我们首先要解释cdecl调用约定(参见论调用约定),所有使用不定参数的函数必须是使用cdecl(全局函数)或者this call(类成员函数)调用约定。该约定对于参数传递规定如下:
- 参数从右向左入栈(也就是如果你调用f(a,b,c),则c先入栈,然后是b,最后是a入栈)
- 调用者负责清理堆栈
其中第二点直接解决了前面三个问题中的第三个问题。我们来详细说说其他两个问题。
确定参数的个数
在一个函数中,一般有如下prolog代码:
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,48h
执行上述代码之后,func(a,b,c)函数所处的堆栈上下文就变成如下布局:
其中,ebp指向保存旧的ebp的堆栈内存的下一个字的地址,ebp+8指向eip地址,ebp+12则指向函数调用的第一个参数,而ebp和esp之间是用于临时变量(也就是堆栈变量)的空间。
注意,由于上述prolog代码的存在,我们很容易通过ebp得到第一个参数的地址,对于不定参数列表之前的类型固定的参数,我们也可以根据类型信息得到其实际的位置(例如,第一个参数的位置偏移第一个参数的大小,就是第二个参数的地址)。
注意不定参数函数有个限制,就是不定参数的列表必须在整个函数的参数列表的最后。我们不可以定义如下的函数:
void func(int a, ..., int c)
所有类型固定的参数都必须出现在参数列表的开始。这样根据前面的论述,我们就可以得到所有类型固定的参数。
在设计具有不定参数列表的函数的时候,我们有两种方法来确定到底多少参数会被传递进来。
方法1是在类型固定的参数中指明后面有多少个参数以及他们的类型。printf就是采用的这种方法,它的format参数指明后面每个参数的类型。
方法2是指定一个结束参数。这种情况一般是不定参数拥有同样的类型,我们可以指定一个特定的值来表示参数列表结束。下面这个sum函数就是一个例子:














使用这个函数的代码为:







访问各个参数
其实前文已经告诉我们怎么去访问不定参数。va_start和va_arg函数可以被结合起来用于依次访问每个函数,他们实际上都是宏函数。
在vc6,va_start函数定义为:


其中_INTSIZEOF(n)计算比n大的sizeof(int)的最小倍数,如果n=101,则_INTSIZEOF(n)为104。
va_start执行完毕后,ap指向变量v后第一个4字节对齐的地址。例如,v的地址为0x123456, v的大小为13,则v后面的下一个与字边界对齐的地址为0x123456+0x0D=0x123463再调整为与4字节对齐的下一个地址,也就是0x123464.
va_arg函数定义为:

分析与va_start一样,它的结果是使ap指向当前变量的下一个变量。
这样,我们只要在开始时使用va_start把不定参数列表赋值给ap,然后依次用va_arg获得不同参数即可。
潜在问题
使用不定参数列表,有两个问题特别需要注意。
问题1的理解相对简单:我们在重载一个函数的时候,不能依赖不定参数列表部分对函数进行区分。
假定我们定义两个重载函数如下:
int func(int a, int b, ...)
int func(int a, int b, float c);
则上述函数会导致编译器不知道怎么去解释func(1,2, 3.3),因为当第三个参数为浮点数时,两个实现都可以满足匹配要求。一般情况,个人建议对于不定参数函数不要去做重载。
另外一个问题是关于类型问题。绝大多数情况下,C和C++的变量都是强类型的,而不定参数列表属于一个特例。
当我们调用va_arg的时候,我们指明下一个参数的类型,而在执行的时候,va_arg正是根据这个信息在堆栈上来找到对应的参数的。如果我们需要的类型和真实传递进来的参数完全一致时自然没有问题,但是假如类型不一样,则会有大麻烦。
假如上面的的sumi函数,我们用下面方法调用:

注意第二个参数我们传入了一个double类型的2.2,我们希望sumi在做加法时可以做隐式类型转换,转换为int进行计算。但是实际情况时,当我们分析到这个参数时,调用的是:

根据前文va_arg的定义,这个宏被翻译成:

如果后面的+=计算出正确的地址,最后就变成
如果希望能得到正确的整数值,必须要求addr所在的地址是一个真实的int类型。但是当我们传入double时,实际上其内存布局和int完全不同,因此我们得不到需要的整数。感兴趣的朋友可以用下面简单的代码做测试:



因此,当我们调用有不定参数列表的函数时,不要期望系统做隐式类型转换,系统不会做这种检查或者转换,你给的参数类型必须严格和你希望的值一样。
==============================================================================================================================
明白以上内容了,我们就很容易用C/C++不定参数实现LOG记录了。
新建一个Class AL_Log, 将其中构造,析构函数私有化(没必要创建这个对象,仅作为接口使用),声音一个静态方法Output
/**
* Output log information
*
* I want to output the log information.
*
* @ Param DWORD dwLogKind: <br> information type [IN] log
* @ Param const CHAR * pkszInfoFormat: (200 characters including format control string, alphanumeric characters maximum (NULL)) info [IN] log
* @ Param ...: arguments [IN] option
* @ Return None
* @ Par log on information type:
* For information on log types that can be set to the type of the argument, <br>
* Please refer to the type of log information AL_LogPublicDef.h. <br>
* If you are adding a new type of log information, you must apply. <br>
* @ Attention
* A string that can be specified in the log information is up to 200 alphanumeric characters, including the NULL. <br>
* If more than 200 characters in the log information is not part of the output later. <br>
* I do not need to add line breaks because the log information internally. <br>
* @ Code
* / / Output log information (vehicle information)
* AL_Log :: Output (AL_LOG_KIND_LOC, "[% s] X [% 05d] Y [% 05d]", "LOC", dwXPos, dwYPos);
*
* Sample output)
* [2005/04/04 08:20:00.000] 00003: LOC: [LOC] X [00003] Y [00006]
* @ Endcode
*/
static VOID Output(AL_LogKind eLogKind, const NCHAR *pkszInfoFormat, ...);
/**
* Output log information
*
* I want to output the log information.
*
* @ Param DWORD dwLogKind: <br> information type [IN] log
* @ Param const CHAR * pkszInfoFormat: (200 characters including format control string, alphanumeric characters maximum (NULL)) info [IN] log
* @ Param ...: arguments [IN] option
* @ Return None
* @ Par log on information type:
* For information on log types that can be set to the type of the argument, <br>
* Please refer to the type of log information AL_LogPublicDef.h. <br>
* If you are adding a new type of log information, you must apply. <br>
* @ Attention
* A string that can be specified in the log information is up to 200 alphanumeric characters, including the NULL. <br>
* If more than 200 characters in the log information is not part of the output later. <br>
* I do not need to add line breaks because the log information internally. <br>
* @ Code
* / / Output log information (vehicle information)
* AL_Log :: Output (AL_LOG_KIND_LOC, "[% s] X [% 05d] Y [% 05d]", "LOC", dwXPos, dwYPos);
*
* Sample output)
* [2005/04/04 08:20:00.000] 00003: LOC: [LOC] X [00003] Y [00006]
* @ Endcode
*/
VOID
AL_Log::Output(AL_LogKind eLogKind, const NCHAR *pkszInfoFormat, ...)
{
// max log info siz 200
NCHAR szLogInfo[200];
memset(szLogInfo,0, sizeof(szLogInfo));
va_list list;
va_start(list, pkszInfoFormat);
WORD iRes = NCHARvsnprintf((szLogInfo),
200, pkszInfoFormat, list);
va_end(list);
}
使用如下:
int _tmain(int argc, _TCHAR* argv[])
{
AL_Log::Output(NTEXT("[xiaoting] test %x, %d, %s"), 100, 98, NTEXT("hello world!"))
return 0;
}

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
以后的笔记潇汀会尽量详细讲解一些相关知识的,希望大家继续关注我的博客。
本节笔记到这里就结束了。
潇汀一有时间就会把自己的学习心得,觉得比较好的知识点写出来和大家一起分享。
编程开发的路很长很长,非常希望能和大家一起交流,共同学习,共同进步。
如果文章中有什么疏漏的地方,也请大家指正。也希望大家可以多留言来和我探讨编程相关的问题。
最后,谢谢你们一直的支持~~~