C语言0数组、柔性数组使用介绍

作者: 良知犹存

前言:

上次看到一篇面试分享,里面有个朋友说,面试官问了char[0] 相关问题,但是自己没有遇到过,就绕过了这个问题。

自己在这篇文章下面做了一些回复。

现在想结合自己的理解,解释一下这个 char[0] C语言柔性数组的问题。

作者:良知犹存

转载授权以及围观:欢迎关注微信公众号:羽林君

或者添加作者个人微信:become_me


0数组和柔性数组的介绍 {0数组和柔性数组的介绍}

0数组顾名思义,就是数组长度定义为0,们一般知道数组长度定义至少为1才会给它分配实际的空间,而定义了0的数组是没有任何空间,但是如果像上面的结构体一样在最后一个成员定义为零数组,虽然零数组没有分配的空间,但是它可以当作一个偏移量,因为数组名这个符号本身代表了一个不可修改的地址常量。柔性数组也叫可伸缩性数组,而0数组是一种柔性数组。

因为在早期没引入0长度数组的时候, 大家是通过定长数组和指针的方式来解决的, 但是定长数组定义了一个足够大的缓冲区, 这样使用方便, 但是每次都造成空间的浪费指针的方式, 要求程序员在释放空间是必须进行多次的free操作, 而们在使用的过程中往往在函数中返回了指向缓冲区的指针, 们并不能保证每个人都理解并遵从们的释放方式所以 GNU 就对其进行了0长度数组的扩展. 当使用data[0]的时候, 也就是0长度数组的时候,0长度数组作为数组名, 并不占用存储空间。这样就可以更加高效的利用内存。

