考虑如下问题,我们试图定义一个名为Student的结构,这个结构应包括学生的姓名,学生已修课程的数量以及已修课程各科的分数。实践中,每个学生已修课程的数目是不一样的,这使得我们在定义用于存储分数的结构成员时面临两难的局面:

  • 如果将该数组定义得比较小,会存在某学生所修课程数量较多,存不下的情况。

  • 如果将该数组定义得很大,比如10000,则对于大多数学生而言,内存空间浪费严重。而且,无论将该数组定义得再大,理论上都存在实际数据超量,存不下的可能。

  解决方案之一是把分数数组成员定义为一个指向float的指针,如下述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
25
26
27
//Project - StudentScores
#include <stdio.h>
#include <stdlib.h>

typedef struct {
char sName[20]; //学生姓名
int n; //已修课程数量
float* scores; //指针作为结构成员,分数数组
} Student;

int main() {
Student s = {"Dorothy Henry", 4, NULL};
printf("sizeof(s) = %lld, sizeof(s.sName) = %lld, "
"sizeof(s.n) = %lld, sizeof(s.scores) = %lld\n",
sizeof(s),sizeof(s.sName),sizeof(s.n),sizeof(s.scores));

s.scores = calloc(s.n,sizeof(float));

s.scores[0] = 80; s.scores[1] = 90; s.scores[2] = 90; s.scores[3] = 80;
float fSum = 0;
for (int i=0;i<s.n;i++)
fSum += s.scores[i];
printf("Average score of %s: %f",s.sName,fSum/s.n);

free(s.scores);
return 0;
}

上述程序的执行结果为:

1
2
sizeof(s) = 32, sizeof(s.sName) = 20, sizeof(s.n) = 4, sizeof(s.scores) = 8
Average score of Dorothy Henry: 85.000000

第5 ~ 9行:定义了Student结构,其包含3个数据成员,分别是20个字节的学生姓名sName,4个字节的已修课程数量n,8个字节的分数”数组“指针scores。其3个成员的字节数相加,等于一个Student对象的尺寸32个字节。

  对于Student结构而言,scores跟其它成员一样,只是数据成员,只不过类型特殊,是float*。

1
Student s = {"Dorothy Henry", 4, NULL};

第12行:s对象的初始化中,将s.n初始化为4,s.scores初始化为空指针。

1
s.scores = calloc(s.n,sizeof(float));

第17行:s.scores只是一个指针,要往s.scores”数组“里存分数前,需要手动申请需要的内存空间。这行代码为其申请了s.n,即4个float的空间。必要时,如果希望往s.scores“数组”中存入超过4个的分数,可以通过realloc()函数重新调整其动态内存的大小。

1
2
3
4
5
s.scores[0] = 80;  s.scores[1] = 90; s.scores[2] = 90; s.scores[3] = 80;
float fSum = 0;
for (int i=0;i<s.n;i++)
fSum += s.scores[i];
printf("Average score of %s: %f",s.sName,fSum/s.n);

第19 ~ 23行:在分配了内存空间之后,s.scores指针便可以当成”数组“来使用。使用过程中,程序员会注意避免下标越界。这几行代码先把4个分数填入s.scores”数组”,然后再计算平均分并打印出来。

1
free(s.scores);

第25行:释放calloc()申请的动态内存。

  这种使用指针成员来管理不定尺寸空间的方法需要程序员手动申请及释放内存,程序会变得比较零散。另外一个解决方案是使用结构的柔性数组成员(flexible array member)。请阅读下述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
25
26
27
28
29
//Project - FlexMember
#include <stdio.h>
#include <stdlib.h>

typedef struct {
char sName[20]; //学生姓名
int n; //已修课程数量
float scores[]; //柔性数组成员
} Student;

