精通C语言C99伸缩型数组成员(Flexible array member)

Posted 从善若水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精通C语言C99伸缩型数组成员(Flexible array member)相关的知识,希望对你有一定的参考价值。

本人就职于国际知名终端厂商,负责modem芯片研发。
在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。


博客内容主要围绕:
       5G协议讲解
       算力网络讲解(云计算,边缘计算,端计算)
       高级C语言讲解
       Rust语言讲解

C99伸缩型数据成员

       C99新增一个特性:伸缩型数组成员(Flexible array member),利用这项新特性声明的结构,其最后一个数组成员具有下述特性:

  • 该数组不会立即存在(不占用内存);
  • 使用这个伸缩型数组成员可以编写合适的code,就好像它确实存在并具有需要数目的元素一样。

一、声明一个伸缩型数组成员

       首先看一个伸缩型数组成员的例子:

struct flex
{
	int count;
	double average;
	double scores[]; //伸缩型数据成员
};

声明一个伸缩型数组成员有如下规则:

  • 伸缩型数组成员必须是结构的最后一个成员
  • 结构中必须至少有一个成员
  • 伸缩数组的声明类似于普通数组,只是它的方括号中是空的

       声明一个struct flex 类型的结构变量时,不能用scores做任何事情。因为没有给这个数组预留存储空间,所以我们需要通过malloc等类似函数给scores数组分配内存。看下面的例子:

#include<stdio.h>
#include<stdlib.h>

#define COURSES_NUM 3

#pragma pack(4)

struct flex
{
    int count;
    double average;
    double scores[]; //伸缩型数组成员
};

int main()
{
    //编译器并没有给 scores 数组分配内存
    printf("struct flex size %zd\\n",sizeof(struct flex));

    //请求为一个结构和一个数组分配存储空间
    struct flex * person = malloc(sizeof(struct flex) + COURSES_NUM*sizeof(double));
    person->count = COURSES_NUM;
    person->scores[0]=95.;
    person->scores[1]=100.;
    person->scores[2]=80.;

    person->average = (person->scores[0]+person->scores[1]+person->scores[2])/3.;

    printf("person average is %lf\\n",person->average);

    free(person);

    return 0;
}

输出如下图:

我们看到 sizeof(struct flex) == 12,说明编译器并没有给scores数组预留内存空间。


二、伸缩型数组成员的限制

       第一,不能用结构体进行赋值和拷贝,例如下面的code是错误的:

struct flex *person1 , *person2; // *person1 , *person2都是结构
......
*person1 = *person2; //错误

这样做只能拷贝除伸缩型数组成员之外的其它成员。如果确定要拷贝,应该使用类似memcpy()的函数操作。

       第二,不要以按值方式把这种结构传递给函数。原因相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数

       第三不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员


三、伸缩型数组成员与指针的区别

       区别一:编译器为结构预留的内存大小不同,看下面的例子:

#include<stdio.h>
#include<stdlib.h>

#define COURSES_NUM 3

#pragma pack(4)

struct flex
{
    int count;
    double average;
    double scores[]; //伸缩型数组成员
};

struct pointer
{
    int count;
    double average;
    double *scores; //指针数据成员
};

int main()
{
    //编译器并没有给 scores 数组分配内存
    printf("struct flex size %zd\\n",sizeof(struct flex));
    //编译器给 scores 指针分配内存
    printf("struct pointer size %zd\\n",sizeof(struct pointer));
    return 0;
}

结果输出如下:

博主使用的是64位操作系统,所以指针占用了8B存储空间,然而伸缩型数组在声明时并未占用存储空间。

       区别二:成员地址分配不同,看下图解释:

上图应该解释的很清楚了,伸缩型数组成员中存储的数据与其在结构中紧邻的上一个成员存储的数据在内存的逻辑地址上是连续。而指针数据成员中存储的存放数据的逻辑地址,不一定与其结构中紧邻的上一个成员存储的数据在内存逻辑地址上连续。

这也是为什么不建议使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。

看下面的Demo code:

#include<stdio.h>
#include<stdlib.h>

#define COURSES_NUM 3

#pragma pack(4)

struct flex
{
    int count;
    double average;
    double scores[]; //伸缩型数据成员
};

struct pointer
{
    int count;
    double average;
    double *scores; //数据成员
};

int main()
{
    struct flex * person_flex = malloc(sizeof(struct flex)+COURSES_NUM*sizeof(double));

    struct pointer * person_pointer = malloc(sizeof(struct pointer));
    person_pointer->scores = malloc(COURSES_NUM*sizeof(double));

    printf("[person_flex] average address is %p , scores address is %p\\n",
           &person_flex->average,person_flex->scores);

    printf("[person_pointer] average address is %p , scores address is %p\\n",
           &person_pointer->average,person_pointer->scores);


    free(person_flex);
    free(person_pointer->scores);
    free(person_pointer);
    return 0;
}

输出结果如下:

这里的对比并不是说谁好谁坏,好与坏取决于项目中的具体应用!


四、ISO C99伸缩型数组成员 与 GCC中的零长度数组的区别

       其实本质还是相同的,只不过在GCC中的声明语法如下:

struct line {
  int length;
  char contents[0]; // Zero-length array
};

struct line *thisline = (struct line *)malloc (sizeof (struct line) + this_length);
thisline->length = this_length;

GCC中的扩展

       GCC中允许对伸缩型数组成员静态初始化,这个过程等价于定义了一个新的结构体,包含原始的数据成员以及足够容纳初始数组对象的数组,看下面的code:

struct f1 {
  int x; 
  int y[];
} f1 = { 1, { 2, 3, 4 } };

//等价于定义一个新结构体
struct f1 {
  int x; 
  int y[3];
};

       GCC中允许有伸缩型数组对象的结构,内嵌在其它结构或联合中,对于这类结构的初始化有一些限制:

  • 只允许对顶层的结构进行非空初始化(Non-empty initialization)
  • 如果是非顶层成员,只能进行空初始化

看下面的code:

struct f1 {
  int x; 
  int y[3];
};

struct f2 {
  struct f1 f1; int data[3];
};   

/* 
 有效,非顶层空初始化
 { 2, 3, 4 }初始化数组data
 而data的逻辑地址与f1结构中的y相同,等价于静态初始化了f1结构中的伸缩型数组y
*/
struct f2 _f2 = { { 1 }, { 2, 3, 4 } }

struct foo { int x; int y[]; };
struct bar { struct foo z; };

struct foo a = { 1, { 2, 3, 4 } };         //顶层所以有效
struct bar b = { { 1, { 2, 3, 4 } } };     //非顶层所以无效
struct bar c = { { 1, { } } };             //非顶层空初始化,有效
struct foo d[1] = { { 1, { 2, 3, 4 } } };  //非顶层非空初始化,无效

以上是关于精通C语言C99伸缩型数组成员(Flexible array member)的主要内容,如果未能解决你的问题,请参考以下文章

C语言怎样定义变长数组

C99 语言中具有未命名成员的结构的正确行为是啥?

梦开始的地方——C语言柔性数组

MDK KEIL 机构体初始化 . 点 成员 初始化, C99

C语言柔性数组

C语言中的datatype是啥