在C99之后,也加了类似的扩展,只不过用的是 char payload[]这种形式(所以如果你在编译的时候确实需要用到-pedantic参数,那么你可以将char payload[0]类型改成char payload[], 这样就可以编译通过了,当然你的编译器必须支持C99标准的,如果太古老的编译器,那可能不支持了。

0数组的常规使用 {0数组的常规使用}

首先们定义一个结构体,再在一个结构体的最后,定义一个长度为0的数组,就可以使得这个结构体是可变长的。

如下所示:

//  0长度数组
struct zero_buffer
{
    int     len;
    char    data[0];
};

这个时候 data[0] 只是个数组名, 是不占用存储空间的.

这个结构体的大小用sizeof取长度,实际就是它的成员int的长度,data[0]不占用空间。(数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量!

sizeof(struct zero_buffer) = sizeof(int)

printf("zero struct length is:%d int length is:%d\n",sizeof(struct zero_buffer),sizeof(int));

zero struct length is:4 int length is:4

对于0长数组的这个特点,很容易构造出们需要的数据结构,如缓冲区,数据包等等。

结构体定义如上所示 {结构体定义如上所示}


假设们需要设置一条tcp待发送的数据,长度是15,数据内容是"Hello My Friend”,这样们就可以按照如下去定义了。其中 zbuffer->data 为定义数据的地址,len表示数据的长度。

开辟空间之后使用 {开辟空间之后使用}

们使用的时候, 只需要开辟一次空间即可。

define CURR_LENGTH 15
struct zero_buffer  *zbuffer = NULL;

//  开辟
if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)
{
    zbuffer->len = CURR_LENGTH;
    memcpy(zbuffer->data, "Hello My Friend", CURR_LENGTH);
    printf("%d, %s\n", zbuffer->len, zbuffer->data);
}

使用完释放空间 {使用完释放空间}

释放空间一次释放即可

//  销毁
free(zbuffer);
zero_buffer = NULL;

其他方法实现一些不定长数据的传输 {其他方法实现一些不定长数据的传输}

除了0数组之外,还有使用定长数组和指针数组实现柔性数组的功能。

定长数组 {定长数组}

定长数组顾名思义,就是在结构体里面有个定长的数组,这个数组大小是按照们定义数据最大来进行设置的,为了就是防止数据储存的时候溢出。

定义 {定义}

//  定长缓冲区
define MAX_LENGTH      512
struct max_buffer
{
    int     len;
    char    data[MAX_LENGTH];
};

不过使用过程中,比如要发送 512 字节的数据, 如果用定长包, 假设定长包的最大长度 MAX_LENGTH 为 1024, 那么就会浪费 512 个字节的空间, 也会造成不必要的流量浪费。如果数组结构对齐放置 (这块知识详细可以看之前的数据对齐的文章) sizeof(struct max_buffer) = sizeof(int)+ sizieof(char) * MAX_LENGTH

数据包的构造 {数据包的构造}

一般来说, 们会返回一个指向缓冲区数据结构 max_buffer 的指针.

define CURR_LENGTH 512
struct max_buffer   *mbuffer = NULL;
if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
{
    mbuffer->len = CURR_LENGTH;
    memcpy(mbuffer->data, "Hello World", CURR_LENGTH);
    printf("%d, %s\n", mbuffer->len, mbuffer->data);
}

前部分 4 个字节 p->len, 作为包头(就是多出来的那部分),这个包头是用来描述紧接着包头后面的数据部分的长度,这里是 1024, 所以前四个字节赋值为 1024 (既然们要构造不定长数据包,那么这个包到底有多长呢,因此,们就必须通过一个变量来表明这个数据包的长度,这就是len的作用),

而紧接其后的内存是真正的数据部分, 通过 p->data, 最后, 进行一个 memcpy() 内存拷贝, 把要发送的数据填入到这段内存当中

释放空间 {释放空间}

当使用完毕释放数据的空间的时候, 直接释放就可以了

free(mbuffer);
mbuffer = NULL;

使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间 MAX_LENGTH, 而实际使用过程中, 达到 MAX_LENGTH 长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的.

但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作

指针数组 {指针数组}

它和0数组的区别在于,零数组最后一个结构体元素定义一个data[0],而指针数组就是结构体中需要定义一个指针数组,这里面的指针数组不需要特定在结构体的最后一个元素。

struct point_buffer
{
    char    *data;
    int     len;
};



考虑数组结构对齐(这块知识详细可以看之前的[数据对齐](https://mp.weixin.qq.com/s/35jJQy166-GgR9RaHafhog)的文章), 那么数据结构的大小 sizeof(point_buffer)= sizeof(int) + (补齐int与char * 类型的长度值)+ sizeof(char * ),在的64位编译环境中int类型是4byte,char * 类型为8byte,所以补齐的长度为8-4,最终sizeof(point_buffer) 为16byte。

如果结构体加上 _attribute((packed)) 数据对齐修饰,则 sizeof(point_buffer)= sizeof(int) sizeof(char * ),最终计算为12byte。

空间分配使用 {空间分配使用}

define CURR_LENGTH 1024 
struct point_buffer *pbuffer = NULL;
if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
{
   pbuffer->len = CURR_LENGTH;
   if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
   {
       memcpy(pbuffer->data, "Hello World", CURR_LENGTH);


       printf("%d, %s\n", pbuffer->len, pbuffer->data);
   }
}

分配内存时,需采用两步

首先, 需为结构体分配一块内存空间;

其次,再为结构体中的成员变量分配内存空间.

这样两次分配的内存是不连续的, 需要分别对其进行管理. 当使用长度为的数组时, 则是采用一次分配的原则, 一次性将所需的内存全部分配给它.

释放 {释放}

相反, 释放时也是一样的.

free(pbuffer->data);
free(pbuffer);
pbuffer = NULL;

使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用固定长度的数组, 不会造成空间的大量浪费.

但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向 struct point_buffer 的指针, 这时候们并不能假定使用者了解们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏

小结: {小结:}

定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放。所以最优解

0数组的优劣以及注意事项 {0数组的优劣以及注意事项}

优点 :比起在结构体中声明一个指针变量、再进行动态分配的办法,这种方法效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。此外,0数组也不会像定长数组会造成一定的内存的浪费。

缺点 :在结构体中,数组为0的数组必须在最后声明,使用上有一定限制。

结语

这就是分享的零数组,如果大家有更好的想法和需求,也欢迎大家加好友交流分享哈。

—END—

推荐阅读

【1】C++的智能指针你了解吗?

【2】嵌入式底层开发的软件框架简述

【3】CPU中的程序是怎么运行起来的 必读

【4】cartographer环境建立以及建图测试

【5】设计模式之简单工厂模式、工厂模式、抽象工厂模式的对比

本公众号全部原创干货已整理成一个目录,回复[ 资源 ]即可获得。

参考文章:https://blog.csdn.net/gatieme/article/details/64131322

原文作者:良知犹存

原文链接:https://www.cnblogs.com/conscience-remain/p/14749806.html

更多推荐

更多
  • Azure数据工程指南-二十四、数据治理的权限 创建 azure 预览帐户,探索 azure 预览,探索词汇表,浏览资产,以编程方式使用预览,摘要,管理凭证和访问,创建扫描, 许多组织需要建立数据治理流程、标准和方法,并且已经能够使用内部 SQL Server 工具(如 Master
    Apache CN

  • Azure数据工程指南-二十二、Synapse 分析工作区 创建 Synapse 分析工作区,使用 Spark 探索样本数据,用 SQL 查询数据,用 SQL 创建外部表,摘要, 微软 Azure 数据平台的众多新增功能已经围绕许多类似的产品及其在现代 Azure 数据平台中的用途产生了兴奋和困
    Apache CN

  • Azure数据工程指南-二十三、数据块中的机器学习 创建 MLflow 实验,安装 MLflow 库,创建笔记本,选择性测井,自动记录,摘要, 寻求利用机器学习(ML)和人工智能能力的组织和开发人员花费大量时间构建 ML 模型,并寻求一种方法来简化他们的机器学习开发生命周期,以跟踪实验,
    Apache CN

  • Azure数据工程指南-二十一、将 Apache Spark 的 GraphFrame API 用于图形分析 安装 JAR 库,加载新数据表,将数据加载到数据块笔记本中,用顶点和边构建一个图,查询图表,寻找有图案的图案,用 PageRank 发现重要性,探索入度和出度度量,摘要,进行广度优先搜索,查找连接的组件, 图形技术使用户能够以图形的形式
    Apache CN

  • Azure数据工程指南-20 二十、部署 SQL 数据库先决条件,创建 Visual Studio SQL 数据库项目,安装 Visual Studio GitHub 扩展,导入 AdventureWorks 数据库,连接到 GitHub Repo 源代码控制,将
    Apache CN

  • Azure数据工程指南-十九、部署数据工厂更改 先决条件,创建 DevOps 持续集成构建管道,创建 DevOps 持续部署发布渠道,验证部署的数据工厂资源,摘要,Azure PowerShell 任务停止触发器,ARM 模板部署任务,Azure PowerShell 任务启动触发器
    Apache CN

  • Azure数据工程指南-十八、用于 Cosmos DB 的 Azure Synapse 链接 创建一个 Azure Cosmos DB 帐户,启用 Azure Synapse 链接,创建一个 Cosmos DB 容器和数据库,将数据导入 Azure Cosmos DB,在 Azure Synapse Analytics 中创建
    Apache CN

  • Azure数据工程指南-十六、流分析异常检测 先决条件,创建流分析输入和输出,创建实时电源 BI 仪表板,监控实时电源 BI 流,摘要,创建 Azure 流分析作业,创建物联网中心,创建 Power BI 服务,下载设备模拟器,添加流输入,添加流输出,编写流分析查询,启动流分析作业
    Apache CN

  • Azure数据工程指南-十七、使用 Apache Spark 的实时物联网分析 先决条件,创建物联网中心,创建数据块集群,安装 Maven 库,创建笔记本并运行结构化流查询,摘要,配置笔记本连接,开始结构化流,启动物联网设备模拟器,显示实时流数据,创建 Spark SQL 表,将流写入增量表, 实时物联网分析、高级
    Apache CN

  • Azure数据工程指南-十五、DeltaLake 为什么是酸性 DeltaLake,先决条件,创建并插入 DeltaLake,更新 DeltaLake,从 DeltaLake 删除,浏览增量日志,摘要,插入,更新,删除, 在使用 Azure Data Lake Storage Gen2
    Apache CN

  • 近期文章

    更多
    文章目录

      推荐作者

      更多