喵の窝

变参函数介绍以及使用技巧

简介

printf函数的声明如下

1
int printf(const char * __restrict, ...);

可以看到,其最后一个参数为三个点,这表示这个函数的参数是可变的.
obj-c,c和c++都支持这种以三个点结尾的可变参数的函数.
obj-c中这种函数的声明方式为

1
-(void) function:(int) arg1 arg2:(int) arg2, ...;

需要注意的是,变长参数的部分只能在参数列表最后.

参数解析

基础

在stdarg.h头文件中提供了一些宏和一些typedef来帮助我们解析变长参数函数.它们分别是

  • typedef va_list 这个类型的变量表示一个变长参数列表
  • macro va_start 这个宏用于开始读取列表
  • macro va_end 这个红用于结束读取列表
  • macro va_arg 这个宏用于读取列表中的参数

用法如下

1
2
3
4
5
6
7
8
+(void) fun:(int) arg1 arg2:(int) arg2, ... {
va_list args;
va_start(args, arg2);
for (int i = 0; i < 3; ++i) {
NSLog(@"%d", va_arg(args, int));
}
va_end(args);
}

代码先声明了一个va_list变量用于表示变长参数,传入va_start的第一参数为va_list变量,第二个参数为函数最后一个参数的名字.
接着,在一个for循环中调用va_arg读取参数,va_arg的第一个参数为va_list变量,第二个参数为需要读取参数的参数类型.这里我们读取了三个int类型的参数
最后,我们调用va_end结束读取.
调用这个函数的代码如下

1
[test fun:1 arg2:2, 3, 4, 5];

程序输出为

1
2
3
3
4
5

进阶

动态确定的类型和个数

在上面代码中,我们事先知道了要读取3个int类型的参数,但是如我我们不知道需要读取的参数个数和参数类型,那怎么办呢?
简单来讲,这个问题是无解的.
详细来说,我们必须在函数的第一个参数中说明变长参数的个数和类型.
以printf为例,printf的第一个参数中用可以使用%c, %d这种占位符来表示变长参数的个数和类型.
对于自己写的变长参数函数,我们也可以采用类似的方法,来处理变长参数的个数和类型.

变长参数的传递

考虑下面两个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void fun1(int arg1, ...) {
// if (arg1....) {
// } else {
// 调用fun2,把可变参数全部传给fun2;
// fun2(.....);
// }
}

void fun2(int arg2, ...) {
va_list args;
va_start(args, arg2);
// 用va_arg处理参数
va_end(args);
}

在fun1的else分支,我们需要把fun1的变长参数直接传给fun2.可是有方法能直接传递变长参数吗?
简单来讲没有.
但是呢,由于va_list实际上是一个指针,而在被调函数中修改一个指针形参的值可以改变主调函数中对应的实参.因此我们可以借助va_list来完成参数的传递.
基本思路如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void fun1(int arg1, ...) {
// if (arg1....) {
// } else {
// va_list args;
// va_start(args, arg1);
// fun2_arg(args);
// va_end(args);
// }
}

void fun2(int arg2, ...) {
va_list args;
va_start(args, arg2);
fun2_arg(args);
va_end(args);
}

void fun2_arg(va_list args) {
// 原来fun2中处理va_list的逻辑
// 用va_arg处理参数
}

这样一来,我们就可以传递变长参数的部分了.