一个printf(结构体指针)引发的血案
一、前言1. 为什么写这篇文章在上周六,我在公众号里发了一篇文章:C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻,以直白的语言、一目了然的图片来解释指针的底层逻辑,有一位小伙伴对文中的代码进行测试,发现一个比较奇怪的问题
一、前言
1. 为什么写这篇文章
在上周六,我在公众号里发了一篇文章:C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻,以直白的语言、一目了然的图片来解释指针的底层逻辑,有一位小伙伴对文中的代码进行测试,发现一个比较奇怪的问题。我把发来的测试代码进行验证,思考好久也无法解释为什么会出现那么奇怪的打印结果。
为了整理思路,我到阳台抽根烟。晚上的风很大,一根烟我抽了一半,风抽了一半,可能风也有自己的烦恼。后来一想,烟是我买的,为什么让风来抽?于是我就开始抽风!不对,开始回房间继续抽代码,我就不信,这么简单的 printf 语句,怎么就搞不定?!
于是就有了这篇文章。
2. 你能得到什么收获函数参数的传递机制;可变参数的实现原理(va_list);printf 函数的实现机制;面对问题时的分析思路。
友情提醒:文章的前面大部分内容都是在记录思考问题、解决问题的思路,如果你对这个过程不感兴趣,可以直接跳到最后面的第四部分,用图片清晰的解释了可变参数的实现原理,看过一次之后,保管你能深刻记住。
3. 我的测试环境3.1 操作系统
每个人的电脑环境都是不一样的,包括操作系统、编译器、编译器的版本,也许任何一个小差别都会导致一些奇奇怪怪的的现象。不过大部分人都是使用 Windows 系统下的 VS 集成开发环境,或者 Linux 下的 gcc 命令行窗口来测试。
我一般都是使用 Ubuntu16.04-64 系统来测试代码,本文中的所有代码都是在这个平台上测试的。如果你用 VS 开发环境中的 VC 编译器,可能在某些细节上与我的测试结果又出入,但是问题也不大,遇到问题再分析,毕竟解决问题也是提升自己能力的最快途径。
3.2 编译器
我使用的编译器是 Ubuntu16.04-64 系统自带的版本,显示如下:
另外,我安装的是 64 位系统,为了编译 32 位的可执行程序,我在编译指令中添加了 -m 选项,编译指令如下:
gcc -m32 main.c -o main
使用 file main 命令来查一下编译得到的可执行文件:
所以,在测试时如果输出结果与预期有一些出入,先检查一下编译器。C 语言本质上都是一些标准,每家的编译器都是标准的实现者,只要结果满足标准即可,至于实现的过程、代码执行的效率就各显神通了。
二、问题导入
1. 网友测试代码#include <unistd.h>#include <stdio.h>#include <stdlib.h>
typedef struct { int age; char name[8];} Student;
int main(){ Student s[3] = {{1, "a"}, {2, "b"}, {3, "c"}}; Student *p = &(s[0]); printf("%d, %d ", *s, *p);}
2. 期望结果
根据上篇文章的讨论,我们知道:
s 是一个包含 3 个元素数组,每个元素的类型是结构体 Student;p 是一个指针,它指向变量s,也就是说指针 p 中保存的是变量 s 的地址,因为数组名就表示该数组的首地址。
既然 s 也是一个地址,它也代表了这个数组中第一个元素的首地址。第一个元素类型是结构体,结构体中第一个变量是 int 型,因此 s 所代表的那个位置是一个 int 型数据,对应到示例代码中就是数字 1。因此 printf 语句中希望直接把这个地址处的数据当做一个 int 型数据打印出来,期望的打印结果是:1, 1。
这样的分析过程好像是没有什么问题的。
3. 实际打印结果
我们来编译程序,输出警告信息:
警告信息说:printf 语句需要 int 型数据,但是传递了一个 Student 结构体类型,我们先不用理会这个警告,因为我们就是想通过指针来访问这个地址里的数据。
执行程序,看到实际打印结果是:1, 97,很遗憾,与我们的期望不一致!
三、分析问题的思路
1. 打印内存模型
可以从打印结果看,第一个输出的数字是 1,与预期符合;第二个输出 97,很明显是字符 'a' 的 ASCII 码值,但是 p 怎么会指到 name 变量的地址里呢?
首先确认 3 个事情:
结构体 Student 占据的内存大小是多少?数组 s 里的内存么模型是怎样的?s 与 指针变量 p 的值是否正确?
把代码改为如下:
Student s[3] = {{1, "a"}, {2, "b"}, {3, "c"}};Student *p = s;
printf("sizeof Student = %d ", sizeof(Student));
printf("print each byte in s: ");char *pTmp = p;for (int i = 0; i < 3 * sizeof(Student); i++){ if (0 == i % sizeof(Student)) printf(""); printf("%x ", *(pTmp + i));}printf("");
printf("print value of s and p ");printf("s = 0x%x, p = 0x%x ", s, p);
printf("%d, %d ", *s, *p);
我们先画一下数组 s 预期的内存模型,如下:
编译、测试,打印结果如下:
从打印结果看:
结构体 Student 占据 12 个字节,符合预期。数组 s 的内存模型也是符合预期的,一共占据 36 个字节。s 与 p 都代表一个地址,打印结果它俩相同,也是符合预期的。
那就见鬼了:既然 s 与 p 代表同一个内存地址,但是为什么用 *p 读取 int 型数据时,得到的却是字符 'a' 的值呢?
2. 分开打印信息
既然第一个 *s 打印结果是正确的,那么就把这个两个数据分开来打印,测试代码如下:
Student s[3] = {{1, "a"}, {2, "b"}, {3, "c"}};Student *p = s;
printf("%d ", *s);printf("%d ", *p);