以下所说编译环境为:
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 7.3.0 (clang-703.0.31)
Target: x86_64-apple-darwin15.5.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
平时我们在写一些工具函数的时候,很多时候会遇到参数长度会变的情况,比如我们要几个数字的和,我们是可以设置一个数组,但是假如这些数字是来自不同的地方,就需要我们额外定义一个数组,把元素先全部一个一个压到数组里,再调用函数。那就是有多少个数字就要有多少次压数组的操作了。
int getArrSum(int arr[], int len){
int sum_ret = 0;
for(int i = 0; i < len; i++){
sum_ret += arr[i];
}//用循环不用递归
return sum_ret;
}
/*
* int getArrSum(vector arr){
* int sum_ret = 0;
* for(unsigned i = 0; i < arr.size(); i++)
* sum_ret += arr[i];
* }
* return sum_ret;
* }
*/
//从不同渠道获得的整数
int a = getNumFromA(params);
int b = getNumFromB(params);
int c = getNumFromC(params);
//正常情况可以使用整数数组
int arr[] = {a, b, c};
//vector也可以, c++11已经可以使用这种方法直接初始化
//vector arr = {a, b, c}
getArrSum(arr, sizeof(arr)/sizeof(int));
//getArrSum(arr);
这种是我平时遇到一些元素,对他们进行求和这种操作的解决方法,但是假如在另外一个地方,求和的参数变为4个了,那么就需要在额外定义一个函数,这个函数是4个参数的。这样其实很麻烦,甚至连参数类型变了也要重新定义。
在C/C++中很早就支持变长参数了,依靠的是va_list, va_start, va_arg, va_end
这四个宏定义,先来看一下这四个的定义:
//file: _val_list.h
#ifndef _VA_LIST_T
#define _VA_LIST_T
typedef __darwin_va_list va_list;
#endif /* _VA_LIST_T */
//然后继续向下追
//file: _types.h
#if (__GNUC__ > 2)
typedef __builtin_va_list __darwin_va_list; /* va_list */
#else
typedef void * __darwin_va_list; /* va_list */
#endif
这里可以看到,等到追到__builtin_va_list
的时候,已经有分支了,首先在__GUNC__>2
的情况,定义了__builtin_va_list
为__drawin_va_list
,然后在老的版本就是一个void *
,就是一个普通的void
指针。
然后看下面这个定义:
//file: stdarg.h
#ifndef _VA_LIST
typedef __builtin_va_list va_list;
#define _VA_LIST
#endif
#define va_start(ap, param) __builtin_va_start(ap, param)
#define va_end(ap) __builtin_va_end(ap)
#define va_arg(ap, type) __builtin_va_arg(ap, type)
这里面定义了上述的四个,同样到__builtin_va_start/end/arg
就已经找不到了,我去google了一下,这些定义是在gcc
里面定义的,然后我去https://gcc.gnu.org
上面找了一下,只找到了以下内容:
After preprocessing, we see 'typedef char * va_list' in the source coming
in from /usr/include/stdio.h. (No, this file is not "fixincluded".) stdio.h
contains code of the form:
#ifndef _VA_LIST
#define _VA_LIST char *
#endif
#if !defined(__VA_LIST)
#define __VA_LIST
typedef _VA_LIST va_list;
#endif
这个内容是1999年写的,也就是说在很老的版本里,其实va_list
就是一个指针的别名或者宏定义。
现在的版本无外乎也就是进行了一些封装,终究是用一个指针来指向参数列表,而参数列表是一个连续内存.
那么封装成什么样子了呢?
在我找了很久都只能找到#define char* va_list
时候,我突然想到既然是封装的,应该就是一个结构体啊,我直接看下它里面有什么属性不就好了?
以下是我根据运行时候的内容看到的:
struct {
unsigned int gp_offset;
unsigned int fp_offset;
void * overflow_arg_area;
void * reg_save_area;
};
而va_list
是一个结构体数组:
typedef struct[] va_list;
正常来说应该是没有其他的内容了,我查看了每个结构体的大小:
va_list args;
sizeof(args[0]) == 24字节 //64位机子 int:4字节 void*:8字节
然而最终我还是找到了真正的文档是如何定义的:
与我的猜测出入很小:
The va_list Type
The va_list type is an array containing a single element of one structure containing the necessary information to implement the va_arg macro. The C definition of va_list type is given in figure 3.34
// Figure 3.34
typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];
The va_start Macro
The
va_start
macro initializes the structure as follows:
reg_save_area
The element points to the start of the register save area.
overflow_arg_area
This pointer is used to fetch arguments passed on the stack. It is initialized with the address of the first argument passed on the stack, if any, and then always updated to point to the start of the next argument on the stack.
gp_offset
The element holds the offset in bytes from reg_save_area to the place where the next available general purpose argument register is saved. In case all argument registers have been exhausted, it is set to the value 48 (6 ∗ 8).
fp_offset
The element holds the offset in bytes from reg_save_area to the place where the next available floating point argument register is saved. In case all argument registers have been exhausted, it is set to the value 304 (6 ∗ 8 + 16 ∗ 16).
文档里已经写了详细的解释了,我就不做赘述了。
只是我的环境里和文档写的好像有些出入:
gp_offset = 48
fp_offset = min(48, n*8) //每次偏移8个字节到48就不再变化了
reg_save_area //这个没问题
overflow_arg_area //当前面48个字节偏移完之后,会开始进行偏移以能够获取参数值
以上是C的文档内容了,那么下面说一下如何使用!
对于C/C++来说,其实使用方法很简单,还是上面的例子,求一堆数字的和:
int getSum(int n, ...){ //参数列表
int sum = 0;
va_list args;//声明一个va_list用来存放遍历参数列表
va_start(args, n); //n第一个参数表示后面会有多少个参数
while (n--) {
int arg = va_arg(args, int);//获取当前参数并进行一次偏移
sum += arg;
}
va_end(args); //释放va_list
return sum;
}
上面的代码中第一个参数n
并不是一定要写参数的个数,但是因为本身函数是不知道你会传过来多少个参数,即使你确定了传四个,运行的时候,函数还是不会知道的,所以这种情况就和strcpy
这种函数比较像了,只知道有个指针,并不知道指针什么时候指向结束的位置。
所以现在写的时候,就是把第一个参数当做参数个数传进去,这样是为了设计需要。
既然我们已经有了C语言的变长参数,鉴于C++向下兼容,为何还要有一个变长参数模板函数呢?
就像我面试的时候遇到的问题,为什么内存的设计要是堆栈这种结构。
存在就是有原因的,所以模版的变长参数函数是为了解决上面的一些问题而出现的。
首先:C语言的变长参数函数,是不知道什么时候遍历到最后一个参数的,每次找下一个参数就是做内存地址偏移。
其次:C语言的变长参数函数,是不能做多个类型的,因为不同类型,所占内存不同,地址偏移量也不一样,如果传的参数类型不一直的话,就会出现不可预测的结果。
定义非常简单,普通的模版定义方法:
template return_type function_name(T... args);
这种情况,就是任意多个参数都可以,即使0个。假如要求最少要传入一个参数的情况:
template<typename T1, typename... T2> return_type function_name(T1 t, T2... args);
这样就能够保证传入一个参数了,假如传入的参数是0个,就可以再写一个:
return_type function_name();
这样当参数是0个的时候,就会调用这个函数。
那函数内部怎么使用呢?
下面写个例子来看一下。
int sumArr() //边界函数 如果参数为空使用这个
{
return 0;
}
template<typename T1, typename... T2> int sumArr(T1 t1, T2... args){ //最少含有一个参数的模版函数
int Sum = 0;
Sum = t1 + sumArr(args...); //递归调用
return Sum;
}
这样就是一个sumArr函数了,假如调用的时候:
cout<<sumArr(1, 2,3,4)</* result */
10
这其实就是一个递归调用的过程,一直递归到边界函数那里,然后就可以弹栈操作了。
对于这个函数肯定大家都不陌生,这个就是一个变长参数的函数。
void print_s(char *s){
while(*s){
if(*s == '%' && *(++s) != '%'){
throw std::runtime_error("invalid format string: missing arguments");
}
cout<<*s++;
}
}
template<typename T, typename... Params> void print_s(const char * s, T value, Params... args)
{
while(*s){
if(*s == '%' && *(++s)!= '%'){
cout<return;
}
cout<<*s++;
}
throw std::logic_error("extra arguments need to printf");
}
以上就是我们常用的printf函数了,调用方法:
print_s("hello i am the %d person, my name is %s", 1, "alps");
/* result */
hello i am the 1 person, my name is alps
所以我们发现,在输出的时候,1
和"alps"
是不同的类型参数,所以其实这里是能解决变长参数是不同类型的情况的,即使是对象类型,只要它内部自己重载了操作符,就可以。
以上。