一直以来习惯了使用printf函数,但是对于可变参数没有深入研究过,觉得可变参数是一个神奇的技术^0^。。。
工作闲下来的时候,想研究研究看可变参数的使用和原理。
目前C提供的可变参数的申明为
void function(const char *format, ...);
这样就可以在function中使用可变参数
C提供了几个宏用于使用可变参数
va_list
va_start
va_arg
va_end
其中
va_list用于定义一个变量获取可变参数指针
va_start用于将va_list定义的指针进行初始化
va_arg用于获取对应指针的真实类型数据
va_end用于清空va_list定义的指针
好了,光说不练假把式,来一个例子吧。嗯,什么样的例子比较好呢?
对了,在c的printf中不支持c++的std::string,就自己实现一个支持std::string的printf吧。
例子:
Code
void Puts(const char *pstr)
{
while(*pstr)
putchar(*pstr++);
}
// Printf函数支持可变类型为
// %c->char
// %d->int
// %s->char*
// %S->std::string
// Bug:不支持'"'等转义符
// 处理%%等出现问题
void Printf(const char *_Format, )
{
va_list arg_ptr;
va_start(arg_ptr,_Format);
const char *pWork = _Format;
while(*pWork != '"0')
{
if(pWork == _Format)
{
if(*pWork != '%')
putchar(*pWork);
}
else
{
if(*(pWork-1) == '%')
{
switch(*pWork)
{
case 'c':
{
char cvalue = va_arg(arg_ptr,char);
putchar(cvalue);
break;
}
case 'd':
{
int ivalue = va_arg(arg_ptr,int);
char buffer[32];
_itoa(ivalue, buffer, 10);
Puts(buffer);
break;
}
case 's':
{
char* psvalue = va_arg(arg_ptr,char*);
Puts(psvalue);
break;
}
case 'S':
{
std::string pstringvalue = va_arg(arg_ptr,std::string);
Puts(pstringvalue.c_str());
break;
}
default:
putchar('%');
putchar(*pWork);
}
}
else if(*pWork != '%')
{
putchar(*pWork);
}
}
pWork++;
}
va_end(arg_ptr);
}
void Puts(const char *pstr)
{
while(*pstr)
putchar(*pstr++);
}
// Printf函数支持可变类型为
// %c->char
// %d->int
// %s->char*
// %S->std::string
// Bug:不支持'"'等转义符
// 处理%%等出现问题
void Printf(const char *_Format, )
{
va_list arg_ptr;
va_start(arg_ptr,_Format);
const char *pWork = _Format;
while(*pWork != '"0')
{
if(pWork == _Format)
{
if(*pWork != '%')
putchar(*pWork);
}
else
{
if(*(pWork-1) == '%')
{
switch(*pWork)
{
case 'c':
{
char cvalue = va_arg(arg_ptr,char);
putchar(cvalue);
break;
}
case 'd':
{
int ivalue = va_arg(arg_ptr,int);
char buffer[32];
_itoa(ivalue, buffer, 10);
Puts(buffer);
break;
}
case 's':
{
char* psvalue = va_arg(arg_ptr,char*);
Puts(psvalue);
break;
}
case 'S':
{
std::string pstringvalue = va_arg(arg_ptr,std::string);
Puts(pstringvalue.c_str());
break;
}
default:
putchar('%');
putchar(*pWork);
}
}
else if(*pWork != '%')
{
putchar(*pWork);
}
}
pWork++;
}
va_end(arg_ptr);
}
调用代码:
Code
std::string s = "abc";
Printf("%cyPrint %cunction %s:%d, Support C++ std::string %Sversion %d",'M','F',"Version",1, s, 2);
std::string s = "abc";
Printf("%cyPrint %cunction %s:%d, Support C++ std::string %Sversion %d",'M','F',"Version",1, s, 2);
输出结果为:
MyPrint Function Version:1, Support C++ std::string abc...version 2
可变参数真是神奇的很啊。。。
2 原理:
我们来看看这几个宏到底干了什么
typedef char * va_list; // 这个仅仅是个重定义而已。。。
// 获取v的地址
#define _ADDRESSOF(v) ( &(v) )
// n的整数字节的大小,必须是sizeof(int)的整数倍。如sizeof(n)为5的话,_INTSIZEOF(n)为8(假设为32位机器的话)
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
// 给v的地址加上v的大小
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
// 给ap自增t的大小,并且获取原有ap的地址的数据,强制转型为t类型
// 这个相当于 ( *(t *)ap )
// (ap += _INTSIZEOF(t))
// 这一个宏相当于完成两件事情
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
// 给ap置0
#define va_end(ap) ( ap = (va_list)0 )
我们有必要了解一下C函数的调用规则了,在调用一个函数之前,调用方会将这个函数参数push(修改ESP指针),并且push规则是先push最后一个参数,最后push第一个参数,因此ESP指针最后应该是指向第一个参数。可变参数就是利用了这一点,一旦获取到第一个参数的地址后,就能够通过地址向前查找所有的参数。(注意:x86上的堆栈是反向的,push会使ESP的值减少,而不是增加)
上面的宏就是帮助用户查找所有的可变参数。
问题:
printf以及Printf都不是类型安全的。调用方必须保证参数个数的正确,以及参数类型的正确,否则将会发生不可预期的错误。
3 探索
是否可以写一个没有固定参数的函数,比如
int f (...);
据ANSI C说不行,但是我的vc8可行。
问题是写出这样的函数,va_start就用不上了,因为它需要可变参之前的那个固定参数。
其实我们可以自己从ESP中获取相关参数。
例子:
Code
// 获取第一个参数值的指针
// 函数在访问的过程中最重要的事情就是要确保堆栈的平衡,而在win32(vc8)的环境下保持平衡的办法是这样的:
// 1.让EBP保存ESP的值;
// push ebp
// mov ebp, esp
// 2.在结束的时候调用
// mov esp,ebp
// pop ebp
// retn
// 下面这个宏将ebp(原esp)中的指针+8放入ap中
// 注意:给ebp加8是因为中间隔着一个ebp和一个函数返回地址
#define va_start_get_first_parameter(ap) \
__asm mov eax, ebp \
__asm add eax, 8 \
__asm mov ap,eax
void NoFirstParameterPrintf()
{
va_list arg_ptr;
va_start_get_first_parameter(arg_ptr);
int first = va_arg(arg_ptr,int);
int second = va_arg(arg_ptr,int);
int third = va_arg(arg_ptr,int);
printf("First=%d"nSecond=%d"nThird=%d"n",first,second,third);
va_end(arg_ptr);
}
// 获取第一个参数值的指针
// 函数在访问的过程中最重要的事情就是要确保堆栈的平衡,而在win32(vc8)的环境下保持平衡的办法是这样的:
// 1.让EBP保存ESP的值;
// push ebp
// mov ebp, esp
// 2.在结束的时候调用
// mov esp,ebp
// pop ebp
// retn
// 下面这个宏将ebp(原esp)中的指针+8放入ap中
// 注意:给ebp加8是因为中间隔着一个ebp和一个函数返回地址
#define va_start_get_first_parameter(ap) \
__asm mov eax, ebp \
__asm add eax, 8 \
__asm mov ap,eax
void NoFirstParameterPrintf()
{
va_list arg_ptr;
va_start_get_first_parameter(arg_ptr);
int first = va_arg(arg_ptr,int);
int second = va_arg(arg_ptr,int);
int third = va_arg(arg_ptr,int);
printf("First=%d"nSecond=%d"nThird=%d"n",first,second,third);
va_end(arg_ptr);
}
调用代码:
Code
NoFirstParameterPrintf(3, 5, 7);
NoFirstParameterPrintf(3, 5, 7);
输出结果:
First=3
Second=5
Third=7
这样完全是可以获取第一个参数的地址的。问题是,由于没有类型信息和类型个数等信息(类似于printf中的%c等信息),所以这样的例子貌似意义不是很大。