一个printf(结构体指针)引发的血案

道哥分享
关注

编译、测试,打印结果如下:

打印结果符合预期!也就是说分成两条打印语句是可以正确读取到目标地址里的 int 型数据的,但是在一条语句里就不行!

其实此时,可以判断出大概是 printf 语句的原因了。从现象上看,似乎是 printf 语句在执行过程中打印第一个数字之后,影响到了指针 p 的值,但是具体是怎么影响的说不清楚,而且它是系统里的库函数,肯定不能改变 p 的值。

于是在 google 中搜索关键字:"glibc printf bug",你还别说,真的搜索到很多相关资料,但是浏览了一下,没有与我们的测试代码类似的情况,还得继续思考。

3. 一步步分析问题本质原因3.1 打印一个最简单的字符串

既然是因为在 printf 语句中打印 2 个数据才出现问题,那么我就把问题简化,用一个最简单的字符串来测试,代码如下:

char aa[] = "abcd";char *pc = aa;printf("%d, %d ", *pc, *pc);

编译、执行,打印结果为:"97, 97",非常正确!这就说明 printf 语句在执行时没有改变指针变量的指向地址。

3.2 打印一个结构体变量

既然在字符串上测试没有问题,那么问题就出在结构体类型上了。那就继续用结构体变量来测试,因为上面的测试代码是结构体变量的数组,现在我们把数组的影响去掉,只对单独的一个结构体变量进行测试:

Student s = {1, "a"};
printf("%d ", s);
printf("%d, %d ", s, s);

注意:这里的 s 是一个变量,不是数组了,所以打印时就不需要用 * 操作符了。编译、执行,输出结果:

输出结果与之前的错误一样,至此可以得出结论:问题的原因至少与数组是没有关系的!

现在测试的结构体中有 2 个变量:age 和 name,我们继续简化,只保留 int 型数据,这样更容易简化问题。

3.3 测试更简单的结构体变量

测试代码如下:

typedef struct _A{   int a;   int b;   int c;}A;
int main(){    A a = {10, 20, 30};    printf("%d %d %d ", a, a, a);}

编译、执行,打印结果为:10 20 30,把 3 个成员变量的值都打印出来了,太诡异了!好像是在内存中,从第一个成员变量开始,自动递增然后获取 int 型数据。

于是我就把后面的两个参数 a 去掉,测试如下代码:

A a = {10, 20, 30};printf("%d %d %d ", a);

编译、执行,打印结果仍然为:10 20 30!这个时候我快疯掉了,主要是时间太晚了,我不太喜欢熬夜。

于是大脑开始偷懒,再次向 google 寻求帮助,还真的找到这个网页:https://stackoverflow.com/questions/26525394/use-printfs-to-print-a-struct-the-structs-first-variable-type-is-char。感兴趣的小伙伴可以打开浏览一下,其中有下面这两段话说明了重点:

一句话总结:用 printf 语句来打印结构体类型的变量,结果是 undefined behavior!什么是未定义行为,就是说发生任何状况都是可能的,这个就要看编译器的实现方式了。

看来,我已经找到问题的原因了:原来是因为我的知识不够扎实,不知道打印结构体变量是未定义行为。

补充一点心得:

我们在写程序的时候,因为脑袋中掌握的大部分知识都是正确的,因此编写的代码大部分也都是与预期符合的,不可能故意去写一些稀奇古怪的代码。就比如打印结构体信息,一般正常的思路都是把结构体里面的成员变量,按照对应的数据类型来打印输出。但是偶尔也会犯低级错误,就像这次遇到的问题一样:直接打印一个结构体变量。因为发生错误了,所以才了解到原来直接打印结构体变量,是一个未定义行为。当然了,这也是一个获取知识的途径。

追查到这里,似乎可以结束了。但是我还是有点不死心,既然是未定义的行为,那么为什么每次打印输出的结果都错的这么一致呢?既然是由编译器的实现决定的,那么我使用的这个 gcc 版本内部是怎么来打印结构体变量的呢?

于是我继续往下查...

3.4 继续打印结构体变量

刚才的结构体 A 中的成员都是 int 型,每个 int 数据在内存中占据 4 个字节,所以刚才打印出的数据恰好是跨过 4 个字节。如果改成字符串型,打印时是否也会跨过4个字节,于是把测试代码改成下面这样:

typedef struct _B{   int a;   char b[12];}B;
int main(){    B  b = {10, "abcdefgh"};    printf("%d %c %c ", b);}

编译、执行,打印结果如下:

果然如此:字符 a 与数字 10 之间跨过 4 个直接,字符 e 与 a 之间也是跨过 4 个字节。那就说明 printf 语句在执行时可能是按照 int 型的数据大小(4个字节)为单位,来跨越内存空间,然后再按照百分号%后面的字符来读取内存地址里的数据。

那就来验证这个想法是否正确,测试代码如下:

Student s = {1, "aaa"};char *pTmp = &s;for (int i = 0;i < sizeof(Student); i++){   printf("%x ", *(pTmp + i));}
printf("");printf("%d, %x ", s);

编译、执行,打印结果为:

输出结果确实如此:数字 1 之后的内存中存放的是 3 个字符 'a',第二个打印数据格式是 %x,所以就按照整型数据来读取,于是得到十六进制的616161。

至此,我们也知道了 gcc 这个版本中,是如何来操作这个 “undefined behavior” 的。但是事情好像还没有结束,我们都知道:在调用系统中的 printf 语句时,传入的参数个数和类型不是固定的,那么 printf 中是如何来动态侦测参数的个数和类型的呢?

四、C语言中的可变参数

在 C 语言中实现可变参数需要用到这下面这几个数据类型和函数(其实是宏定义):

va_listva_startva_argva_end

处理动态参数的过程是下面这 4 个步骤:

定义一个变量 va_list arg;调用 va_start 来初始化 arg 变量,传入的第二个参数是可变参数(三个点)前面的那个变量;使用 va_arg 函数提取可变参数:循环从 arg 中提取每一个变量,最后一个参数用来指定提取的数据类型。比如:如果格式化字符串是 %d,那么就从可变参数中提取一个 int 型的数据,如果格式化字符串是 %c,就从可变参数中提取一个 char 型数据;数据处理结束后,使用 va_end 来释放 arg 变量。

文字表达起来好像有点抽象、复杂,先看一下下面的 3 个示例,然后再回头看一下上面这 4 个步骤,就容易理解了。

1. 利用可变参数的三个函数示例示例1:参数类型是 int,但是参数个数不固定#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <stdarg.h>
void my_printf_int(int num,...){    int i, val;    va_list arg;    va_start(arg, num);    for(i = 0; i < num; i++)    {        val = va_arg(arg, int);        printf("%d ", val);    }    va_end(arg);    printf("");}
int main(){    int a = 1, b = 2, c = 3;    my_printf_int(3, a, b, c);}

编译、执行,打印结果如下:

示例2:参数类型是 float,但是参数个数不固定#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <stdarg.h>
void my_printf_float (int n, ...){  int i;  double val;  va_list vl;  va_start(vl,n);  for (i = 0; i < n; i++)  {    val = va_arg(vl, double);    printf ("%.2f ",val);  }  va_end(vl);  printf ("");}
int main(){    float f1 = 3.14159, f2 = 2.71828, f3 = 1.41421;    my_printf_float (3, f1, f2, f3);}

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

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

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