一个printf(结构体指针)引发的血案
编译、执行,打印结果如下:
示例3:参数类型是 char*,但是参数个数不固定#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <stdarg.h>
void my_printf_string(char *first, ...){ char *str = first; va_list arg; va_start(arg, first); do { printf("%s ", str); str = va_arg(arg, char*); } while (str != NULL ); va_end(arg); printf("");}
int main(){ char *a = "aaa", *b = "bbb", *c = "ccc"; my_printf_string(a, b, c, NULL);}
编译、执行,打印结果如下:
注意:以上这3个示例中,虽然传入的参数个数是不固定的,但是参数的类型都必须是一样的!
另外,处理函数中必须能够知道传入的参数有多少个,处理 int 和 float 的函数是通过第一个参数来判断的,处理 char* 的函数是通过最后一个可变参数NULL来判断的。
2. 可变参数的原理2.1 可变参数的几个宏定义typedef char * va_list;
#define va_start _crt_va_start#define va_arg _crt_va_arg #define va_end _crt_va_end
#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_end(ap) ( ap = (va_list)0 )
注意:va_list 就是一个 char* 型指针。
2.2 可变参数的处理过程
我们以刚才的示例 my_printf_int 函数为例,重新贴一下:
void my_printf_int(int num, ...) // step1{ int i, val; va_list arg; va_start(arg, num); // step2 for(i = 0; i < num; i++) { val = va_arg(arg, int); // step3 printf("%d ", val); } va_end(arg); // step4 printf("");}
int main(){ int a = 1, b = 2, c = 3; my_printf_int(3, a, b, c);}
Step1: 函数调用时
C语言中函数调用时,参数是从右到左、逐个压入到栈中的,因此在进入 my_printf_int 的函数体中时,栈中的布局如下:
Step2: 执行 va_start
va_start(arg, num);
把上面这语句,带入下面这宏定义:
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
宏扩展之后得到:
arg = (char *)num + sizeof(num);
结合下面的图来分析一下:首先通过 _ADDRESSOF 得到 num 的地址 0x01020300,然后强转成 char* 类型,再然后加上 num 占据的字节数(4个字节),得到地址 0x01020304,最后把这个地址赋值给 arg,因此 arg 这个指针就指向了栈中数字 1 的那个地址,也就是第一个参数,如下图所示:
Step3: 执行 va_arg
val = va_arg(arg, int);
把上面这语句,带入下面这宏定义:
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
宏扩展之后得到:
val = ( *(int *)((arg += _INTSIZEOF(int)) - _INTSIZEOF(int)) )
结合下面的图来分析一下:先把 arg 自增 int 型数据的大小(4个字节),使得 arg = 0x01020308;然后再把这个地址(0x01020308)减去4个字节,得到的地址(0x01020304)里的这个值,强转成 int 型,赋值给 val,如下图所示:
简单理解,其实也就是:得到当前 arg 指向的 int 数据,然后把 arg 指向位于高地址处的下一个参数位置。
va_arg 可以反复调用,直到获取栈中所有的函数传入的参数。
Step4: 执行 va_end
va_end(arg);
把上面这语句,带入下面这宏定义:
#define _crt_va_end(ap) ( ap = (va_list)0 )
宏扩展之后得到:
arg = (char *)0;
这就好理解了,直接把指针 arg 设置为空。因为栈中的所有动态参数被提取后,arg 的值为 0x01020310(最后一个参数的上一个地址),如果不设置为 NULL 的话,下面使用的话就得到未知的结果,为了防止误操作,需要设置为NULL。
3. printf利用可变参数打印信息
理解了 C 语言中可变参数的处理机制,再来思考 printf 语句的实现机制就很好理解了。
3.1 GNU 中的 printf 代码__printf (const char *format, ...){ va_list arg; int done;
va_start (arg, format); done = vfprintf (stdout, format, arg); va_end (arg);
return done;}
可见,系统库中的 printf 也是这样来处理动态参数的,vfprintf 函数最终会调用系统函数 sys_write,把数据输出到 stdout 设备上(显示器)。vfprintf 函数代码看起来还是有点复杂,不过稍微分析一下就可以得到其中的大概实现思路:
逐个比对格式化字符串中的每一个字符;如果是普通字符就直接输出;如果是格式化字符,就根据指定的数据类型,从可变参数中读取数据,输出显示;
以上只是很粗略的思路,实现细节肯定复杂的多,需要考虑各种细节问题。下面是 2 个简单的示例:
void my_printf_format_v1(char *fmt, ...){ va_list arg; int d; char c, *s;
va_start(arg, fmt); while (*fmt) { switch (*fmt) { case 's': s = va_arg(arg, char *); printf("%s", s); break;
case 'd': d = va_arg(arg, int); printf("%d", d); break;
case 'c': c = (char) va_arg(arg, int); printf(" %c", c); break; default: if ('%' != *fmt || ('s' != *(fmt + 1) && 'd' != *(fmt + 1) && 'c' != *(fmt + 1))) printf("%c", *fmt); break; } fmt++; } va_end(arg);}
int main(){ my_printf_format_v1("age = %d, name = %s, num = %d ", 20, "zhangsan", 98);}
编译、执行,输出结果:
完美!但是再测试下面代码(把格式化字符串最后面的 num 改成 score):
my_printf_format_v1("age = %d, name = %s, score = %d ", 20, "zhangsan", 98);
编译、执行,输出结果:
因为普通字符串 score 中的字符 s 被第一个 case 捕获到了,所以发生错误。稍微改进一下:
void my_printf_format_v2(char *fmt, ...){ va_list arg; int d; char c, lastC = '', *s;
va_start(arg, fmt); while (*fmt) { switch (*fmt) { case 's': if ('%' == lastC) { s = va_arg(arg, char *); printf("%s", s); } else { printf("%c", *fmt); } break;
case 'd': if ('%' == lastC) { d = va_arg(arg, int); printf("%d", d); } else { printf("%c", *fmt); } break;
case 'c': if ('%' == lastC) { c = (char) va_arg(arg, int); printf(" %c", c); } else { printf("%c", *fmt); } break; default: if ('%' != *fmt || ('s' != *(fmt + 1) && 'd' != *(fmt + 1) && 'c' != *(fmt + 1))) printf("%c", *fmt); break; } lastC = *fmt; fmt++; } va_end(arg);}
int main(){ my_printf_format_v2("age = %d, name = %s, score = %d ", 20, "zhangsan", 98);}
编译、执行,打印结果:
五、总结
我们来复盘一下上面的分析过程,开头的第一个代码本意是测试关于指针的,结果到最后一直分析到 C 语言中的可变参数问题。可以看出,分析问题-定位问题-解决问题是一连串的思考过程,把这个过程走一遍之后,理解才会更深刻。
我还有另外一个感受:如果我没有写公众号,就不会写这篇文章;如果不写这篇文章,就不会研究的这么较真。也许在中间的某个步骤,我就会偷懒对自己说:理解到这个层次就差不多了,不用继续深究了。所以说以文章的形式来把自己的思考过程进行输出,是技术提升是非常有好处的,也强烈建议各位小伙伴尝试一下这么做。
而且,如果这些思考过程能得到你们的认可,那么我就会更有动力来总结、输出文章。因此,如果这篇总结对你能有一丝丝的帮助,请转发、分享给你的技术朋友,在此表示衷心的感谢!
【原创声明】
作者:道哥
最新活动更多
-
11月28日立即报名>>> 2024工程师系列—工业电子技术在线会议
-
12月19日立即报名>> 【线下会议】OFweek 2024(第九届)物联网产业大会
-
即日-12.26火热报名中>> OFweek2024中国智造CIO在线峰会
-
即日-2025.8.1立即下载>> 《2024智能制造产业高端化、智能化、绿色化发展蓝皮书》
-
精彩回顾立即查看>> 2024 智能家居出海论坛
-
精彩回顾立即查看>> 【在线会议】多物理场仿真助跑新能源汽车
推荐专题
发表评论
请输入评论内容...
请输入评论/评论长度6~500个字
暂无评论
暂无评论