C语言进阶可变参数列表
可变参数
可变参数是C语言提供的一种参数可变的机制,咱希望函数带有可变数量的参数,而不是预定义数量的参数。它允许咱定义一个函数,能根据具体的需求接受可变数量的参数,比如这种:
int Max(int num,...) { va_list arg; va_start(arg,num); int max = va_arg(arg,int); for(int i = 1;i<num;i++) { int sid = va_arg(arg,int); } if(sid > max) { max = sid; } va_end(arg); return max; } int main() { int a = Max(5,1,2,3,4,5); printf("%d\n",a); return 0; }
如上形式Max函数就用到了可变参数,注意!使用可变参数时,Max内首元素 ‘ 5 ’代表元素个数
那么问题来了,如果函数没有形式参数,可以给函数传递吗?答案是可以的,在C语言中,只要发生了函数调用并调用了参数,必定会形成临时变量;所谓临时拷贝(变量)的本质,也就是在栈帧内部形成的(从右向左形成临时拷贝(变量)).
宏观过程
va_list定义了可以访问可变参数部分的变量,他的本质是一个 char 类型指针。va_start 使 b 指向可变参数部分,va_end 是用来完成收尾工作的,本质就是将参数arg置为空,避免野指针。
掐头去尾,我们看看主体部分。首先 arg 指针先让我的数据入栈,我们打开反汇编能看到栈顶 esp 位置,再在内存窗口找到 esp 位置,就会看到这个经典的一幕,倒着入栈连着几个数据入栈是压在一起的,这种结构对我们查找元素就非常友好了。
宏观的框架就是我们传入的变量 num 就代表第一个参数 5,va_start 就是让 arg 原本指向5的 ,再让他指向有效部分,比如 1,根据指向 1 的起始地址, va_start 指向他的可变部分(去掉已指向的有效部分),具体如何实现见下文;最后 va_arg 就是根据类型 int ,从起始地址开始连续读取找到某一个元素,这样最终会把所需要的 max 的值读出来。
原理
可变参数列表对应的函数,最终调用也是函数调用,也要形成栈帧,栈帧形成前,临时变量会先入栈,根据咱之前总结的,参数位置都是相对固定的;在可变参数中 ,如果是短整型,一般都要进行整型提升,比如参数传入的是 char 类型,但实际传出的是 int 类型,这就是我们的 va_arg(arg,int)为什么是 int 而不是 char,所以在 va_arg 中指定了错误的类型,那结果无法预测。
要注意:
1.可变参数必须从头到尾逐个进行访问,如果你访问了几个可变参数后想半途而废,是可以做到的,但如果一开始就想访问中间某个元素的话,哒咩!
2.参数列表中至少有一个命名参数,如果连一个参数都没有,就没办法使用 va_start;
3.这些宏是没办法直接判断实际存在的参数数量的,也无法判断每个参数的类型
格局打开
#define _crt_va_start(ap,v) (ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) #define _crt_va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t) - _INTSIZEOF(t)) #define _crt_va_start(ap) (ap = (va_list)0) )
谈完原理就要谈原理的原理,可变参数的几个宏就给出了他的运作原理,ap 就相当于 arg, v 就相当于变量 num,va_list 相当于 char *,这里 ADDRESS 相当于取地址,所以就是在对 char 指针强转之后,此时就有了一个指针以一字节为单位,指向入栈的第一个有效元素。要想继续指向后面可变部分,就要继续向下移动四个字节,加上他本身大小就能移动到可变部分。
第二个宏也是特别有意思,ap是va_arg(arg,int),t 是我们的类型—— int ,括号里的部分:(ap += _INTSIZEOF(t))其中 INTSIZEOF 计算了int 的大小,这里让 ap 先 += 四个字节,就让 ap 直接指向了下一个元素的位置,后面再减去 int 的大小让他又回到了第一个元素
注意减的过程并没有赋给 ap,ap指向的是 2,而整个表达式指向的是 4,(t) 将这个 char 类型指针强转成 int 类型指针再解引用,通过强制转换,提取出符合类型大小的数据。整个过程就是把第一个元素分离出来了。这个设计可谓非常优秀,不仅指针下移了,元素也访问了,属实美哉。
end宏很好理解,ap = 0了再强转成 char* ,他的实际意义就是将指针归0,避免野指针。
四字节对齐
INTSIZEOF 是如何实现的?我们将 INTSIZEOF 转到定义,下面这段宏在函数内部就开始进行使用了为什么还要进行四字节对齐呢?因为从首元素到第二个元素中间的空间是可以访问的,我不限制大小就有可能访问不到第二个元素,所以在形参被运用时除了发生整型提升还有就是四字节对齐。
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1)& ~(sizeof(int) - 1))
比如我是个 char 类型,sizeof(char)+sizeof(4)-1 &~ (sizeof(4)-1)就是 4 &~ 3,0000……0100 & 1111……1100 = 4 , 如此就能实现以最小的方式向上四字节取整,完成四字节对齐。从可读性上讲,这是真的麻烦,我们其实直接写成(n+4-1)& -(4-1)也无妨,这种简洁版不香吗是吧。