C/C++可变参数
1. C语言可变参数
c中典型的变参函数是我们已经用得非常熟练的printf和scanf函数,它们都可以接受不确定个数的参数,它们的函数声明形式如下:
1
2
int printf(const char *format, ...);
int scanf(const char *format, ...);
两个函数后面的...
是占位符,并不是参数,而是告诉编译器,该函数是变参函数,不管该函数使用时参数有多少个,都对其一一做压栈处理,这就实现了变参函数。
变参函数的实现与栈密切相关,栈是一种数据结构,是一种只能在一端进行插入和删除的特殊线性表。栈的存储规律是先进后出(First In Last Out),先入栈的数据被压入栈底,最后入栈的数据在栈顶,需要读数据时则从栈顶开始弹出数据(最后一个数据被第一个弹出来)。
一个栈中,栈底是固定的,而栈顶是浮动的,对栈的插入和删除操作,不需要改变栈底的位置。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。在i386机器中,栈底位于高地址,栈顶位于低地址,压栈(PUSH)使得栈顶地址变小,弹栈(POP)(也可以称为退栈)使栈顶地址变大。
栈在程序的运行中有着举足轻重的作用。栈可以用来在函数调用时存储断点信息,做递归时要用到栈。最重要的是栈保存了一个函数调用时所需要的维护信息,通常称为堆栈帧,保存函数的返回地址和参数,以及函数的局部变量。
下面我们来分析变参函数参数的压栈过程。一般来说,函数参数的入栈顺序是从右向左的,意味着,最右边的参数最先入栈,位于高地址处,而最左边的参数最后入栈,位于低地址。下面通过一个具体的例子来看函数的压栈操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
void myPrint(int n, ...) {
int *p, i;
p = &n + 1;
for (i = 0; i < n; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
return;
}
int main() {
myPrint(4, 1, 2, 3, 4); // 1 2 3 4
return 0;
}
以上代码,首先在myPrint函数中使用占位符...
,因此该函数在编译时被当成变参函数来处理,对该函数调用中的参数将一一进行压栈。上述代码定义了一个int型指针p,由于函数参数的压栈顺序是从右向左,参数的存储的地址由高地址到低地址,所以p = &n + 1
得到的是指向第一个可变参数的地址,接下来通过循环一一取出函数中的参数。
在使用变参函数时,...
前面至少要有一个普通的参数。必须知道参数什么时候结束,如果没有给出变参函数的个数,直接结出第一个参数,则必须约定一个参数作为结束标志。
但是这里需要注意的是,上面的代码只在32位系统下能够运行成功,64位系统下无法顺序输出,因此需要安装32位的库和运行环境:
1
2
3
4
# Linux查看系统架构(结果为 x86_64)
uname -m
# 安装32位的库和运行环境
apt-get install gcc-multilib
安装环境完成后,gcc增加-m32
参数,表示编译32位架构下的可执行程序,gcc myPrint.cpp -m32 -o myPrint
而在64位系统中,要实现以上功能,需要借助
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<cstdarg> // C中是<stdarg.h>
// va_list是一种数据类型,args用于持有可变参数。
va_list args;
// 调用va_start并传入两个参数:第一个参数为va_list类型的变量
// 第二个参数为"..."前最后一个参数名
// 将args初始化为指向第一个参数(可变参数列表)
va_start(args, paramN);
// 检索参数,va_arg的第一个参数是va_list变量,第二个参数指定返回值的类型
// 每一次调用va_arg会获取当前的参数,并自动更新指向下一个可变参数。
va_arg(args,type);
// 释放va_list变量
va_end(args);
接下来实现一下myPrint函数:
1
2
3
4
5
6
7
8
9
10
11
void myPrint(int n, ...) {
int i, arg;
va_list args;
va_start(args, n);
for(i = 0; i < n; ++i) {
arg = va_arg(args, int);
printf("%d ", arg);
}
printf("\n");
va_end(args);
}
上述函数可能实现方式为:
1
2
3
4
5
typedef char *va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#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)
一个一个来解释:
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
函数的压栈是按照4字节对齐的,小于4字节的统统按4字节对齐来入栈,而这里的_INTSIZEOF宏,就是用于实现内存中的字节对齐操作。
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))
作用是先得到变量v的地址,然后将其转化成char型指针,再加上v对齐之后所占用的内存大小,使指针指向下一个参数。注意此时的指针为char类型,所以接下来在使用va_arg(ap, t)时要将其强制转换为当前参数类型t的指针。
#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
ap += _INTSIZEOF(t)得到的是下一个参数的地址,再减去_INTSIZEOF(t)得到当前参数的地址。通过一个for循环就可以一一取出其中压栈的所有参数。
#define va_end(ap) (ap = (va_list)0)
清除指针,表示在接下来的部分不再使用该指针变量
上述可以用在一般函数上,但是无法使用在宏定义中,如果一定要在宏定义中使用,需要配合 __VA_ARGS__,示例如下:
1
2
3
4
5
6
//#define CALC(fmt, ...) func(fmt, ...) //错误的使用
#define CALC(fmt, ...) func(fmt, __VA_ARGS__)
int main() {
CALC("%d %f %s\n", 1, 2.0f, "hello world");
return 0;
}
实现类似printf函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<stdarg.h>
#include<cstdio>
void myPrintf(const char *fmt, ...) {
char c;
va_list list;
va_start(list, fmt);
while (*fmt != '\0') {
c = *fmt;
if (c != '%') {
putchar(c);
}else {
fmt++;
switch (*fmt) {
case 'd':
printf("%d", va_arg(list, int)); // #1
break;
case 'f':
printf("%f", va_arg(list, double));
break;
case 's':
printf("%s", va_arg(list, const char*));
break;
default:
break;
}
}
fmt++;
}
va_end(p);
}
int main() {
int a = 5;
double d = 3.14;
const char* s = "test";
myPrintf("a = %d d = %f s = %s\n", a, d, s); // a = 5 d = 3.140000 s = test
return 0;
}
注意上述代码中#1处,64位与32位环境下都可以这样写,但是下面的写法只适用于32位环境:
1
2
3
4
case 'd':
printf("%d", *((int*)list));
va_arg(list, int);
break;
TODO:搞清楚原因。
2. C++可变参数
2.1 可变参数函数
1. initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参(C++11新标准)。和vector一样,initializer_list也是一种模板类型。
下面给出一个例子,需要注意的是,含有initializer_list形参的函数也可以同时拥有其他形参。另外,如果想给initializer_list形参传递一个实参的序列,必须把序列放在一对花括号内:
1
2
3
4
5
6
7
8
9
10
11
string func(initializer_list<string> li) {
string str("");
for(auto beg=li.begin(); beg!=li.end(); ++beg)
str += *beg;
return str;
}
int main() {
cout << func({"This"," ","is"," ","C++"}) << endl;
return 0;
}
2. 占位符…形参
与c语言一致,见上文。
2.2 可变参数模板
C++11 中引入了新的功能,可变参数模版,语法如下:
1
2
template <typename T, typename ... Args>
void func(T t,Args ... args);
这里面,Args 称之为模板参数包(template parameter pack),表示模板参数位置上的变长参数,
args 称之为函数参数包(function parameter pack),表示函数参数位置上的变长参数
可以使用 sizeof…() 获取可变参数数目
先看一个示例:
1
2
3
4
5
6
7
8
9
template<typename... Args>
void print(Args... args) {
int num = sizeof...(args);
printf("%d\n", num);
}
int main() {
print(1, 2, "123", 4); // 4
return 0;
}
遍历可变参数的时候,有两种遍历方式:
1. 递归遍历
可变参数一般使用递归的方式进行遍历,利用模板的推导机制,每次从可变参数中取出第一个元素,直到包为空
缺点:递归毕竟是使用栈内存,过多的递归层级容易导致爆栈的发生
1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
void print(const T& val) {
cout << val << endl;
}
template<typename T, typename... Args>
void print(const T &value, Args... args) {
cout << value << endl;
print(args...);
}
int main() {
print(1, 2, "333", 4);
return 0;
}
2. 非递归遍历
利用 std::initializer_list ,即初始化列表展开可变参数,这里是C++17引入的折叠表达式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
void run(const T &t)
{
cout << t << endl;
}
template<typename... Args>
void print(Args... args)
{
std::initializer_list<int>{(run(args), 0)...};
}
int main()
{
print(1, 2, "333as", 4);
return 0;
}