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虚拟机大同小异…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
SCAU期末笔记 - 数据分析与数据挖掘题库解析
这门怎么题库答案不全啊日 来简单学一下子来 一、选择题(可多选) 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘:专注于发现数据中…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...
基于Java+MySQL实现(GUI)客户管理系统
客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息,对客户进行统一管理,可以把所有客户信息录入系统,进行维护和统计功能。可通过文件的方式保存相关录入数据,对…...
深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用
文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么?1.1.2 感知机的工作原理 1.2 感知机的简单应用:基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...
C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...
