C语言柔性数组详解:让你的程序更灵活
柔性数组
- 一、前言
- 二、柔性数组的用法
- 三、柔性数组的内存分布
- 四、柔性数组的优势
- 五、总结
一、前言
仔细观察下面的代码,有没有看出哪里不对劲?
struct S
{int i;double d;char c;int arr[];
};
还有另外一种写法:
struct S
{int i;double d;char c;int arr[0];
};
你应该一眼就看到了,结构体的最后一个成员数组的写法是int arr[];
或者是int arr[0]
,这两种写法是等价的,意思是这个数组的大小是不确定的、未知的、可以变化的。
C99允许这种特殊的结构体存在。这样的结构体满足下面两个条件:
- 最后一个成员变量是一个大小可以变化的数组。
- 这个成员数组前面至少有另外一个成员变量。
我们称这个大小可以变化的成员数组为柔性数组。
注意,柔性数组不能是结构体里唯一一个成员,下面的代码是不允许的:
struct S
{int arr[0];
};
这篇文章里,我将重点探讨柔性数组的用法、内存分布以及和优势。
二、柔性数组的用法
我不建议在栈上直接定义有柔性数组的结构体,也就是这么写:
struct S s;
因为柔性数组的大小是可以变化的,我建议在堆上申请空间,采取动态内存管理的方法,这样就能发挥出柔性数组大小可以改变的优势。
假设我们使用malloc()函数来开辟空间,一开始应该malloc出多大的空间呢?要回答这个问题,首先我们要知道sizeof(struct S)是多少。
事实上,sizeof(struct S)计算出来的结果是该结构体不考虑柔性数组的大小。如果我们想要给柔性数组开辟空间,malloc出来的大小应该是sizeof(struct S)加上柔性数组的大小。
假设这个柔性数组在结构体中的声明是int arr[0];
,我想给这个数组的大小是40个字节,这样这个数组就能存储10个int,那么一开始malloc的大小就应该是sizeof(struct S)+10*sizeof(int),具体的例子如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}return 0;
}
该结构体中的i,d,c等变量可以正常使用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->i = 10;ps->d = 3.14;ps->c = 'F';return 0;
}
柔性数组也可以像正常的数组一样访问,比如把1~10放进去。注意此时这个数组的容量是10个int,不能越界访问。使用的例子如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->i = 10;ps->d = 3.14;ps->c = 'F';for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");return 0;
}
我们还可以对柔性数组扩容,如果我们想让这个柔性数组的容量是20个int,整个结构体的新的大小就是sizeof(struct S)+20*sizeof(int),因为sizeof(struct S)是不考虑柔性数组的大小时计算的结构体大小。只需要对ps进行realloc就行了。实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->i = 10;ps->d = 3.14;ps->c = 'F';for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));if (tmp == NULL){printf("realloc()->%s\n", strerror(errno));return 1;}else{ps = tmp;}return 0;
}
扩容后的柔性数组的空间更大了,我们可以把11~20都放进去。实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->i = 10;ps->d = 3.14;ps->c = 'F';for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));if (tmp == NULL){printf("realloc()->%s\n", strerror(errno));return 1;}else{ps = tmp;}for (int i = 10; i < 20; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 20; i++){printf("%d ", ps->arr[i]);}return 0;
}
当然最后别忘了free掉ps,否则会导致内存泄漏。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->i = 10;ps->d = 3.14;ps->c = 'F';for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));if (tmp == NULL){printf("realloc()->%s\n", strerror(errno));return 1;}else{ps = tmp;}for (int i = 10; i < 20; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 20; i++){printf("%d ", ps->arr[i]);}free(ps);ps = NULL;return 0;
}
对于柔性数组的使用,在上面的例子中,可以总结出几个要点:
- malloc出来的大小是sizeof(struct S)加上柔性数组的大小,calloc同理。
- 扩容时realloc出来的新大小也是sizeof(struct S)加上柔性数组的新大小。
- 每次使用malloc和realloc等函数时,需要检查返回值,否则可能导致对NULL指针的解引用(这点是动态内存管理的常识了)。
- 一定要记得柔性数组的容量是多少,不要越界访问了,空间不够记得扩容。
- 记得free,防止内存泄漏。
三、柔性数组的内存分布
柔性数组是结构体的一个成员数组,在前面的例子中,整个结构体都是在堆上malloc出来的。此时,整个结构体都存储在堆上的一块连续的空间里,包括前面几个成员变量i,d,c和柔性数组arr。也就是这样:
只不过数组arr的大小是可以改变的,所以叫“柔性数组”。
有些朋友可能会说了,我不需要柔性数组也能实现类似这样的效果呀!我在结构体里存一个指针,指向一块malloc出来的空间,这块空间也是堆上的,可以动态管理。也就是说,像下面这样定义结构体:
struct S
{int i;double d;char c;int* arr;
};
这样似乎还简单一点,先malloc出一个struct S出来,malloc的大小就是sizeof(struct S),像这样:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}return 0;
}
然后再malloc出10个int的大小出来,用结构体中的arr指针来管理这块空间,像这样:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->arr = (int*)malloc(10 * sizeof(int));if (ps->arr == NULL){printf("2: malloc()->%s\n", strerror(errno));return 1;}return 0;
}
此时arr就可以当成一个数组来使用了,比如把1~10放进去。同样还是要注意不要越界访问。示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->arr = (int*)malloc(10 * sizeof(int));if (ps->arr == NULL){printf("2: malloc()->%s\n", strerror(errno));return 1;}for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");return 0;
}
你如果觉得空间不够,还可以扩容。比如,你可以把结构体中的arr进行realloc,新的大小能存放20个int。示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->arr = (int*)malloc(10 * sizeof(int));if (ps->arr == NULL){printf("2: malloc()->%s\n", strerror(errno));return 1;}for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");int* tmp = (int*)realloc(ps->arr, 20 * sizeof(int));if (tmp == NULL){printf("realloc()->%s\n", strerror(errno));return 1;}else{ps->arr = tmp;}return 0;
}
此时,你就可以把11~20也放进去。实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->arr = (int*)malloc(10 * sizeof(int));if (ps->arr == NULL){printf("2: malloc()->%s\n", strerror(errno));return 1;}for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");int* tmp = (int*)realloc(ps->arr, 20 * sizeof(int));if (tmp == NULL){printf("realloc()->%s\n", strerror(errno));return 1;}else{ps->arr = tmp;}for (int i = 10; i < 20; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 20; i++){printf("%d ", ps->arr[i]);}return 0;
}
最后别忘了把arr和ps都free掉,而且顺序不能错了。如果你先free掉了ps,结构体就没了,里面的arr就成为了野指针,内存就泄露了。实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){printf("malloc()->%s\n", strerror(errno));return 1;}ps->arr = (int*)malloc(10 * sizeof(int));if (ps->arr == NULL){printf("2: malloc()->%s\n", strerror(errno));return 1;}for (int i = 0; i < 10; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}printf("\n");int* tmp = (int*)realloc(ps->arr, 20 * sizeof(int));if (tmp == NULL){printf("realloc()->%s\n", strerror(errno));return 1;}else{ps->arr = tmp;}for (int i = 10; i < 20; i++){ps->arr[i] = i + 1;}for (int i = 0; i < 20; i++){printf("%d ", ps->arr[i]);}free(ps->arr);ps->arr = NULL;free(ps);ps = NULL;return 0;
}
那这种实现的内存分布是怎么样的呢?这个结构体是存储在堆上的,用ps来管理,结构体里的一个指针arr又指向了堆上的另一块空间,如下图:
这种实现方式和柔性数组的方式感觉差不多呀!都是在堆上有个结构体,结构体里有个大小可以变化的数组。那为什么非要搞出来个柔性数组的概念呢?那是因为,柔性数组有它独特的优势。
四、柔性数组的优势
前面我们先用柔性数组实现了一种效果,又不使用柔性数组实现了相似的效果,对比两种实现方式,我们可以做一些总结:
- 使用上:柔性数组malloc了一次,free了一次;不使用柔性数组要malloc两次,free两次。柔性数组的使用更加简单,不容易出错。如果不使用柔性数组,可能会忘记free掉结构体里的arr指针,导致内存泄漏。
- 效率上:柔性数组的存储空间是连续的,访问时效率更高。
所以,虽然有相似的效果,我更推荐使用柔性数组的方式。
五、总结
在这篇博客里,重点需要掌握以下几点:
- 如果结构体里最后一个成员变量是一个数组,并且大小可以变化,这个成员数组就叫做柔性数组。一个结构体里,除了柔性数组外必须至少有一个成员变量。
- 使用sizeof计算含有柔性数组的结构体大小时,只计算除柔性数组之外的空间大小。
- 使用柔性数组,比不使用柔性数组操作更加简单,不易出错,且效率更高。
相关文章:

