入栈规则
可变参数函数的实现与函数调用的栈帧结构是密切相关的。所以在我们实现可变参数之前,先得搞清楚 栈是怎样传参的。
正常情况下,C的函数参数入栈遵照__stdcall规则, 它是从右到左的,即函数中的参数入栈是从右到左的。
例如:
1 void test(char a, int b,double c,char * d){
2 printf("a:%#p
b:%#p
c:%#p
d:%#p",&a,&b,&c,d);
3 }
4 int main(){
5 char ch;
6 test('a',12,23,&ch);
7 return 0;
8 }
从各个形参变量的地址可以看出它们地址大小确实是从右到左依次减小的,说明它们是从右到左压栈的,
实现原理
对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过&a就可以得到a的地址,并通过函数原型声明了解到a是char类型的。
对于变长参数的函数,怎么办呢?其实想想函数传参的过程,无论"..."中有多少个参数、每个参数是什么类型的,它们都和固定参数的传参过程是一样的,简单来讲都是栈操作,同时C标准的说明中,是支持变长参数的函数在原型声明中的,但须至少有一个最左固定参数,嘿嘿~ 这样,我们不就可以得到其中固定参数的地址了吗?
知道了某函数帧的栈上的一个固定参数的位置,所以我们完全可以自己通过栈操作,推导出其他变长参数的位置,进而实现可变参数函数。(这个“固定的参数”一般就是可变参数函数里在第一个位置的参数,通过它就可以开始找到后面各种类型、个数不定的参数了)
(上述说了一下实现原理,知道的大佬就请忽略咯~)
实现步骤
我们常用的可变参数列表有这几个:
1.va_list
1 源码:typedef char * va_list;
va_list为char*类型重定义,所以va_list为一个指向char类型的指针(va_list p就等同于 char *p)
2.va_start(ap,v)
源码:1.#define va_start _crt_va_start
2.#define _crt_va_start(ap,v) (ap=(va_list)_ADDRESSOF(v)+ _INTSIZEOF(V))
把v的地址强转为va_list类型即char* ,把其移动_INTSIZEOF(V)个字节后的地址赋值给ap,其实就是让ap跳过第一个参数,指向"..."里的第一个可变参数。
(这里这个_ADDRESSOF(v)是一个宏,对变量v取地址的意思;这个INSIZEOF(v)也是宏,是对变量v向上取4的倍数,
也就是说如果v占字节大小在1~4个字节范围内,就取4,v所占字节大小在5~8字节之间就取8,以此类推...
至于这里,_ADDRESSOF(v),_INTSIZEOF(V)这两个宏怎么实现,后面有小弟我一点浅浅的见解~ )
3.va_arg(ap,t)
源码:1.#define va_arg _crt_va_arg
2.#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
它的作用就是将ap移动_INTSIZEOF(t)个字节后,然后再取出ap未移动前所指向位置对应的数据。
下面假使t为一个int型变量,如下图分析
4.va_end(ap)
源码:1.#define va_end _crt_va_end
2.#define _crt_va_end(ap) ( ap = (va_list)0 )
将0强转为va_list类型,并赋值给ap,使其置空
于是这样就可以开始实现my_print函数
#include<stdio.h>
#include<assert.h> #include<stdarg.h> void putInt(int n){ if(n>9){ putInt(n/10); } putchar(n%10+ '0'); } int My_print(const char *formt, ...) { assert(formt); va_list arg;//定义arg va_start(arg, formt);//初始化arg 即跳过传进来的第一个参数 ,这里相当于跳过"output:>%s %c %c %d"这个字符串 const char *start=formt; while (*start!= '