看山是山,看山不是山,最终看山才是山,并且是无穷的山峦。当我们学习一门技术的时候,起初是先模仿,但是最终是为了超越,也就是得到秘籍,看到本质。
于是,今天来继续看可变参数,我们来分析这个过程,代码如下:
声明int add (int num,...);...会说明是个可变函数,这样子编译器在编译遇到的函数的地方,就知道自动解析,依据传入的参数,直接进行进栈,从而不需要报错。
我们默认的函数,如果声明是两个参数,调用的是三个参数的话,最终会出错,会提示你找不到实现体。
声明完了后,实现函数,然后我们在使用的地方,直接传入多个参数,就不会出问题。
我们这里要看下add里面的具体实现,第一个参数我们利用这个值,用来判定长度,循环的结束点。
然后使用va_start 来卡找到第一个参数的地址,我这节画个图,大家就能理解了。再一个你也就明白了,为什么是int add(int num,...);而不是int add(...); 因为如果是这个,你实现的时候,就没法定位到起始地址,导致你无法解析后续的参数,这个明白了吧。
所以,编译时候,系统会提示错误,规定...参数前面必须有一个有名参数,否则系统编译的时候,你在实现体里面,无法做处理。这里就是 va_start(valist,num);
然后我们使用va_arg 去获取后续的参数,第一个是起始地址,第二个是后面紧跟着的数据,该以哪个大小去解析。我们这里是int,都是这个尺寸,所以用了循环。
在printf里面,使用的是%s ,%f这类处理,依据这个会变化。
使用完成后,就可以用va_end来结束指针。
我们如果把调用地方改成add(5,a,b);最终能输出出来结果,但是非常乱,原因很简单,这里的第一个参数5,让遍历寻找了栈上面的一些脏数据,导致结果未知。
我们如果把int b=6改成float b=6;会发生什么神奇的现象呢?出现了神奇的结果,原因很简单,float 进入栈的时候,占用的空间比int大,但是我们执行的时候,用了int大小去解析了这个数据,导致出现问题。
我们来看下如何修正这个问题,就需要格式化处理了,第一个参数,我们把它调整下,变成char *format,我们把代码改成这个,
为什么这里有float变为了double,主要是方便系统进行处理,升级后你 解析的时候,就要用double去解析,否则的话你处理完数据,ap指针就没有指到下一个位置,导致出错。
不过这个问题现在你不需要担心,如果你没有写对,系统会在编译时候提醒你,直接系统报错,让你去修改的。
这样子看下来,是不是觉得可变参数也没多神奇了?简单说下就是编译器支持...让函数参数可变,同时保留一个有名参数,让实现体可以用这个去定位到起始位置,然后进行遍历解析,完成逻辑处理。
我们把这个再抽象一层,简单来说,就是一组数据约束,存放在一起,然后我们依据一个格式化参数,对这个数据进行解析处理。当你看到这个的时候,就突然明白,协议的概念。
协议就是约定,约定双方同时遵循一个规则,同时遵守,就是协议。TCP/IP协议,ELF文件解析协议。
当你抽象到这里,基本上就大彻大悟,一切处理都是协议头+数据(+校验)。
在今天最后,我们来看下反编译后的代码,就明白了第一个参数的地址的意义,我这里用的int b=6;原因是如果是float的话,指令会比较复杂,不方便我们学习。
这里可以看到,我们地参数是 %d,%d a b 这三个,在汇编代码中,可以看到,
rbp-0x8 放的是b的值,6
rbp-0xc 放的是a的值,5
rbp-0x14,放着一个地址4007ec,这地方就是%d,%d 常量字串位置
所以我们add函数实现里面,fmt拿到的就是rbp-0x14的地址,也就是第一个参数,随后的解析就是依据给的格式,把对应数据解析出来,移动指针ap的位置到合适地方(依据对齐原则,以及sizeof(数据类型))
好了这一节就说到这里,下一节我们来说下 static 这个关键字我们该怎么用,以及它存在的意义。