C语言结构体里的成员数组和指针(C语言的一个隐晦角落

C语言结构体里的成员数组和指针(关于零数组)

单看这文章的标题,你可能会觉得好像没什么意思。你先别下这个结论,相信这篇文章会对你理解C语言有帮助。这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接。微博截图如下。我觉得好多人对这段代码的理解还不够深入,所以写下了这篇文章。

#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语句改成:

printf("%x\n",f.a->s);

你会看到程序不crash了。程序输出:4。 这下你知道了,访问0×4的内存地址,不crash才怪。于是,你一定会有如下的问题:

1)为什么不是 13行if语句出错?f.a被初始化为空了嘛,用空指针访问成员变量为什么不crash?

2)为什么会访问到了0×4的地址?靠,4是怎么出来的?

3)代码中的第4行,char s[0] 是个什么东西?零长度的数组?为什么要这样玩?

让我们从基础开始一点一点地来解释C语言中这些诡异的问题。

·结构体中的成员

首先,我们需要知道——所谓变量,其实是内存地址的一个抽像名字罢了。在静态编译的程序中,所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的,只知道地址。

所以有了——栈内存区,堆内存区,静态内存区,常量内存区,我们代码中的所有变量都会被编译器预先放到这些内存区中。

有了上面这个基础,我们来看一下结构体中的成员的地址是什么?我们先简单化一下代码:

struct test{

int i;

char *p;

};

上面代码中,test结构中i和p指针,在C的编译器中保存的是相对地址——也就是说,他们的地址是相对于struct test的实例的。

如果我们有这样的代码:

struct test t;

我们用gdb跟进去,对于实例t,我们可以看到:

# t实例中的p就是一个野指针

(gdb) p t

$1 = {i = 0, c = 0’\000′, d = 0 ‘\000’, p = 0x4003e0 "1\355I\211\…"}

# 输出t的地址

(gdb) p &t

$2 = (struct test *)0x7fffffffe5f0

#输出(t.i)的地址

(gdb) p &(t.i)

$3 = (char **)0x7fffffffe5f0

#输出(t.p)的地址

(gdb) p &(t.p)

$4 = (char **)0x7fffffffe5f4

我们可以看到,t.i的地址和t的地址是一样的,t.p的址址相对于t的地址多了个4。说白了,t.i 其实就是(&t +0×0),t.p 的其实就是 (&t +0×4)。0×0和0×4这个偏移地址就是成员i和p在编译时就被编译器给hard code了的地址。于是,你就知道,不管结构体的实例是什么——访问其成员其实就是加成员的偏移量。

下面我们来做个实验:

struct test{int i;short c;char *p;}; int main(){struct test *pt=NULL;return 0;}

编译后,我们用gdb调试一下,当初始化pt后,我们看看如下的调试:(我们可以看到就算是pt为NULL,访问其中的成员时,其实就是在访问相对于pt的内址)

(gdb) p pt

$1 = (struct test *)0x0

(gdb) p pt->i

Cannot access memoryat address 0x0

(gdb) p pt->c

Cannot access memoryat address 0x4

(gdb) p pt->p

Cannot access memoryat address 0x8

注意:上面的pt->p的偏移之所以是0×8而不是0×6,是因为内存对齐了(我在64位系统上)。

好了,现在你知道为什么原题中会访问到了0×4的地址了吧,因为是相对地址。

相对地址有很好多处,其可以玩出一些有意思的编程技巧,比如把C搞出面向对象式的感觉来。

·指针和数组的差别

有了上面的基础后,你把源代码中的structstr结构体中的chars[0];改成char*s;试试看,你会发现,在13行if条件的时候,程序因为Cannot accessmemory就直接挂掉了。为什么声明成char s[0],程序会在14行挂掉,而声明成char *s,程序会在13行挂掉呢?那么char*s 和char s[0]有什么差别呢?

在说明这个事之前,有必要看一下汇编代码,用GDB查看后发现:

·对于char s[0]来说,汇编代码用了lea指令,lea 0×04(%rax), %rdx

得意时应善待他人,因为你失意时会需要他们

C语言结构体里的成员数组和指针(C语言的一个隐晦角落

相关文章:

你感兴趣的文章:

标签云: