可变参数..其实我也是第一次听说..
但是说起来,要说到当年用vim之前,还在用xcode的日子…每次打开xcode新建一个程序,他会帮你写好那么一小段代码:
#include <stdio.h>
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
return 0;
}
当年我还…什么都不懂..还没开始敲代码就懵了。。main后面那一段是什么鬼???
让我们来看一段函数:
int add(int x, int y) {
return x+y;
}
在小括号里的是这个函数的参数,有没有觉得和main函数的参数很像?
int main(int argc, const char * argv[]) {
return 0;
}
要知道,main函数并不是我们程序运行的第一个函数,在调用 main 前,还有 _tmainCRTstartup 等函数,当然,这不是我们关注的重点,重点是,就如同main调用其他函数一样,会传入参数,int argc, const char * argv[]
这些,就是调用 main 函数时,给 main 传入的参数。
当然,这不是完整的main参数列表,更完整的应为:
int main(int argc, char * argv[], char * envy[]) {
pragma—statements
}
这就是一个很完整的可变参数列表了。为什么说是“可变”呢?因为他的参数个数不是一定的。下面我们来具体分析:
argc :一个整型变量,参数个数;
argv :一个字符指针的数组,参数列表;
envp :传递给该程序的环境变量列表(告诉编译器,头文件、库等信息在哪里)。
撇开环境变量不提(因为我也不知道用来干啥的..以前看linux的书的时候有见过环境变量..但是没仔细看所以也不大清楚),我们主要来看前两个。
第一个参数,argc指接下来会传入多少个参数,这个很好理解,但是char * argv[]
,光看形式可能就有人不懂了。要理解他,我们首先得知道 指针数组 与 数组指针 的概念。
从这张图中可以看出,在char * argv[]
中,数组下标的优先级更高,于是,argc首先与数组下标先结合,因此,argc是一个数组,那么数组中存放着什么呢?接下来继续看,他又与指针相结合,噢,放着指针。什么类型的指针?继续看,噢,char型啊。这就是一个指针数组了。如下图:
那么这些指针指向什么呢?我们来看两条语句:
char str1[] = "hi";
char* str2 = "hello";
像这样两种定义方式,区别在于,str1可能在栈、堆、在全局区(具体看怎么定义的),可以修改,而str2在地址空间中的字符常量区,不可修改;在这里我们可以类比着去理解。
- 第一种类型:数组是开辟了一块空间后,每一块小空间,都放入一个数据(可能是int型,可能是char型,可能是指针、结构体等等)。
- 第二种类型:一个指针,指向了一片空间,这块空间里存放着一串字符,如图所示:
这个指针p,指向了内存中的一块空间(在字符常量区),我们顺着这个指针,就能找到这个字符串,和数组原理相同,有时候(注意是有时候,不要以为数组名就是地址!!)数组名代表着数组首元素的地址,我们顺着这个地址去查找,就能知道这个地址的空间里存放着什么。
知道了这点以后,我们再来看char * argv[]
代表着什么:
图中可以看出,argc是一个数组,数组中存放着指针,每个指针指向一个字符串。(数组最后一个元素存放NULL指针,图中未画出)
关于main函数的可变参数,在linux下的体现更突出一些,我们可以在编译链接期间传入参数,来达到一些特定的目的,这里暂且不提(不提的原因是…还没学linux..所以具体什么作用我了解的也不是很清楚TAT)。
接下来说说可变参数的用处。
根据可变参数列表的特性,在什么地方用起来会很方便呢?
看一个例子:
求几个数的平均数(具体多少个数不知道)
这样一道题,或许我们可以将所有数放入一个数组中,再将数组和元素个数作为参数传入,一个数一个数的读取取平均值即可。但这里要介绍的是另一种方法:利用可变参数。
以此函数为例,我们来具体分析下。
一个含可变参数的函数由以下几部分组成:
- 头文件
#include <stdarg.h>
- 函数名:
// 注意,参数列表里的 "..." 实际写的时候就是这么写的
// n代表之后还会传入n个参数,n后面可以没有参数,但n一定要传
int average(int n, ...) {
// ...
}
- 函数主要内容
va_list arg;
va_start(arg, n);
// ... va_arg(arg, int); // int也可为其他类型
va_end(arg);
这里的 va_list
,用于定义一个变量,变量名我取名为arg,他的本质其实就是一个字符型指针,vs中定义如下(由于我没有vs,所以摘抄自网络):
typedef char* va_list;
注意,这里为什么需要是char? 接下来后面会提到。
我们的arg这个指针,声明完,需要让他指向地址空间中的某一地址处,就用到了接下来这个函数:
void va_start ( va_list ap, prev_param );
va_start(arg, n)
的功能 毫无疑问是将arg指针指向某一地址处,但我们还不清楚是指向哪里,目前我们所知道的地址只有 n 的地址,测试下和 n 的地址有何关联。
程序运行结果:
可以看出,n 和 arg 的地址只相差了4(这里是64位平台int型,占4字节),且 arg 的地址比 n 大4。
再来看vc中的宏:
// 获取类型占用的空间长度,最小占用长度为int的整数倍
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
v即为这里的n,ap即为这里的arg,ap = (va_list)&v
,可知arg 赋为n的地址加上n的类型的大小(我写的代码中为4)。
这里说一句,前面对arg的定义为什么需要是字符型呢?我们知道,对指针进行自增或自减操作,实际上是加或减了指针的类型的字节数。什么意思呢,比如说这里定义arg为int型,当arg+k,其实并不是加了k,而是加了4k(是跳过了其类型的k倍)。
那么,加上这么一段大小后,指向了哪里呢?
其实,传参时,参数从后往前传(这个解释起来就很麻烦了,以后可能会写一篇关于栈帧的博客,应该会具体解析,写完再传地址吧…),这里只需要知道,
传参时,后面的参数比前面的参数地址大即可,由此可知,arg指向了 n 后面的第一个参数,即可变参数部分的第一个参数。
既然我们已经知道了第一个参数的地址,由于函数调用的性质(啊又要用到栈帧的知识了…话说我真的应该先写栈帧再写可变参数的..),参数的地址,由低到高连续排列(就像数组一样)。我们将其类比成数组(还记得我前面讲到的那个用数组的方法吗,原理都是差不多的),由其地址得到他们的值,再将所有值相加来求平均数。
那么如何通过地址得到他们的值?
下一个函数为va_arg(arg, int)
,vs中定义如下:
type va_arg ( va_list ap, type );
此函数用于提取数据,从网上找到了其宏如下:
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这条语句更能说明 va_arg
是如何运作的。他将arg指针先强转为当前可变参数的类型,再解引用后返回,并将其指向下一个可变参数(ap += _INTSIZEOF(t))。由于会有返回值,所以我们可以用一个变量来接受返回值。
最后,让arg指针指向NULL。
void va_end ( va_list ap );
- 完整的求平均值的函数:
#include <stdio.h>
#include <stdarg.h>
int average(int n, ...) {
va_list arg; // 定义指针
int i = 0, sum = 0;
va_start(arg, n); // 指针指向第一个可变参数
for ( ; i < n; ++i) {
// 用于接收每个参数的值
sum += va_arg(arg, int);
}
va_end(arg); // 置空
return sum/n;
}
int main()
{
printf("%d\n", average(5, 1, 2, 3, 4, 5));
return 0;
}
以上就是所有内容了… emmm..写的比较仓促,其实很不全…补上栈帧的知识就比较全了..但是栈帧又涉及到很多,写一起不方便,到时候分开写~
// 另外。。。具体可变参数的实现原理。。过几天可能会写一篇栈帧的博客。。写完再补上博客地址