一个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 语言中的可变参数问题。可以看出,分析问题-定位问题-解决问题是一连串的思考过程,把这个过程走一遍之后,理解才会更深刻。

我还有另外一个感受:如果我没有写公众号,就不会写这篇文章;如果不写这篇文章,就不会研究的这么较真。也许在中间的某个步骤,我就会偷懒对自己说:理解到这个层次就差不多了,不用继续深究了。所以说以文章的形式来把自己的思考过程进行输出,是技术提升是非常有好处的,也强烈建议各位小伙伴尝试一下这么做。

而且,如果这些思考过程能得到你们的认可,那么我就会更有动力来总结、输出文章。因此,如果这篇总结对你能有一丝丝的帮助,请转发、分享给你的技术朋友,在此表示衷心的感谢!

【原创声明】

作者:道哥

声明: 本文由入驻OFweek维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存