什么是可变参数?
可变参数就是指参数个数不确定,举一例即可明白。
- 实现一个函数计算整数和,形式如下
int sum(int a,...) //被调用的函数,可变的部分用...来表示 { //....具体操作 } sum(10,20); //调用函数形式1 sum(10,20,30); //调用函数形式2 sum(10,20,30,40); //调用函数形式3
如代码所示,无法确定究竟传进来多少个参数,可能2个,3个,4个等等,但是却要求sum函数针对各种可能都能求得结果。
函数堆栈
C语言函数的参数是从右往左压入堆栈,有以下几个特点:
- 栈底在高地址,栈顶在低地址,增长方向是从高字节->低字节
- 入栈的次序是从右往左
这样说有点抽象,最好的方式就是观察内存,实践一下一目了然。
先写一个程序
#include "stdio.h" void sum(int a,...) { char *c=(char *)&a; } //运行到此处 int main() { sum(10,20,30,40); //断点,F11进入函数 return 0; }
调试的内存数据如下:
根据上面的推论,分别为40,30,20,10四个数依次入栈,因此40处于最高字节,10处于最低字节,这验证了前面的结论。
手动计算结果
现在先用正常的思路来计算结果,四个整数既然已经入栈,并且第一个参数的地址可以得到(通过强制类型转换),那么顺着这条线往下挨个找到每个数是没有问题的。
代码:
#include "stdio.h" void sum(int a,...) { char *c=(char *)&a; //第一个参数的地址,即处于栈顶位置(最低位置)的地址(也即本例中整数10的地址); printf("%d\n",*((int *)c)); c+=sizeof(int); //手动计算,地址+4指向第二个参数,得到参数20 printf("%d\n",*((int *)c)); c+=sizeof(int); //手动计算,地址+4指向第三个参数,得到参数30 printf("%d\n",*((int *)c)); c+=sizeof(int); //手动计算,地址+4指向第四个参数,得到参数40 printf("%d\n",*((int *)c)); } int main() { sum(10,20,30,40); //断点,F11进入函数 return 0; }
打印的结果分别是:10,20,30,40,说明手工获取每一个参数是可以的。
从上面可以看出,理解可变参数的问题在于知道参数是如何传入堆栈的,然后顺着把堆栈的每个数取出来即可,一句话概括就是:把堆栈里面的数据顺序取出来。
衍生问题:字符和字符串是如何入栈的?
整型数据是直接数据入栈的,那么字符和字符串呢?根据程序验证可知
- 字符依然是数据直接入栈,并且占用4个字节(对齐)
- 字符串入栈的却是字符串地址
试验程序如下:
#include "stdio.h" void sum(int a,...) { char *c=(char *)&a; //第一个参数的地址 } int main() { sum(10,20,'c',30,"test"); //断点,F11进入函数,传入五个参数,包括整数,字符和字符串 return 0; }
调试内存数据:
内存数据说明,结论是正确的。(对于其它类型的数据没有调试,有兴趣的自己试验)
回到计算整数和问题
前面已经总结了什么是可变参数,函数入栈的概念,以及整型,字符和字符串等入栈的方式,这些知识非常重要,一点一滴汇集然后来解决大问题。
回到问题,我们现在能做到:得到各个参数值。下面还剩下唯一的一个问题就是没法确定参数的个数。
于是,我们采用一个边界限制一下,比如参数传进去一个END,如果得到的整数值等于这个END,说明传入的参数已经结束。
程序如下:
#include <stdio.h> #define END -1 int sum (int first, ...) { char * ap=(char *)&first; //ap指向第一个数据的地址 int result = first; //和初始化等于第一个数据 int temp = 0; for(;;) //循环相加一直到尾部 { ap+=sizeof(int); //指向第二个数据 temp=*ap; //得到数据 if(temp != END) //判断边界,如果没有到尾 { result+=temp; } else break; } ap=(char *)NULL; //指针置为空; return result; } int main () { int result = sum(1, 2, 3, 4, 5, END); printf ("%d", result); return 0; }
到了这一步,终于解决开头提出的问题,如何用一个函数sum计算不定长的参数和,虽然实现的有点简陋,但基本说明了不定长参数的解决样式。
三个宏
有了前面一系列通俗的解释,我们了解了不定长参数的解决方式,可以大致的总结一下:
- 得到第一个参数的地址
- 根据第一个参数的地址往下得到第二个参数的地址(并获取值),因为不管参数是什么,入栈的结果都是四个字节,整数、字符或者字符串的32位指针等等。
- 依次类推,一直获取到参数的结尾
现在来看几个库中的宏
typedef char * va_list; //类型 #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 )
这些宏的作用就是把前文中简陋的手工做法用宏来实现,效果相同。所以根据前面最基础的方式,一步一步的推导到现在就能理解这三个宏的用法,也就是知其所以然。
一、va_start(ap,v)
v:函数中的定位参数,可能是函数第一个参数,也可能是中间某一个。这个参数可能是整型、字符或者字符串(后面会逐步说明)
比如:
sum(int a,int b,int c,int d,...);
va_start(ap,c); //执行之后,定位的开始参数为第三个参数c,那么ap就指向它的下一个参数d.
ap:等于定位参数的下一个参数的地址
因此整个宏的作用就是让ap指向v之后的参数。
宏实现中还有一个宏_INTSIZEOF(v),这个宏有点难,关于这个宏的详细内容可以参看这篇博客:
http://www.cnblogs.com/diyunpeng/archive/2010/01/09/1643160.html
其中牵涉到一些数学知识,*注: 凡是编程上稍难的东西都可能牵涉到数学。
二、va_arg(ap,t)
返回ap指向的参数值,难点同上。
三、va_end(ap)
将ap置空。
将手工方法改成宏的方法
上面说明了为什么会出现宏,其实是为了方便,比手工书写的严谨,但是原理是相同的。
现在将前面的代码改为宏的实现方式并做下对比:
#include <stdio.h> #include "stdarg.h" #define END -1 int sum (int first, ...) { char *ap; va_start(ap,first); //ap指向第二个参数; int result = first; //和初始化等于第一个数据 int temp = 0; for(;;) //循环相加一直到尾部 { temp=va_arg(ap,int); //得到当前参数值并使ap指向下一个参数 if(temp != END) //判断边界 { result+=temp; } else break; } va_end(ap); return result; } int main () { int result = sum(1, 2, 3, 4, 5, END); printf ("%d", result); return 0; }
printf的实现
前面的内容系统说明了可变参数的问题,实现了一个简易的求和函数,现在研究一下printf,看其形式:
printf(".%s,%d..\n",a,b,c,d);
事实上和前面的sum函数很相似,区别在于printf函数的第一个参数是字符串,后面接着的是各个具体参数。我们可以看出,最关键的部分在于前面第一个字符串参数,因为后面有多少个参数,每个参数具体又是什么类型的值都是由前面的这个字符串来指定。
比如:%d,说明后面打印的是整型,%s,说明后面打印的是字符串。
因此我们需要做的是把第一个字符串参数的每一个字符进行遍历,算法为:
一、假如字符不是%,就往后继续。
二、假如字符是%,则判断后面接着的这个字符是什么,比如是'c','d',或's'等。
看下代码:
#include "stdio.h" #include "stdarg.h" void print(char* fmt, ...) { char* pfmt = NULL; va_list ap; va_start(ap, fmt); //ap指向第一个参数(字符串之后的) pfmt = fmt; //pfmt指向字符串首地址,准备用它遍历 while (*pfmt) { if (*pfmt == '%') //遇到%号 { switch (*(++pfmt)) //判断%号后面的符号 { case 'c': printf("%c\n", va_arg(ap, char)); break; case 'd': printf("%d\n", va_arg(ap, int)); break; case 's': printf("%s\n", va_arg(ap, char *)); break; default: break; } pfmt++; } else { pfmt++; } } va_end(ap); } int main() { print("test:%d,%s", 20, "字符串测试"); return 0; }
有两个需要说明的问题:
一、print模拟printf函数,关键在于对前面的字符串进行遍历,根据遍历结果进行后续处理。
二、后面又借用printf()函数打印只是为了说明问题,可以自行设计函数。
/*****待续*****/