C语言结构体里的成员数组和指针——读后感

我也是在偶然的情况下看到了这个问题,乍一看觉得貌似没什么意思。但是稍微瞟了一眼代码,觉得还是有点意思,然后接着看完下面的回复评论,觉得有必要总结整理一下,对理解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++上会在14行的printf处挂掉。

如果把14行的printf语句改成:

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

程序就不会crash了,程序输出:4。 实际是访问0×4的内存地址。

有些人有一些问题比如:

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

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

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

当然也有了相应的解答

1)结构体中的成员

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了的地址。于是不管结构体的实例是什么,访问其成员其实就是加成员的偏移量。
2) 指针和数组的区别

把源代码中的struct str结构体中的char s[0];改成char *s;则会在13行执行if条件的时候,程序因为Cannot access memory而直接。为什么声明成char s[0],程序会在14行挂掉,而声明成char *s,程序会在13行挂掉呢?那么char *s 和 char s[0]有什么差别呢?
使用汇编代码查看后发现:
对于char s[0]来说,汇编代码用了lea指令,lea   0×04(%rax),   %rdx
对于char*s来说,汇编代码用了mov指令,mov 0×04(%rax),   %rdx
lea全称load effective address,是把地址放进去,而mov则是把地址里的内容放进去。所以,就挂掉了。
访问成员数组名得到的是数组的相对地址,而访问成员指针是相对地址里的内容。

3)0长度数组

0长度的数组在ISO C和C++的规格说明书中是不允许的。所以在VC++2012下编译你会得到一个警告:“arning C4200: 使用了非标准扩展 : 结构/联合中的零大小数组”。而GCC为了预先支持C99,没有任何警告。关于GCC对于这个事,有人给了一个例子。

#include <stdlib.h>
#include <string.h>
 
struct line {
   int length;
   char contents[0]; // C99的玩法是:char contents[]; 没有指定数组长度
};
 
int main(){
    int this_length=10;
    struct line *thisline = (struct line *)
                     malloc (sizeof (struct line) + this_length);
    thisline->length = this_length;
    memset(thisline->contents, 'a', this_length);
    return 0;
}
上面这段代码的意思是:想分配一个不定长的数组,先定义一个结构体,其中有两个成员,一个是length,代表数组的长度,一个是contents,代码数组的内容。后面代码里的 this_length(长度是10)代表想分配的数据的长度。这种方法英文叫:Flexible Array,中文翻译叫:柔性数组。

如果sizeof(char[0])或是 sizeof(int[0]) 之类的零长度数组,会发现sizeof返回了0,也就是说,零长度的数组是存在于结构体内的,但是不占结构体的size。可以简单的理解为一个没有内容的占位标识,直到给结构体分配了内存,这个占位标识才变成了一个有长度的数组。
如果把contents声明成一个指针,然后为它再分配一下内存不也可以么?就像下面:

struct line {
   int length;
   char *contents;
};
 
int main(){
    int this_length=10;
    struct line *thisline = (struct line *)malloc (sizeof (struct line));
    thisline->contents = (char*) malloc( sizeof(char) * this_length );
    thisline->length = this_length;
    memset(thisline->contents, 'a', this_length);
    return 0;
}
答案肯定是可以,这也是普遍的编程方式,代码很清晰,也很容易理解。

但是使用0长度数组也是有好处的:

第一个是方便内存释放。如果代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个是这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。

总结提问的人觉得这是C语言的坑,而有的回复的人解释这不是坑。我个人认为坑就是有歧义的,易于迷惑的。像这种需要专家C程序员来公开解惑的特性就叫坑。当然假如这个不算坑,只能说这样的写法很少见喽~~~但又反过来想想,C/C++不就是既简单又复杂的语言么,像这样的问题不都挺有意思的么,总是会吸引人们去关注、去解释、去学习~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值