C语言| 可变参数

可变参数..其实我也是第一次听说..
但是说起来,要说到当年用vim之前,还在用xcode的日子...每次打开xcode新建一个程序,他会帮你写好那么一小段代码:

#include <stdio.h>
int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    return 0;
}

当年我还...什么都不懂..还没开始敲代码就懵了。。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[],光看形式可能就有人不懂了。要理解他,我们首先得知道 指针数组数组指针 的概念。
c语言运算符优先级
从这张图中可以看出,在char * argv[]中,数组下标的优先级更高,于是,argc首先与数组下标先结合,因此,argc是一个数组,那么数组中存放着什么呢?接下来继续看,他又与指针相结合,噢,放着指针。什么类型的指针?继续看,噢,char型啊。这就是一个指针数组了。如下图:

argv图解

那么这些指针指向什么呢?我们来看两条语句:

char str1[] = "hi";
char* str2 = "hello";

像这样两种定义方式,区别在于,str1可能在栈、堆、在全局区(具体看怎么定义的),可以修改,而str2在地址空间中的字符常量区,不可修改;在这里我们可以类比着去理解。

  • 第一种类型:数组是开辟了一块空间后,每一块小空间,都放入一个数据(可能是int型,可能是char型,可能是指针、结构体等等)。
  • 第二种类型:一个指针,指向了一片空间,这块空间里存放着一串字符,如图所示:
字符串指针

这个指针p,指向了内存中的一块空间(在字符常量区),我们顺着这个指针,就能找到这个字符串,和数组原理相同,有时候(注意是有时候,不要以为数组名就是地址!!)数组名代表着数组首元素的地址,我们顺着这个地址去查找,就能知道这个地址的空间里存放着什么。
知道了这点以后,我们再来看char * argv[] 代表着什么:
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 的地址有何关联。
测试程序
程序运行结果:
arg的地址

可以看出,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..写的比较仓促,其实很不全...补上栈帧的知识就比较全了..但是栈帧又涉及到很多,写一起不方便,到时候分开写~

后续

另外,最近看了 CSAPP 的栈帧部分,更加深入理解了一点具体可变参数的实现原理。实际上 argv 是一个指针(数组的首地址,可以理解为一个指针),在调用 main 时,存在于新开辟的栈帧空间中,它指向了一片区域,区域内存在着一个数组,也就是上文提到的指针数组。而数组内存着传入的参数。
argc >= 1,因为第一个参数就是文件名,这个参数是必备的。从第二个参数开始,是用户输入的参数。

over

哈哈哈哈哈哈