C语言变参函数实现方法分析

26 篇文章 0 订阅

    C语言中,最经典的可变参数函数莫过于printf(),它的声明如下所示:

int printf(const char *format, ...);

    printf()的参数列表中,第1个参数是const char* format,而第2个参数是3点“...”,两者用逗号隔开。这就是可变参函数的声明方式。那么,第1个参数和后续的若干参数之间是什么关系呢?可以写个程序来测试一下。

#include <stdio.h>

void function(const char *arg, ...);

int main(int argc, char *argv[]){
	
	function("hello", "world", "I love you");
	return 0;
}

void function(const char *arg, ...)
{
	printf("arg=%s\n", arg);
	printf("(&arg)[0]=%s\n", (&arg)[0]); // &arg的类型是char**, 可以理解为字符串数组,然后用下标访问序号为0的字符串
	printf("(&arg)[1]=%s\n", (&arg)[1]);
	printf("(&arg)[2]=%s\n", (&arg)[2]);
}

    编译程序,运行结果如下:


                            图 1

    结论:C语言的可变参数列表是一个字符串数组,arg是第1个字符串,其它字符串参数紧跟在后面。

    需要注意的是,arg是字符串而不是字符串数组!由于数组的特性,知道第1个元素就可以依次访问剩余的元素,故知道第1个元素的地址即可!

    知道了上述结论,已经可以自定义自己想要的可变参函数。但是每次都要通过(&arg)[0]的形式来访问参数,不是很方便。因此,可以定义一些宏来简化参数的访问。具体实现如下:

#include <stdio.h>

// 为了简化可变参数的访问,定义一下类型和宏
typedef char * va_list;
#define _INTSIZEOF(v) ( (sizeof(v) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) // 32位系统中,此宏的结果恒为4
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) // 初始化ap使其指向第2个参数(忽略第1个!)
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) // 将当前的字符串返回,并将ap指向下一个字符串
#define va_end(ap) ( ap = (va_list)0 ) // 将ap赋为char *即NULL

void function(const char *arg, ...);

int main(int argc, char *argv[]){
	
	function("hello", "world", "I love you");
	return 0;
}

void function(const char *arg, ...)
{
	va_list ap;
	va_start(ap, arg);
	
	printf("arg=%s\n", arg);
	printf("va_arg(ap, char*)=%s\n", va_arg(ap, char*)); // 利用辅助宏来获取参数 
	printf("va_arg(ap, char*)=%s\n", va_arg(ap, char*));
	printf("va_arg(ap, char*)=%s\n", va_arg(ap, char*));
	
	va_end(ap);
}

    编译程序,运行结果如下:


                             图2

    结论:这个结果和图1的结果基本相同。但需要注意的是图2中hello只输出一次,因为第6行代码已经说明,忽略第1个参数。另外,图2的最后一个输出为乱码,那是因为function的输入只有3个参数,而最后一个输出对应的是第4个参数,故乱码。

    那么,上面的宏分别代表什么意思呢?下面进行详细的分析。

#define _INTSIZEOF(v) ( (sizeof(v) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

    对比其宏名和sizeof()可知,该宏主要用于获取类型占用的空间长度,但与sizeof()不同的是,此宏获取的最小占用长度为int的整数倍。例如:在32位系统中sizeof(int)=4,因此~(sizeof(int) -1) 的二进制最低两位为0,任何数与其作位与则使得末两位为0!在32为系统中,当v为char型时,sizeof(v) + sizeof(int) -1 = 4,位与后结果为4;当v为short型时,sizeof(v) + sizeof(int) -1 = 5,位与后结果为4;当v为int型或者指针时,sizeof(v) + sizeof(int) -1 = 7,位与后结果为4;当v为double型,sizeof(v)为8,最终位与的结果是8。这些结果都是4的整数倍!

#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )

    此宏主要用于获取可变参数列表的第2个参数的地址(ap是类型为va_list的指针,v是可变参数第1个参数)。为什么是第2个参数,不是第1个呢?下面将详细分析。由定义知v为char*类型,因此&v为char**类型,即参数列表数组。然而,为了避免赋值时类型冲突,将&v强制转换为va_list类型,也就是char*型,这是一个非常有意思的做法。因此,ap并不是第1个字符串的地址!可以用下面的程序进行测试。

#include <stdio.h>

// 为了简化可变参数的访问,定义一下类型和宏
typedef char * va_list;
#define _INTSIZEOF(v) ( (sizeof(v) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) // 32位系统中,此宏的结果恒为4
#define va_start(ap,v) ( ap = (va_list)&v/* + _INTSIZEOF(v) */) // 仅测试(va_list)&v的效果,故先注释+ _INTSIZEOF(v) 
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) // 将当前的字符串返回,并将ap指向下一个字符串
#define va_end(ap) ( ap = (va_list)0 ) // 将ap赋为char *即NULL

void function(const char *arg, ...);

int main(int argc, char *argv[]){

	function("hello", "world", "I love you");
	return 0;
}

void function(const char *arg, ...)
{
	va_list ap;
	va_start(ap, arg);
	printf("ap=%s\n", ap); //将ap当做第1个字符窜的地址进行输出,看其结果对不对
	printf("((char **)ap)[0]=%s\n", ((char **)ap)[0]); // 将ap先转换为char**类型,然后将其当做字符窜数组,用下标进行访问
	va_end(ap);
}

    编译代码,运行结果如下:


    可见,将ap当做字符串的输出时乱码,而现将其转换为char**再用下标的方式进行访问,则可以得到正确的字符串输出。

    上面提到,va_list主要用于获取可变参数列表的第2个参数的地址,那是因为&v加了_INTSIZEOF(v)再赋值给ap,从而使得ap指向的是第2个参数,而不是第1个。为什么要这样做呢?那是因为第1个参数可以由arg获得,不必再过va_list来获取了!

#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

    此宏主要用于获取可变参数的当前参数,返回指定值,并将指针指向下一参数(t——当前参数的类型)。最里层的(ap += _INTSIZEOF(t)) - _INTSIZEOF(t),表示先将ap增加_INTSIZEOF(t)的值,而再减_INTSIZEOF(t)则表示增加前的值了,然后对其进行类型转换(t *)。对于字符串,t为char*,因此(t*)就是(char **)。最后对char**类型进行取值运算,则变成char*型。好曲折,但是很巧妙吧!

#define va_end(ap) ( ap = (va_list)0 )

    这个宏几比较简单了,主要用于清空va_list可变参数列表。将ap赋值为(va_list)0类型,也即是(char *)0类型,即NULL!这时ap不再有效!

    这些宏确实方便了可变参函数的实现,但是想不出这么巧妙的宏怎么办呢?没关系,别人早就已经想好了,而且已经实现了,上述的宏都在头文件stdarg.h中!使用时只需要包含此头文件即可!


参考资料

[1]va_list_百度百科

[2]C语言可变参数函数详解示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OneSea

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值