单看这文章的标题,你可能会觉得好像没什么意思。你先别下这个结论,相信这篇文章会对你理解C语言有帮助。这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接。微博截图如下。我觉得好多人对这段代码的理解还不够深入,所以写下了这篇文章。
为了方便你把代码copy过去编译和调试,我把代码列在下面:
#include <stdio.h> struct str{ int len; char s[0]; }; struct foo { struct str *a; }; int main(int argc, char** argv) { struct foo f={0}; if (f.a->s) { printf( f.a->s); } return 0; }
你编译一下上面的代码,在VC++和GCC下都会在14行的printf处crash掉你的程序。@Laruence 说这个是个经典的坑,我觉得这怎么会是经典的坑呢?上面这代码,你一定会问,为什么if语句判断的不是f.a?而是f.a里面的数组?写这样代码的人脑子里在想什么?还是用这样的代码来玩票?不管怎么样,看过原微博的回复,我个人觉得大家主要还是对C语言理解不深,如果这算坑的话,那么全都是坑。
接下来,你调试一下,或是你把14行的printf语句改成:
1
|
printf ( "%x
" , f.a->s); |
你会看到程序不crash了。程序输出:4。 这下你知道了,访问0x4的内存地址,不crash才怪。于是,你一定会有如下的问题:
1)为什么不是 13行if语句出错?f.a被初始化为空了嘛,用空指针访问成员变量为什么不crash?
2)为什么会访问到了0x4的地址?靠,4是怎么出来的?
3)代码中的第4行,char s[0] 是个什么东西?零长度的数组?为什么要这样玩?
让我们从基础开始一点一点地来解释C语言中这些诡异的问题。
结构体中的成员
首先,我们需要知道——所谓变量,其实是内存地址的一个抽像名字罢了。在静态编译的程序中,所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的,只知道地址。
所以有了——栈内存区,堆内存区,静态内存区,常量内存区,我们代码中的所有变量都会被编译器预先放到这些内存区中。
有了上面这个基础,我们来看一下结构体中的成员的地址是什么?我们先简单化一下代码:
1
2
3
4
|
struct test{ int i; char *p; }; |
上面代码中,test结构中i和p指针,在C的编译器中保存的是相对地址——也就是说,他们的地址是相对于struct test的实例的。如果我们有这样的代码:
1
|
struct test t; |
我们用gdb跟进去,对于实例t,我们可以看到:
# t实例中的p就是一个野指针 (gdb) p t $1 = {i = 0, c = 0 '