int main() {
unsigned int nBytes = sizeof(Student) + 4*sizeof(float);
Student* s = malloc(nBytes);
printf("sizeof(*s) = %lld, sizeof(s->sName) = %lld, "
"sizeof(s->n) = %lld, nBytes = %lld\n",
sizeof(*s),sizeof(s->sName),sizeof(s->n), nBytes);

printf("s = %p, s->scores = %p\n", s, s->scores);

s->n = 4;
s->scores[0] = 80; s->scores[1] = 90; s->scores[2] = 90; s->scores[3] = 80;
float fSum = 0;
for (int i=0;i<s->n;i++)
fSum += s->scores[i];
printf("Average score: %f",fSum/s->n);

free(s);
return 0;
}

上述程序的执行结果为:

1
2
3
sizeof(*s) = 24, sizeof(s->sName) = 20, sizeof(s->n) = 4, nBytes = 40
s = 0000000000711480, s->scores = 0000000000711498
Average score: 85.000000

说明:在读者的计算机上,执行结果中的地址很可能与本书不同。

1
2
3
4
5
typedef struct {
char sName[20]; //学生姓名
int n; //已修课程数量
float scores[]; //柔性数组成员
} Student;

第5 ~ 9行:scores数组成员即为Student结构的柔性数组成员。柔性数组成员的定义要满足如下要求。

  • 该成员必须是结构的最后一个成员;

  • 该成员在语法上定义了一个不指定元素数量的“空”数组。

  事实上,对于一个Student类型的对象而言, 只有sName及n成员会被分配空间,scores成员是不占空间的。

1
2
unsigned int nBytes = sizeof(Student) + 4*sizeof(float);
Student* s = malloc(nBytes);

第12 ~ 13行:现假设我们要存4门课程的分数,通过一个Student的对象大小加上4个float的对象大小得到需要的内存字节数nBytes。然后,通过malloc()函数分配nBytes的堆空间,并把地址传给指针s。

1
2
3
printf("sizeof(*s) = %lld, sizeof(s->sName) = %lld, "
"sizeof(s->n) = %lld, nBytes = %lld\n",
sizeof(*s),sizeof(s->sName),sizeof(s->n), nBytes);

第14 ~ 16行:通过执行结果可以看到,sName成员占20个字节,n成员占4个字节。虽然我们事实上给s所指向的Student对象申请了nBytes = 40个字节的空间,但在编译器看来,*s,即s所指向的Student对象的大小只有24个字节。

1
printf("s = %p, s->scores = %p\n", s, s->scores);

第18行:把s,s->scores按地址格式输出。根据执行结果,我们可以画出该Student对象的内存结构图。

  如果把s->scores的地址值减去s的地址值,差为24 = sizeof(Student)。这说明,结构的柔性数组成员事实上是一个指针,它指向紧随该对象的内存地址,其值恒等于对象地址+sizeof(类型)。换句话说:如果我们实际分配给结构对象的空间大于sizeof(Student),那么多出来的内存可以通过其柔性数组成员来访问。

1
2
Student s1;
printf("\n%p - %p",&s1,s1.scores);

  如果我们直接定义类型为Student的变量s1,编译器会为s1分配sizeof(Student) = 24个字节的空间。但即便如此,s1.scores仍然会等于s1的地址+24。如果我们强行通过s1.scores进行数据访问,事实上访问的是不属于s1对象的空间,这是程序员需要小心避免的。

1
2
3
4
5
6
s->n = 4;
s->scores[0] = 80; s->scores[1] = 90; s->scores[2] = 90; s->scores[3] = 80;
float fSum = 0;
for (int i=0;i<s->n;i++)
fSum += s->scores[i];
printf("Average score: %f",fSum/s->n);

第20 ~ 25行:给s的柔性数组成员赋值,然后计算平均分并打印。由于我们确信s->scores所对应的内存空间属于s指向的结构对象,上述操作是安全的。

1
free(s);

第27行:一定不要忘了释放动态分配的内存空间。

  请读者注意,将带有柔性数组成员的结构对象赋值给另外一个同类型对象是危险的:

1
2
Student s1;
s1 = *s; //*s是存有4个分数的占40个字节空间的结构对象

  对于编译器而言,s1和*s都只有sizeof(Student) = 24字节的空间。从*s到s1的赋值,只会拷贝前24个字节。同样的危险也会发生在函数传值时,函数的传值,可以认为是从实际参数到形式参数的赋值。