C语言结构体里的成员数组和指针

在微博上看到一篇文章,提到了结构体中的指针相关知识点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

struct {
int len;
char s[0];
};

struct foo {
struct * a;
};

int main()
{
struct foo f = {0};
if (f.a->s) {
printf(f.a->s);
}
}

考虑程序能否正常运行。
实际上程序会在运行的时候崩溃,调试发现会在第14行printf处崩溃。

这段代码看起来很怪异,将f赋值为了0,那么f.a能够正常访问吗?f.a->s又指向了哪儿呢?

我们可以换个思路,输出f.a->s的内存地址,考虑printf("%xn", f.a->s);输出的是什么。结果是输出4
所以实际上上述崩溃代码中我们是在访问0x04的内存空间,不属于程序自己的内存地址(属于系统),肯定会崩溃。

为什么是指向0x04的内存空间呢
我们知道开始struct foo f = {0};这句话将结构体的内存地址指向了0x00,然后通过计算偏移量,跳过len这个int型变量4个字节,所以s指向的0x04了。

由此我们可以得出结论:
实际上结构体中的成员变量对于编译器来说,只不过是一个偏移量,访问结构体中的成员就是访问结构体指针后移对应偏移量的内存空间
可以通过一个简单的例子验证(考虑到结构体对齐)

1
2
3
4
5
6
7
8
9
10
struct test{
int i;
short c;
char *p;
};

int main(){
struct test *pt=NULL;
return 0;
}

通过调试我们可以看出pt->i地址是0x0,pt->c地址是4,pt->p地址是8(结构体对齐,将short填充成了4个字节)

指针和数组的区别

如果把上述代码中的char s[0]改成char* s,再运行程序,到达if条件的时候,程序因为Cannot access memory就直接挂掉了。说明指针和数组还是有差别的。

看一下汇编代码,用GDB查看后发现:

  • 对于char s[0]来说,汇编代码用了lea指令,lea 0x04(%rax), %rdx
  • 对于char *s来说,汇编代码用了mov指令,mov 0x04(%rax), %rdx
    lea全称load effective address,是把地址放进去,而mov则是把地址里的内容放进去。所以,就crash了。

从这里,我们可以看到,访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的)

换句话说,对于数组char s[10]来说,数组名 s 和 &s 都是一样的(不信你可以自己写个程序试试)。在我们这个例子中,也就是说,都表示了偏移后的地址。

所以对于如下例子,也可以运行不会crash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct test{
int i;
short c;
char *p;
char s[10];
};

int main(){
struct test *pt=NULL;
printf("&s = %xn", pt->s);
printf("&i = %xn", &pt->i); //因为操作符优先级,我没有写成&(pt->i)
printf("&c = %xn", &pt->c);
printf("&p = %xn", &pt->p);
return 0;
}