从printf谈可变参数函数的实现

从printf谈可变参数函数的实现

  • 一直以来都以为printf彷佛是c语言库中功能最强大的函数之一,不只由于它能格式化输出,更在于它的参数个数没有限制,要几个就给几个,来者不拒。printf这种对参数个数和参数类型的强大适应性,让人产生了对它进行探索的浓厚兴趣。

1. 使用情形 c++

1. int a =10;
2. double b = 20.0;
3. char *str = "Hello world";
4. printf("begin print\n");
5. printf("a=%d, b=%.3f, str=%s\n", a, b, str);
6. ...

从printf的使用状况来看,咱们不难发现一个规律,就是不管其可变的参数有多少个,printf的第一个参数老是一个字符串。而正是这第一个参数,使得它能够确认后面还有有多少个参数尾随。而尾随的每一个参数占用的栈空间大小又是经过第一个格式字符串肯定的。然而printf究竟是怎样取第一个参数后面的参数值的呢,请看以下代码 函数

2. printf 函数的实现 spa

01. //acenv.h
02. typedef char *va_list;
03.  
04. #define  _AUPBND        (sizeof (acpi_native_int) - 1)
05. #define  _ADNBND        (sizeof (acpi_native_int) - 1)
06.  
07. #define _bnd(X, bnd)    (((sizeof (X)) + (bnd)) & (~(bnd)))
08. #define va_arg(ap, T)   (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
09. #define va_end(ap)      (void) 0
10. #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
11.  
12. //start.c
13. static char sprint_buf[1024];
14. int printf(char *fmt, ...)
15. {
16. va_list args;
17. int n;
18. va_start(args, fmt);
19. n = vsprintf(sprint_buf, fmt, args);
20. va_end(args);
21. write(stdout, sprint_buf, n);
22. return n;
23. }
24.  
25. //unistd.h
26. static inline long write(int fd, const char *buf, off_t count)
27. {
28. return sys_write(fd, buf, count);
29. }

3. 分析 内存

从上面的代码来看,printf彷佛并不复杂,它经过一个宏va_start把全部的可变参数放到了由args指向的一块内存中,而后再调用vsprintf. 真正的参数个数以及格式的肯定是在vsprintf搞定的了。因为vsprintf的代码比较复杂,也不是咱们这里要讨论的重点,因此下面就再也不列出了。咱们这里要讨论的重点是va_start(ap, A)宏的实现,它对定位从参数A后面的参数有重大的制导意义。如今把 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 的含义解释一下以下: ci

1. va_start(ap, A)
2. {
3. char *ap =  ((char *)(&A)) + sizeof(A)并int类型大小地址对齐
4. }

在printf的va_start(args, fmt)中,fmt的类型为char *, 所以对于一个32为系统 sizeof(char *) = 4, 若是int大小也是32,则va_start(args, fmt);至关于 char *args = (char *)(&fmt) + 4; 此时args的值正好为fmt后第一个参数的地址。对于以下的可变参数函数 字符串

1. void fun(double d,...)
2. {
3. va_list args;
4. int n;
5. va_start(args, d);
6. }

则 va_start(args, d);至关于 编译器

1. char *args = (char *)&d + sizeof(double);

此时args正好指向d后面的第一个参数。 it

可变参数函数的实现与函数调用的栈结构有关,正常状况下c/c++的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最早入栈。对于函数 编译

1. void fun(int a, int b, int c)
2. {
3. int d;
4. ...
5. }

其栈结构为 table

1. 0x1ffc-->d
2. 0x2000-->a
3. 0x2004-->b
4. 0x2008-->c

对于任何编译器,每一个栈单元的大小都是sizeof(int), 而函数的每一个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是

1. 0x1ffc-->a  (4字节)
2. 0x2000-->b  (4字节)
3. 0x2004-->c  (8字节)
4. 0x200c-->d  (4字节)

对于函数void fun1(char a, int b, double c, short d)

