C 语言数据结构笔记(四):动态数组的三种删除操作与内存释放
本文介绍了动态数组的三种删除操作:尾部删除、全部删除和中间删除。尾部删除只需将数据计数减1,无需赋值0;全部删除直接将计数置0,高效清空数据;中间删除需数据前移,较复杂。文章还区分了“全部删除”和“内存释放”的本质差异,前者保留容量,后者彻底回收内存。测试验证了每种删除操作的逻辑正确性,并讨论了动态数组不主动缩容的设计考量。
目录
- 一、前言:聚焦动态数组的删除操作
- 二、尾部删除:最简单的删除方式
- 2.1 核心逻辑:为什么无需赋值 0?
- 2.2 代码实现与测试验证
- 2.3 讨论:是否需要缩减容量?
- 三、全部删除:快速清空数据
- 3.1 核心逻辑:效率优先,无需逐个赋值 0
- 3.2 代码实现与测试验证
- 四、释放动态数组:彻底回收内存
- 4.1 关键区别:“全部删除” vs “内存释放”
- 4.2 代码实现与测试验证
- 五、中间删除:需数据前移的复杂操作
- 5.1 核心逻辑:对比 “中间插入”,方向相反
- 5.2 代码实现(含下标容错)
- 5.3 循环条件解析:为什么要 “nCount-1”?
- 5.4 测试验证
- 六、数组特性总结与链表对比
- 七、下一篇预告
- 八、总结
一、前言:聚焦动态数组的删除操作
大家好,我是 Hello_Embed。上一篇我们实现了动态数组 “中间添加节点” 的功能,本次笔记将围绕动态数组的删除操作展开 —— 这是动态数组核心操作的另一半,主要包含三种场景:删除尾部元素、删除全部元素、删除中间元素,同时会讲清楚 “删除全部元素” 与 “释放动态数组” 的本质区别,避免内存操作误区。
二、尾部删除:最简单的删除方式
2.1 核心逻辑:为什么无需赋值 0?
尾部删除是三种删除方式中最简单的,核心操作只有 “将已存数据数量(nCount)减 1”,无需给末尾元素赋值 0,原因如下:动态数组的所有操作(添加、遍历、输出)都以 “nCount” 为依据 —— 比如遍历数据时,循环条件是 “i < nCount”,只要nCount减 1,后续操作就不会访问到 “原末尾元素”,相当于 “逻辑删除”,赋值 0 不仅多余,还会增加不必要的计算开销。
2.2 代码实现与测试验证
尾部删除函数仅需两步:参数合法性检测(空指针判断)、nCount自减:
// 动态数组尾部删除函数:删除最后一个元素
void Delate_Last(struct SZ *pSZ)
{
// 容错:判断结构体指针是否为空
if(NULL == pSZ)
{
printf("NULL!\n");
return;
}
// 核心操作:已存数据数量减1(逻辑删除尾部元素)
pSZ->nCount--;
}
主函数测试逻辑
先通过 “末尾添加” 和 “中间添加” 初始化数组,再调用两次Delate_Last,对比删除前后的输出结果:
int main(void)
{
struct SZ SZ1;//定义动态数组结构体
SZInit(&SZ1);//初始化
// 步骤1:初始化数组数据,最终数组为 [1,2,100,3,4,5,6,200]
Add_Last(&SZ1, 1);
Add_Last(&SZ1, 2);
Add_Last(&SZ1, 3);
Add_Last(&SZ1, 4);
Add_Last(&SZ1, 5);
Add_Last(&SZ1, 6);
Add_Middle(&SZ1, 100, 2); // 下标2插入100
Add_Middle(&SZ1, 200, 10); // 下标过大,插入末尾
printf("删除前:\n");
Output(&SZ1);//输出删除前的数组
// 步骤2:调用两次尾部删除,删除最后两个元素(6和200)
Delate_Last(&SZ1);
Delate_Last(&SZ1);
printf("\n删除后:\n");
Output(&SZ1);//输出删除后的数组
free(SZ1.pSDZ);//释放内存
return 0;
}
测试结果
如下图所示,删除后数组末尾两个元素(6、200)消失,已存数量(Count)从 8 变为 6,符合预期:
2.3 讨论:是否需要缩减容量?
当nCount减小到远小于nSize(比如容量 25,仅存 6 个数据)时,是否需要缩减容量?答案是不需要,原因如下:
- 效率优先:如果每次
nCount减小都缩减容量,需要重新申请小空间、复制数据、释放旧空间 —— 后续若再添加数据,又要重新扩容,反复操作会大幅降低程序效率; - 行业惯例:大多数算法中,动态数组只 “扩容” 不 “缩容”,优先保证操作速度,少量冗余空间对现代计算机影响可忽略。
三、全部删除:快速清空数据
3.1 核心逻辑:效率优先,无需逐个赋值 0
“全部删除” 的目标是清空数组中的所有数据,核心逻辑依然是 “操作nCount”—— 直接将nCount赋值为 0,无需遍历数组给每个元素赋值 0,原因和尾部删除一致:
- 赋值 0 会遍历整个数组,若数据量大会浪费大量时间(效率低);
nCount赋值为 0 后,所有操作都会认为数组 “无数据”,达到 “逻辑清空” 的目的。
3.2 代码实现与测试验证
全部删除函数仅需 “空指针检测 +nCount置 0”:
// 动态数组全部删除函数:逻辑清空所有数据
void Delate_All(struct SZ *pSZ)
{
// 容错:判断结构体指针是否为空
if(NULL == pSZ)
{
printf("NULL!\n");
return;
}
// 核心操作:已存数据数量置0(逻辑清空所有数据)
pSZ->nCount = 0;
}
测试逻辑
在 “尾部删除” 的测试代码后,添加Delate_All调用:
// 尾部删除后的代码继续添加
printf("\n全部删除后:\n");
Delate_All(&SZ1);
Output(&SZ1);
测试结果
如下图所示,全部删除后Count变为 0,数据无显示,但Size(容量)仍保留,说明仅清空了 “逻辑数据”,未改变内存容量:
四、释放动态数组:彻底回收内存
4.1 关键区别:“全部删除” vs “内存释放”
很多人会混淆 “全部删除” 和 “释放动态数组”,两者本质不同:
- 全部删除(Delate_All):仅逻辑清空(
nCount=0),数组的容量(nSize)、首地址(pSDZ)仍有效,后续可继续添加数据; - 内存释放(Relase):彻底回收动态数组占用的内存,需将
nSize置 0、pSDZ置空,并调用free释放pSDZ指向的内存 —— 释放后数组无法直接使用,需重新初始化。
4.2 代码实现与测试验证
释放动态数组需四步:空指针检测、nSize置 0、nCount置 0、pSDZ置空 +free:
// 动态数组内存释放函数:彻底回收内存,数组不可再用
void Relase(struct SZ *pSZ)
{
// 容错:判断结构体指针是否为空
if(NULL == pSZ)
{
printf("NULL!\n");
return;
}
// 1. 容量置0
pSZ->nSize = 0;
// 2. 已存数量置0
pSZ->nCount = 0;
// 3. 释放动态内存(避免内存泄漏)
free(pSZ->pSDZ);
// 4. 指针置空(避免野指针,防止误操作)
pSZ->pSDZ = NULL;
}
测试逻辑
在 “全部删除” 的测试代码后,添加Relase调用:
// 全部删除后的代码继续添加
printf("\n释放数组后:\n");
Relase(&SZ1);
Output(&SZ1);
测试结果
如下图所示,释放后Size和Count均为 0,数据无显示,数组彻底失效,需重新调用SZInit才能使用:
五、中间删除:需数据前移的复杂操作
5.1 核心逻辑:对比 “中间插入”,方向相反
中间删除比尾部、全部删除复杂,核心是 “数据前移”—— 对比 “中间插入” 的 “数据后移”,两者方向相反:
- 中间插入:从最后一个元素开始,往后挪 1 位,腾出目标下标位置;
- 中间删除:从目标下标开始,往前填 1 位(用后一个元素覆盖当前元素),覆盖待删除元素。
函数参数仅需 “结构体指针 + 目标下标”(无需数据参数,因为是删除已有元素),同时要处理 “下标过大” 的容错(提示错误,不执行删除)。
5.2 代码实现(含下标容错)
注意下标容错和数据前移逻辑:
// 动态数组中间删除函数:删除指定下标处的元素
void Delate_Middle(struct SZ *pSZ, unsigned int Index)
{
// 容错:1.指针为空;2.下标过大(超过已存数据范围)
if(NULL == pSZ || Index > pSZ->nCount)
{
printf("NULL 或 下标过大!\n");
return;
}
// 核心操作:数据前移(从目标下标开始,用后一个元素覆盖当前元素)
for(unsigned int i = Index; i < pSZ->nCount - 1; i++)
{
pSZ->pSDZ[i] = pSZ->pSDZ[i+1];
}
// 数据前移后,已存数量减1
pSZ->nCount--;
}
5.3 循环条件解析:为什么要 “nCount-1”?
循环条件i < pSZ->nCount - 1是关键,避免数组越界,原因如下:假设nCount=3(数据下标 0、1、2),删除下标 1 的元素:
- 若循环条件为
i < nCount,i会遍历到 2,此时i+1=3,而pSDZ[3]超出数组已存数据范围(越界); - 改为
i < nCount - 1,i仅遍历到 1(nCount-1=2),i+1=2,刚好是最后一个元素,无越界风险。
5.4 测试验证
主函数测试逻辑
初始化简单数组,中间插入数据后,删除指定下标元素:
int main(void)
{
struct SZ SZ1;//定义动态数组结构体
SZInit(&SZ1);//初始化
// 步骤1:初始化数组为 [1,2]
Add_Last(&SZ1, 1);
Add_Last(&SZ1, 2);
printf("初始数组:\n");
Output(&SZ1);
// 步骤2:中间插入数据,数组变为 [1,2,100,200]
Add_Middle(&SZ1, 100, 2); // 下标2插入100
Add_Middle(&SZ1, 200, 10); // 下标过大,插入末尾
printf("\n插入数据后:\n");
Output(&SZ1);
// 步骤3:删除下标2处的元素(即100)
Delate_Middle(&SZ1, 2);
printf("\n删除下标2后:\n");
Output(&SZ1);
free(SZ1.pSDZ);//释放内存
return 0;
}
测试结果
如下图所示,下标 2 处的元素 100 被成功删除,数组变为[1,2,200],Count从 4 变为 3,符合预期:
六、数组特性总结与链表对比
动态数组的核心特性(优缺点)与后续要学的 “链表” 有本质区别,这里提前对比,为后续学习铺垫:
| 特性 | 动态数组(顺序表) | 链表(后续学习) |
|---|---|---|
| 内存存储 | 连续内存空间 | 非连续内存空间(节点通过指针连接) |
| 访问方式 | 随机访问(下标访问,效率高) | 顺序访问(需遍历前序节点,效率低) |
| 增删效率 | 尾部增删快,中间增删慢(需移动数据) | 中间增删快(仅需改指针),尾部增删慢(需遍历) |
| 空间利用率 | 可能有冗余空间(仅扩容不缩容) | 无冗余空间(按需申请节点) |
核心结论:选择哪种结构,需结合场景 —— 若频繁 “随机访问”,选动态数组;若频繁 “中间增删”,选链表。
七、下一篇预告
动态数组的核心操作(创建、添加、删除、释放)已全部讲完,下一篇我们将用动态数组的 “下标特性” 做一个实践案例 ——实现 99 乘法表,通过案例巩固数组下标的使用,同时练习循环逻辑,为后续更复杂的数据结构应用铺垫。
八、总结
本次笔记围绕动态数组的删除操作展开,核心收获包括:
- 三种删除方式的核心逻辑:
- 尾部删除:
nCount--(逻辑删除,无需赋值 0); - 全部删除:
nCount=0(效率优先,不遍历赋值); - 中间删除:数据前移(从目标下标开始)+
nCount--(注意循环越界);
- 尾部删除:
- 关键区别:“全部删除” 是逻辑清空,“内存释放” 是彻底回收(需
free和指针置空); - 数组特性:连续内存 + 随机访问(优点),中间增删需移动数据(缺点),对比链表明确适用场景;
- 实践建议:动态数组操作的核心是 “围绕
nCount和下标”,避免内存越界和泄漏,一定要动手敲代码验证逻辑。
动态数组是线性表的基础,掌握其增删操作后,后续学习链表、栈、队列会更轻松。我是 Hello_Embed,数据结构系列笔记持续更新中!
更多推荐



所有评论(0)