C语言柔性数组详解:让你的程序更灵活
柔性数组 一、前言二、柔性数组的用法三、柔性数组的内存分布四、柔性数组的优势五、总结 一、前言 仔细观察下面的代码,有没有看出哪里不对劲? struct S {int i;double d;char c;int arr[]; };还有另外一种写法: struct S {int i;double …...

Redis-带你深入学习数据类型list
目录 1、list列表 2、list相关命令 2.1、添加相关命令:rpush、lpush、linsert 2.2、查找相关命令:lrange、lindex、llen 2.3、删除相关命令:lpop、rpop、lrem、ltrim 2.4、修改相关命令:lset 2.5、阻塞相关命令:…...
react拖拽依赖库react-dnd
注:对于表格自定义行可以拖拽和树自定义节点可以拖拽等比较适用,其余的拖拽处理可以使用dragstart,drop等js原生事件来实现 react-dnd使用方法很简单,直接上干货 第一步安装依赖并引入 import { DndProvider } from react-dnd;…...

win10环境安装使用docker-maxwell
目的:maxwell可以监控mysql数据变化,并同步到kafka、mq或tcp等。 maxwell和canal区别: maxwell更轻量,canal把表结构也输出了 docker bootstrap可导出历史数据,canal不能 环境 :win10,mysql5…...

Docker部署RabbitMQ
Docker部署RabbitMQ 介绍 RabbitMQ是一个开源的消息队列系统,它被设计用于在应用程序之间传递消息。它采用了AMQP(高级消息队列协议)作为底层通信协议,这使得它能够在不同的应用程序之间进行可靠的消息传递。 那么,…...

23个react常见问题
1、setState 是异步还是同步? 合成事件中是异步 钩子函数中的是异步 原生事件中是同步 setTimeout中是同步 相关链接:你真的理解setState吗?: 2、聊聊 react16.4 的生命周期 图片 相关连接:React 生命周期 我对 Reac…...
【python基础】——Anaconda下包更新的坑及安装与卸载、及安装后Jupyter Notebook没反应的解决方法
文章目录 前言一、起因:如何一步步走到卸载重装anaconda?二、卸载anaconda二、重新安装anaconda三、关于安装Anaconda后,打开Jupyter Notebook运行代码没反应且in[ ]没有*前言 本文主要用来记录自己近期踩坑的一些复盘。其中坑有: ‘.supxlabel’ 不起作用的解决pip list 与…...
CSS 中的 display 和 visibility
CSS 中的 display 和 visibility 都可以设置一个元素在浏览器中的显示或隐藏效果。 display: 隐藏某个元素时,不会占用任何空间。换句话讲,不会影响布局。visibility: 隐藏某个元素时,仍需占用与未隐藏之前一样的空间。换句话讲,…...
解决mysql报错this is incompatible with DISTINCT
环境 centos 9 php7.4 mysql5.7 问题 mysql查询报如下错误: SQLSTATE[HY000]: General error: 3065 Expression #1 of ORDER BY clause is not in SELECT list, references column hst_csc.q.timestamp which is not in SELECT list; this is incompatible with…...

C++-map和set
本期我们来学习map和set 目录 关联式容器 键值对 pair 树形结构的关联式容器 set multiset map multimap 关联式容器 我们已经接触过 STL 中的部分容器,比如: vector 、 list 、 deque 、forward_list(C11)等,这些容器统称为序列式…...

微信小程序AI类目-深度合成-AI问答/AI绘画 互联网信息服务算法备案审核通过教程
近期小程序审核规则变化后,很多使用人类小徐提供的chatGPT系统的会员上传小程序无法通过审核,一直提示需要增加深度合成-AI问答、深度合成-AI绘画类目,该类目需要提供互联网信息服务算法备案并上传资质,一般对企业来说这种务很难实…...
蓝桥杯官网练习题(星期一)
题目描述 本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。 整个 2020 世纪(1901 年 1 月 1 日至 2000 年 12 月 3131 日之间),一共有多少个星期一?(不要告诉我你不知道今天是星…...

centos7更新podman
实验环境:centos7.7.1908 1.安装podman并查看版本 yum install podman podman -v 当前podman版本信息是1.6.4 2.更新podman版本 通过查看资料显示centos 7 支持最高版本为 3.4.4,更新podman大致有以下四步: golang 安装(本次使用版本: 1.…...

Java特性之设计模式【抽象工厂模式】
一、抽象工厂模式 概述 抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式 在抽象工厂模式中,接口是…...

机器学习简介
引言 为何现在机器学习如此热门? 主要原因是由于“人类无论如何也做不到在短时间内实现从大量的数据中自动的计算出正确的结果操作”。 什么是机器学习? 所谓的机器学习,就是通过对数据进行反复的学习,来找出其中潜藏的规律和模式…...
linux之perf(2)list事件
Linux之perf(2)list事件 Author:Onceday Date:2023年9月3日 漫漫长路,才刚刚开始… 参考文档: Tutorial - Perf Wiki (kernel.org)perf-list(1) - Linux manual page (man7.org) 1. 概述 perf list用于列出可用的性能事件,这…...
将多个EXCEL 合并一个EXCEL多个sheet
合并老版本xls using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using NPOI.HSSF.UserModel; …...

【送书活动】揭秘分布式文件系统大规模元数据管理机制——以Alluxio文件系统为例
前言 「作者主页」:雪碧有白泡泡 「个人网站」:雪碧的个人网站 「推荐专栏」: ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄,vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄ÿ…...
微信小程序——数据绑定
在微信小程序中,可以通过以下代码实现数据绑定: 在WXML中,使用双大括号{{}}绑定数据,将数据渲染到对应的视图中。 <view>{{message}}</view>在JS中,定义一个数据对象,并将其绑定到页面的data…...

libbpf-bootstrap安卓aarch64适配交叉编译
1.为什么移植 疑惑 起初我也认为,像libbpf-bootstrap这样在ebpf程序开发中很常用的框架,理应支持不同架构的交叉编译。尤其是向内核态的ebpf程序本身就是直接通过clang的-target btf直接生成字节码,各个内核上的ebpf虚拟机大同小异…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...

MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...

P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...

Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...

Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
Caliper 负载(Workload)详细解析
Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...