若是知道了参数a的地址,则要取后续参数的值则能够经过a的地址计算a后面参数的地址,而后取对应的值,然后面参数的个数能够直接由变量a指定,固然也能够像printf同样根据第一个参数中的%模式个数来决定后续参数的个数和类型。若是参数的个数由第一个参数a直接决定,则后续参数的类型若是没有变化而且是已知的,则咱们能够这样来取后续参数, 假定后续参数的类型都是double;

1. void fun1(int num, ...)
2. {
3. double *p = (double *)((&num)+1);
4. double Param1 = *p;
5. double Param2 = *(p+1);
6. ...
7. double Paramn  *(p+num);
8. }

若是后续参数的类型是变化并且是未知的,则必须经过一个参数中设定模式来匹配后续参数的个数和类型,就像printf同样,固然咱们能够定义本身的模式,如能够用i表示int参数,d表示double参数,为了简单,咱们用一个字符表示一个参数,并由该字符的名称决定参数的类型而字符的出现的顺序也表示后续参数的顺序。 咱们能够这样定义字符和参数类型的映射表,

1. i---int
2. s---signed short
3. l---long
4. c---char

"ild"模式用于表示后续有三个参数,按顺序分别为int, long, double类型的三个参数那么这样咱们能够定义本身版本的printf 以下

01. void printf(char *fmt, ...)
02. {
03. char s[80] = "";
04. int paramCount = strlen(fmt);
05. write(stdout, "paramCount = " , strlen(paramCount = ));
06. itoa(paramCount,s,10);
07. write(stdout, s, strlen(s));
08. char *p = (char *)(&fmt) + sizeof(char *);
09. int *pi = (int *)p;
10. for (int i=0; i<paramCount; i++)
11. {
12. char line[80] = "";
13. strcpy(line, "param");
14. itoa(i+1, s, 10);
15. strcat(line, s);
16. strcat(line, "=");
17. switch(fmt[i])
18. {
19. case 'i':
20. case 's':
21. itoa((*pi),s,10);
22. strcat(line, s);
23. pi++;
24. break;
25. case 'c':
26. {
27. int len = strlen(line);
28. line[len] = (char)(*pi);
29. line[len+1] = '\0';
30. }
31. break;
32. case 'l':
33. ltoa((*(long *)pi),s,10);
34. strcat(line, s);
35. pi++;
36. break;
37. default:
38. break;
39. }
40. }
41. }

也能够这样定义咱们的Max函数,它返回多个输入整型参数的最大值

01. int Max(int n, ...)
02. {
03. int *p = &n + 1;
04. int ret = *p;
05. for (int i=0; i<n; i++)
06. {
07. if (ret < *(p + i))
08. ret = *(p + i);
09. }
10. return ret;
11. }

能够这样调用, 后续参数的个数由第一个参数指定

1. int m = Max(3, 45, 12, 56);
2. int m = Max(1, 3);
3. int m = Max(2, 23, 45);
4.  
5. int first = 34, second = 45, third=5;
6. int m = Max(5, first, second, third, 100, 4);

结论

对于可变参数函数的调用有一点须要注意,实际的可变参数的个数必须比前面模式指定的个数要多,或者不小于, 也即后续参数多一点没关系,但不能少, 若是少了则会访问到函数参数之外的堆栈区域,这可能会把程序搞崩掉。前面模式的类型和后面实际参数的类型不匹配也有可能形成把程序搞崩溃,只要模式指定的数据长度大于后续参数长度,则这种状况就会发生。如:

1. printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);

参数1,2,3,4的默认类型为整型,而模式指定的须要为double型,其数据长度比int大,这种状况就有可能访问函数参数堆栈之外的区域,从而形成危险。可是printf("%d, %d, %d", 1.0, 20., 3.0);这种状况虽然结果可能不正确,可是确不会形成灾难性后果。由于实际指定的参数长度比要求的参数长度长,堆栈不会越界。

相关文章
相关标签/搜索