这个问题是阿里的一个面试题。当时没有很清楚,答得很差,特地实验看一下运行结果。

在结构体中定义了一个char*指针,与定义一个零元素的char数组有什么区别?

作用

常用来构成缓冲区。比起指针,用空数组有这样的优势:

  • 不需要初始化,数组名直接就是所在的偏移;
  • 不占任何空间,指针需要占用int长度空间,空数组不占任何空间。

    “这个数组不占用任何内存”,意味着这样的结构节省空间;
    “该数组的内存地址就和它后面的元素地址相同”,意味着无需初始化,数组名就是后面元素的地址,直接就能当指针使用。

这样的写法最适合制作动态buffer,因为可以这样分配空间malloc(sizeof(structXXX) + buff_len); 直接就把buffer的结构体和缓冲区一块分配了。用起来也非常方便,因为现在空数组其实变成了buff_len长度的数组了。这样的好处是:

  • 一次分配解决问题,省了不少麻烦。为了防止内存泄露,如果是分两次分配(结构体和缓冲区),那么要是第二次malloc失败了,必须回滚释放第一个分配的结构体。这样带来了编码麻烦。其次,分配了第二个缓冲区以后,如果结构里面用的是指针,还要为这个指针赋值。同样,在free这个buffer的时候,用指针也要两次free。如果用空数组,所有问题一次解决。
  • 小内存的管理是非常困难的,如果用指针,这个buffer的struct部分就是小内存了,在系统内存在多了势必严重影响内存管理的性能。要是用空数组把struct和实际数据缓冲区一次分配大块问题,就没有这个问题。如此看来,用空数组既简化编码,又解决了小内存碎片问题提高了性能。

结构体最后使用0或1长度数组的原因:

为了方便的管理内存缓冲区(其实就是分配一段连续的内存,减少内存的碎片化),如果直接使用指针而不使用数组,那么,在分配内存缓冲区时,就必须分配结构体一次,然后再分配结构体内的指针一次,(而此时分配的内存已经与结构体的内存不连续了,所有要分别管理即申请和释放)而如果使用数组,那么只需要一次就可以全部分配出来,反过来,释放时也是一样,使用数组,一次释放。使用指针,得先释放结构体内的指针,再释放结构体,还不能颠倒顺序

结构体中最后一个成员为[1]长度数组的用法:与长度为[0]数组的用法相同,改写为[1]是出于可移植性的考虑。有些编译器不支持[0]数组,可将其改成[]或[1].

解释

1
2
3
4
5
6
7
8
struct A{
int a;
char* b;
};
struct B{
int a;
char b[0];
}

为了说明这个问题,我们定义一下几个结构体作为比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct A
{
int a;
};
struct B
{
int a;
char* b;
};
struct C
{
int a;
char b[0];
};
struct D
{
int a;
char b[1];
};

struct E
{
int a;
char b[10];
};
1
2
3
4
5
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
cout << sizeof(D) << endl;
cout << sizeof(E) << endl;

输出占用空间大小为

4 8 4 8 16

可以看到struct A大小为int大小 4字节,struct B由于包含了一个指针,在32位系统中,大小为 4字节,总共8字节。strcut C大小为4字节,明显char[0]没有分配内存。struct D大小由于内存对齐原因得到为8字节。struct E大小同样由于内存对齐原因得到为16字节。

由此可以看到长度为0的数组没有分配内存。
为了更详细的说明内存分配情况,我们查看一下每个的地址.

1
2
3
4
5
6
7
A a; B b; C c; D d; E e;
cout << &a.a << '\t' << endl;
cout << &b.a << '\t' << (int)&b.b - (int)&b.a << '\t' << &b.b << endl;
cout << &c.a << '\t' << (int)&c.b - (int)&c.a << '\t' << &c.b << endl;
cout << &d.a << '\t' << (int)&d.b - (int)&d.a << '\t' << &d.b << endl;
cout << &e.a << '\t' << (int)&e.b - (int)&e.a << '\t' << &e.b << endl;

输出为:

00AFFB4C
00AFFB3C 4 00AFFB40
00AFFB30 4 00AFFB34
00AFFB20 4 00AFFB24
00AFFB08 4 00AFFB0C

可以看到每个b都指向了同一位置,int a后面一位的地址.
为了更清楚的描述,在中间插入一个char c可以看到有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct A
{
int a;
char c;
};
struct B
{
int a;
char c;
char* b;
};
struct C
{
int a;
char c;
char b[0];
};

A a;
B b;
C c;
cout << &a.a << '\t' << endl;
cout << &b.a << '\t' << (int)&b.b - (int)&b.a << '\t' << &b.b << endl;
cout << &c.a << '\t' << (int)&c.b - (int)&c.a << '\t' << &c.b << endl;

输出为:

012FF9E8
012FF9D4 8 012FF9DC
012FF9C4 5 012FF9C9

很明显可以得到结论,char b[0]不分配内存,但是可以获得结构体的末尾地址.