当前位置: 首页 > java >正文

从C语言到数据结构:保姆级顺序表解析

目录

PS:从C语言到数据结构——为什么要学这个

一、顺序表的本质

1.1 如何用C语言描述这个“智能数组”?

1.2 手撕代码

1.2.1 头文件

1.2.2 宏定义

1.2.3 类型重定义

1.2.4:定义“人”的结构体 —— Person

1.2.5顺序表结构定义

逐个解释:

二、顺序表的创建、成长与销毁

2.1 初始化(Init)—— 买一个初始大小的“鞋柜”

2.1.1 关键问题:函数传参的两种方式

2.1.2 为什么不能传值?

2.2 扩容(CheckCapacity)—— 鞋子多了,换个大鞋柜!

2.2.1 扩容整体代码

2.2.2 手撕代码

第一部分:函数名及传参

第二部分:断言

第三部分 判断空间够不够

第四部分 三目操作扩容

第五部分:检测扩容是否成功

第六部分:更新结构体

2.2.3 画图帮助理解

2.3 顺序表的销毁

鞋柜坏了,鞋柜得扔掉

2.3.1 先看代码(逐行解剖)

第一部分:void SLDestroy(SL* ps)

第二部分:条件判断

第三部分:释放空间

第四部分:防止野指针

第五部分:重新开始

三、数据的操作——增、删、查、改

3.1 尾插(PushBack)—— 往鞋柜最后空位放新鞋

3.1.1 先看代码

第一部分:函数名

第二部分:扩容为插入准备

它做了什么?

第三部分:插入

3.1.2 画图理解

3.2 删除(Pop)—— 从鞋柜中间拿出一双鞋

3.2.1 先看代码:

第三句:

最重要部分:

3.3 查找(Find)—— 按名字找鞋子

3.3.1 先看代码

第二步:遍历查找(线性搜索)

第三步:判断对比

3.4 修改

四、总结

4.1 回顾我们的核心函数

4.2 关键思想提炼

1. 内存安全第一

2.防御性编程

4.3 进阶之路(通讯录实战)


在我们共同踏入数据结构这座宏伟殿堂的第一步,我非常理解大家的心情。我们刚刚经历了C语言和Python的洗礼,对指针、内存、结构体这些概念可能既熟悉又有些许畏惧。今天开始,我们将以C语言知识为基石,搭建起通往数据结构的第一座桥梁——顺序表

PS:从C语言到数据结构——为什么要学这个

在我们写C语言程序时,如果我们需要管理100个学生的成绩,我们会怎么做?

你会毫不犹豫地说:用数组!

int scores[100];

没错,数组就是我们最早接触的、最原始的顺序存储结构。它非常好用,因为:

  • 连续存储:所有数据在内存中紧挨在一起。
  • 随机访问:通过下标 scores[i] 可以直接访问任何一个元素,速度极快。

但是一个东西不会十全十美,当我们遇到数组的局限性时,该怎么办呢

  • “我申请了100个位置,但如果只有10个学生,是不是浪费了90个空间?”

  • “如果突然来了第101个学生,怎么办?数组大小是固定的,我无法改变啊!”

我们来举一个生活的例子:

你买了一个十个格子的鞋柜,一开始鞋子都能够放进去,但当你的鞋子慢慢积累下来,或者交了男女朋友,那十个可能就有点不够了。那这个时候你只能换一个更大的鞋柜来放这些鞋子。

数据结构要解决的,就是如何更灵活、更高效地组织和管理数据的问题。 顺序表,就是我们为了解决数组的固定大小问题,而设计的第一个“超级数组”。

一、顺序表的本质

顺序表的核心理念非常简单:用时申请,不用时释放,不够了就扩容

它不再像普通数组那样在编译时就固定大小,而是在程序运行过程中,根据我们的需要,动态地申请内存、调整大小。

那么我们该如何去完成这个顺序表呢?

1.1 如何用C语言描述这个“智能数组”?

在C语言里,我们用什么来管理一块动态的内存?指针
我们用什么来记录当前用了多少?整型变量
我们用什么来记录总量有多大?另一个整型变量

所以,我们需要把这三样东西“打包”在一起。用什么打包?结构体(struct)

首先,我们先创建三个文件,来完成我们对顺序表的理解

现在我们现在 .h 文件里定义顺序表每个联系人的数据类型,以及这个“智能数组”本身:

//这里代码有个小错误,待我慢慢分析
//先声明等会要用到函数的头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//大家可以学我这样先把我们要用到的变量通过预处理先定义一下
//这样后续大家要改的话就会很方便
#define NAME_MAX 10 //意思是:NAME_MAX最大值是10 
#define PHONE_MAX 11
//这里也是,我们后续结构体的类型不一定都是int,所以我们通过typedef来改个名字typedef int SLDataType;typedef struct
{char name[NAME_MAX];char phone[PHONE_MAX];
};Person;//结构体本身
typedef struct{SLDataType* arr;;  // 一个指针,指向动态开辟的数组(鞋柜的位置)int size;      // 当前有效元素的个数(鞋柜里实际有几双鞋)int capacity;  // 容量,当前最大能容纳多少元素(鞋柜总共有多少格子)
}SL;

ok,我们详细讲一下这个

1.2 手撕代码

1.2.1 头文件

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

这三个是标准库头文件:

头文件作用
stdio.h提供输入输出函数,比如 printfscanf
stdlib.h提供动态内存管理函数,如 mallocfree,还有 exit 等
assert.h提供断言宏 assert(),用于调试时检查条件是否成立

PS:写 C 程序时,这三个几乎是标配,尤其是做数据结构。

1.2.2 宏定义

#define NAME_MAX 10
#define PHONE_MAX 11

意思是:把所有 NAME_MAX 替换成 10  PHONE_MAX的意思和上面一样

利用宏定义方便后面对变量的修改

1.2.3 类型重定义

typedef int SLDataType;

这是给 int 起了个别名叫 SLDataType(Sequence List Data Type)。

 为什么要这么做?

为了提高代码可扩展性

假设今天存的是整数:

typedef int SLDataType;

明天我要把他改成字符类型:

typedef char SLDataType;

只需要改这一行,其他所有用到 SLDataType 的地方自动跟着变,不需要一个个改 int → char。

设计思想:解耦数据类型和容器逻辑。

这个方法大家一定要会用,不管是现在的学习还是后续就业对于项目的开发,都是必备的知识

1.2.4:定义“人”的结构体 —— Person

typedef struct
{char name[NAME_MAX];   // 姓名,最多10个字符char phone[PHONE_MAX]; // 电话,最多11个字符
} Person;
  • 定义了一个匿名结构体(没有名字),并用 typedef 给它起个别名叫 Person
  • 每个 Person 包含两个字段:
    • name: 字符数组,最多放 10 个字符 + 1 个 (实际最多存 9 个有效字符)
    • phone: 字符数组,最多放 11 个字符(适合中国手机号 11 位)

📌 注意:数组大小是固定的,不能动态增长。

1.2.5顺序表结构定义

typedef struct {SLDataType* arr;      // 指向动态数组的指针int size;             // 当前有效元素个数int capacity;         // 当前最大容量
} SL;

这个结构体是用来实现顺序表(Sequential List) 的核心。

我们来打个比方帮助记忆:

成员类比含义
arr鞋柜的地址指向一块动态分配的内存空间,用来存放数据
size鞋柜里实际有几双鞋当前已经存了多少个有效元素
capacity鞋柜总共有多少格子最多能存多少个元素
逐个解释:

SLDataType* arr;

  • 这是一个指针,指向一片连续的内存空间。

这片空间是通过 malloc 动态申请的,比如:

ps->arr = (SLDataType*)malloc(sizeof(SLDataType) * 初始容量);
  • 它就是顺序表的“底层数组”

注意:虽然叫“数组”,但它是动态的,可以扩容。

int size;

  • 表示当前顺序表中有多少个有效元素
  • 插入一个元素,size++
  • 删除一个元素,size--
  • 初始值为 0。

int capacity;

  • 表示 arr 指向的空间最多能容纳多少个元素。
  • 当 size == capacity 时,就不能再插入了,必须先扩容

注意,其实这里有个错误:

typedef int SLDataType;
//这里我是为了方便讲解,才这么写
//但 Person 是一个结构体。如果你想要用顺序表来存“人”的信息// 让顺序表存储 Person 类型
typedef Person SLDataType;

这样 SLarr 就是指向 Person 数组的指针,才能存人!

否则你现在这个顺序表只能存整数,不能存通讯录!

所以准确代码一个是这样的:

//.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define NAME_MAX 10
#define PHONE_MAX 11
typedef Person SLDataType;typedef struct
{char name[NAME_MAX];char phone[PHONE_MAX];
};Person;//结构体本身
typedef struct{SLDataType* arr;;  // 一个指针,指向动态开辟的数组(鞋柜的位置)int size;      // 当前有效元素的个数(鞋柜里实际有几双鞋)int capacity;  // 容量,当前最大能容纳多少元素(鞋柜总共有多少格子)
}SL;

二、顺序表的创建、成长与销毁

现在,我们来让这个结构体“活”起来。

2.1 初始化(Init)—— 买一个初始大小的“鞋柜”

我们现在.h文件定义一下方法

//.h
//顺序表初始化
void SLInit(SL* ps);//给 ps->arr 分配内存(比如 malloc 一片空间)
//把 size 设为 0
//把 capacity 设为0
//.c
void SLInit(SL *ps)
{ps->arr = NULL;ps->size = ps->capacity = 0;
}

在完成初始化的具体实现之前,我先抛出一个问题为什么形参是SL* ps而不是SL s呢?

这里就是涉及到我们指针的传值传址问题了

2.1.1 关键问题:函数传参的两种方式

❌ 方法1:传值(错误写法)

void SLInit(SL ps)  // 传的是 SL 类型的变量(拷贝)
{ps.arr = (SLDataType*)malloc(sizeof(SLDataType) * 4);ps.size = 0;ps.capacity = 0;
}

问题出在哪?

假设你在 main 函数里这样调用:

int main()
{SL list;           // 定义一个顺序表变量(在栈上)SLInit(list);      // ❌ 传的是 list 的“副本”return 0;
}

执行过程如下:

  1. list 被创建(在 main 的栈帧中)
  2. 调用 SLInit 时,系统会拷贝一份 list 的副本给 ps
  3. SLInit 修改的是这个副本
  4. 函数结束,副本被销毁
  5. 原来的 list 什么都没变!→ arr 还是野指针,size 是垃圾值!

所以这种写法是错误的,因为根本不会访问到你需要初始化的SL,而是生成一个一模一样的副本

那么该如何正确的传参呢?

答案是:传指针

void SLInit(SL* ps)  // 传的是 &list(地址)
{ps->arr = (SLDataType*)malloc(sizeof(SLDataType) * 4);ps->size = 0;ps->capacity = 4;
}
int main()
{SL list;            // 定义变量SLInit(&list);      //  传地址return 0;
}

2.1.2 为什么不能传值?

再举个生活例子

假设你有个朋友叫小明,他手里拿着一个空盒子(list)。

  • ❌ 你把盒子复制一份给快递员(传值),说:“帮我把这盒子装满书。”

  • ✅ 快递员装满了复制的盒子,但小明手里的原盒子还是空的。

  • ✅ 你告诉快递员:“去小明家,地址是XXX,帮他把盒子装满。”(传指针)

  • ✅ 快递员去了小明家,直接操作原盒子 → 成功!

2.2 扩容(CheckCapacity)—— 鞋子多了,换个大鞋柜!

这是顺序表区别于普通数组最核心的功能。

2.2.1 扩容整体代码

老规矩,先在.h文件声明一下函数

//检查 扩容
void SLCheckCapacity(SL* ps);

具体实现:

//检查 扩容
void SLCheckCapacity(SL* ps)
{assert(ps);  // 防空指针// 如果当前空间已满,才需要扩容if (ps->size == ps->capacity){// 计算新容量:如果当前为0,初始给4;否则翻倍(三目操作符)int newCapacity = (ps->capacity == 0) ? 4 : (ps->capacity * 2);// 使用临时指针接收 realloc 结果,防止原指针丢失SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));// 检查扩容是否成功if (tmp == NULL){printf("realloc fail! Not enough memory.\n");exit(-1);  // 终止程序(实际项目中可返回错误码)}// 扩容成功,更新结构体成员ps->arr = tmp;              // 指向新空间ps->capacity = newCapacity; // 更新容量// ps->size 不变,仍是原来的有效元素个数}// 如果没满,什么都不做,直接返回

核心逻辑:

“插入前检查,空间不够就翻倍”

我们在test.c文件里面检测一下这个函数的实现功能,确实没问题。那么我们来讲一下这个函数是如何实现的吧!

2.2.2 手撕代码

第一部分:函数名及传参
void SLCheckCapacity(SL* ps)
  • SL* ps:传的是结构体的地址
  • 为什么传指针?因为我们要修改结构体内部的成员arr 和 capacity
  • 如果传 SL ps,改的是副本,原结构体不变 → 无效操作!

大家回想,这个刚刚不是具体分析过了吗,由于博主原来高中时期是文科生,还是比较相信学习知识的过程就是重复这一道理,所以比较重要的点,遇到了我就会再讲一遍,这样认真看博客的大家对于这个点也会更加记忆深刻,从而看完这个顺序表知识的博客,不仅仅是顺序表,可以是之前知识的查漏补缺,温习复习。

第二部分:断言
assert(ps);//防止空指针
  • assert 是“断言”,来自 <assert.h>
  • 作用:如果 ps == NULL,程序直接崩溃,报错
  • 相当于说:“你传个空指针来调用我?门都没有!”

类比:你让快递员去“NULL地址”送货?不可能!

因为压根没有这么一个地方

防御性编程的第一步:先保命,再干活。(这是个好习惯,干事细心为主)

第三部分 判断空间够不够
if (ps->size == ps->capacity)
  • ps->size:当前用了多少格子
  • ps->capacity:总共有多少格子

如果两者是相等的,其实也说明我们刚刚的初始化代码完成了使命,又或者说明目前空间不够(空间满了)需要补充

核心判断:只有“满了”才扩容!

第四部分 三目操作扩容
// 计算新容量:如果当前为0,初始给4;否则翻倍(三目操作符)
int newCapacity = (ps->capacity == 0) ? 4 : (ps->capacity * 2);

这是整个函数的“智慧大脑”!

我们分两种情况:

情况1:capacity == 0(第一次扩容)

  • 说明还没分配过空间
  • 我们给一个初始容量 4
  • 为什么不给 1?因为频繁 realloc 太慢!给 4 是“启动资金”

情况2:capacity > 0

  • 直接翻倍capacity * 2(大多都是翻倍,这是最优的,好像是根据倍率论吧,这个点笔者也不懂,大家感兴趣可以百度一下)
  • 为什么是倍增?因为:
    • 插入 n 个元素,总共 realloc 次数是:1 + 2 + 4 + 8 + ... + n ≈ 2n
    • 所以均摊每次插入是 O(1),非常高效!(这个点我们后面博客会讲到)

第五部分:扩容的具体实现

// 使用临时指针接收 realloc 结果,防止原指针丢失
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));

这是“扩容”的核心操作!

我们来拆解 realloc 的三大能力(这个知识点笔者没更,下一篇续上):


realloc 能做什么?

情况行为
原空间后面有足够空闲直接扩展原空间,arr 地址不变
原空间后面不够malloc 新空间,把旧数据拷贝过去,free 旧空间
申请失败返回 NULL原空间保持不变

 reallocmalloc 更智能,它会尝试“就地扩容”。

那么,为什么用 tmp 接收?其实这样更为安全一点

SLDataType* tmp = realloc(...);

错误写法:

ps->arr = realloc(ps->arr, ...); // 如果失败,ps->arr 变成 NULL,原数据丢了!

正确写法:用临时指针 tmp 接收,成功后再赋值。

 思想:先拿到新房本,再搬过去,防止“旧房被拆,新房没拿到”。

紧接着下一个问题:计算要申请多大?

newCapacity * sizeof(SLDataType)
  • newCapacity:新的格子数量
  • sizeof(SLDataType):每个格子多大(比如 Person 是 10+11=21 字节)
  • 相乘就是总字节数

第五部分:检测扩容是否成功

 if (tmp == NULL){printf("realloc fail! Not enough memory.\n");exit(-1);  // 终止程序(实际项目中可返回错误码)}

我是用条件判断一下,这时候就是使用tmp接受的好处了,这里直接调用就行,方便

  • ealloc 可能失败(比如内存不足)
  • 失败时返回 NULL
  • 如果你不检查,又是扩容失败的情况下,后面用 ps->arr 访问,就会段错误崩溃

进入函数内部:

  • 如果我们建立失败了,那就打印提示程序员这里打印失败,并且exit(-1)退出程序
  • 如果成功,则不会进入if函数内部,开始下面的代码

第六部分:更新结构体

那么我们扩容好了之后,不更新结构体,那就无法使用我们更新后的所有东西

ps->arr = tmp;              // 指向新空间
ps->capacity = newCapacity; // 更新容量

只有成功了才更新

  • ps->arr = tmp:正式切换到新空间
  • ps->capacity = newCapacity:告诉别人“我现在能装这么多”
  • size 不变:有效元素个数没变

2.2.3 画图帮助理解

由于博客不必视频讲解,不能用调试的方法给大家具体看看程序是如何走下去的

那我就想着用画图的方法来模拟一下

假设当前状态:

+---------------------+
|        list         |
|---------------------|
| arr  --> 0x1000     |
| size --> 4          |
| cap  --> 4          |
+---------------------+0x1000: [张三] [李四] [王五] [赵六]

调用 SLCheckCapacity(&list)后:

realloc(0x1000, 8 * sizeof(Person)) → 返回 0x2000(新地址)+---------------------+
|        list         |
|---------------------|
| arr  --> 0x2000     |  ← 更新
| size --> 4          |
| cap  --> 8          |  ← 更新
+---------------------+0x2000: [张三] [李四] [王五] [赵六] [空] [空] [空] [空]↑原数据被自动拷贝过来!

2.3 顺序表的销毁

鞋柜坏了,鞋柜得扔掉

有始有终,从堆上申请的内存,不用时必须手动释放,否则会造成内存泄漏

2.3.1 先看代码(逐行解剖)

void SLDestroy(SL* ps)
{if (ps->arr) //等价于  if(ps->arr != NULL){free(ps->arr);}ps->arr = NULL;ps->size = ps->capacity = 0;
}
第一部分:void SLDestroy(SL* ps)

  • 参数是 SL*,因为我们要修改结构体内部的指针和成员
  • 必须传地址,否则改的是副本
第二部分:条件判断
if (ps->arr)

作用:防止对 NULL 指针调用 free

等价于:

if (ps->arr != NULL)
第三部分:释放空间
free(ps->arr);
  • 释放 realloc 或 malloc 分配的堆内存
  • 关键点:只释放 arr 指向的空间,不释放 SL 结构体本身
  • SL 结构体可能是栈上变量(如 SL list;),不能 free 它!

 比喻:你租了一间仓库(arr),现在不用了就退租(free),但你的“仓库管理本”(SL)还可以留着下次用。

第四部分:防止野指针
ps->arr = NULL;

在我们使用free释放空间后,并不代表仓库直接平地消失了(空间),只是仓库我不用了(free),这个操作(ps->arr = NULL;)你可以视为“撕掉了仓库的地址标签”

第五部分:重新开始
ps->size = ps->capacity = 0;
  • size = 0:没有有效元素了
  • capacity = 0:当前容量为0,下次插入会从头开始扩容(0→4)

思想:把顺序表“归零”,变成一个干净的、可复用的状态

就像你清空一个U盘后,它可以被再次使用

free 是归还资源,= NULL 是自我约束,size=0 是重新开始。
三者合起来,才是真正的“优雅退出”。

三、数据的操作——增、删、查、改

有了“鞋柜”,我们开始放鞋子、找鞋子、扔鞋子。

3.1 尾插(PushBack)—— 往鞋柜最后空位放新鞋

3.1.1 先看代码

void SLPushBack(SL* ps, SLDataType x)
{assert(ps); SLCheckCapacity(ps);ps->arr[ps->size++] = x;
}

功能一句话:

把一个元素 x 插入到顺序表的末尾。

第一部分:函数名
void SLPushBack(SL* ps, SLDataType x)
  • SL* ps:顺序表指针(要修改内部数据,必须传地址)
  • SLDataType x:要插入的元素(值传递)

📌 注意:这里是值传递,不是指针!

意味着 xps->arr[i] 的一个副本

✅ 好处:安全,不会影响原数据
❌ 缺点:如果 SLDataType 很大(比如 1KB 的结构体),拷贝开销大

第二部分:扩容为插入准备
SLCheckCapacity(ps);

这是尾插的“安全阀”!

它做了什么?
  • 检查当前 size == capacity 是否满了
  • 如果满了,自动扩容(0→4, 4→8, 8→16...)
  • 扩容后 ps->arr 可能指向新地址,capacity 更新

📌 关键点:调用 SLCheckCapacity 后,ps->arr 可能已经变了!
SLPushBack 不关心,它只管“现在有地方插就行”。

第三部分:插入
ps->arr[ps->size++] = x;

这是整个函数的“灵魂”

我们来拆解这个“复合操作”:

等价于三步:

// 第1步:找到插入位置
int index = ps->size;  // 当前 size 就是要插入的位置// 第2步:赋值(插入)
ps->arr[index] = x;// 第3步:size + 1
ps->size++;

为什么 size++ 是后置?

  • ps->size 是当前元素个数
  • 插入位置是 arr[size](下标从0开始)
  • 插入后,size 才加1

 思想:先用旧 size 当下标,再让 size 自增。后置++的知识点

3.1.2 画图理解

+---------------------+
|        list         |
|---------------------|
| arr  --> 0x1000     |
| size --> 3          |
| cap  --> 4          |
+---------------------+0x1000: [A] [B] [C] [空]0   1   2   3↑size=3

调用 SLPushBack(&list, D)

  1. SLCheckCapacitysize=3 < cap=4 → 不扩容
  2. ps->arr[ps->size++] = D
    • ps->arr[3] = D → 写入 D
    • size++ → size=4

结果为:

0x1000: [A] [B] [C] [D]0   1   2   3

3.2 删除(Pop)—— 从鞋柜中间拿出一双鞋

3.2.1 先看代码:

void SLPopBack(SL* ps)
{assert(ps);assert(ps->size);//顺序表不为空//ps->arr[ps->size - 1] = -1;--ps->size;
}

和尾插的思想类似

功能一句话:

删除顺序表最后一个元素。

⚠️ 注意:它只是“逻辑上删除”,不是“物理上清空”。

第三句:

assert(ps->size);
  • ps->size 是当前元素个数
  • assert(ps->size) 等价于 assert(ps->size > 0)
  • 如果 size == 0(空表),程序崩溃

为什么必须有这一步?

--ps->size;  // 如果 size=0,变成 -1!

我们假设size = 0,那么再--则会变成-1

后果:

  • size 变成负数
  • 下次插入时 arr[-1] → 越界访问!段错误!
  •  所以必须保证:不能从空表中删元素!

最重要部分:

--ps->size;
  • 把 size 减 1
  • 表示“有效元素少了一个”

为什么是 --ps->size 而不是 ps->size--

  • --ps->size:先减,再用(虽然这里没用)
  • ps->size--:也可以,效果一样

📌 但 --ps->size 更高效一点点(少一个临时值)

3.3 查找(Find)—— 按名字找鞋子

3.3.1 先看代码

int SLFind(SL* ps, SLDataType x)
{assert(ps);for (int i = 0; i < ps->size; i++){if (ps->arr[i] == x){//找到啦return i;}}//没有找到return -1;
}

这个代码只是顺序表如何查找里面其中一个元素,不是最终版

直接看到第二步

第二步:遍历查找(线性搜索)
for (int i = 0; i < ps->size; i++)
  • 从下标 0 开始,一个一个看
  • 只看到 ps->size - 1(有效元素)
  • 这是为了后面的判断做准备的

 思想:顺序表是“紧凑存储”,只需查有效数据。

第三步:判断对比
if (ps->arr[i] == x)
  • 把当前元素 ps->arr[i] 和目标 x 比较
  • 如果“相等”,说明找到了

关键问题:什么叫“相等”?

  • 对 int:值相同 → 相等
  • 对 char:字符相同 → 相等
  • 对结构体:所有字段都相同? → 需要定义

3.4 修改

这一步我们在下一篇的通讯录实战来讲解

四、总结

4.1 回顾我们的核心函数

我们围绕顺序表的增、删、查、毁四大操作,深入剖析了每一个细节。

函数作用关键点
SLDestroy销毁顺序表free + NULL + 归零
SLPushBack尾插检查容量 + 插入 + size++
SLPopBack尾删检查非空 + --size
SLFind查找遍历 + 比较

4.2 关键思想提炼

1. 内存安全第一

  • free 后必须 arr = NULL → 防野指针
  • assert(ps) → 防空指针
  • assert(ps->size) → 防空删

2.防御性编程

  • 每个函数入口都 assert
  • 不信任调用者
  • 宁可崩溃,也不让程序“带病运行”

4.3 进阶之路(通讯录实战)

我们已经掌握了顺序表的“基本功”,下一步可以探索:

  • 任意位置插入/删除
  • 通讯录修改数据
  • 使用顺序表完成一个通讯录项目

http://www.xdnf.cn/news/18765.html

相关文章:

  • 数据库之两段锁协议相关理论及应用
  • 前端开发:详细介绍npm、pnpm和cnpm分别是什么,使用方法以及之间有哪些关系
  • Ansible 任务控制与事实管理指南:从事实收集到任务流程掌控
  • 面向过程与面向对象
  • AP服务发现中两条重启检测路径
  • Linux系统操作编程——http
  • 逆向抄数工程师能力矩阵:设备操作(±0.05mm 精度)× 曲面重构 ×GDT 公差分析
  • springboot项目每次启动关闭端口仍被占用
  • CTFshow系列——命令执行web53-56
  • GO学习记录八——多文件封装功能+redis使用
  • Coze用户账号设置修改用户昵称-前端源码
  • Vue 3 defineOptions 完全指南:让组件选项声明更现代化
  • `lock()` 和 `unlock()` 线程同步函数
  • Node.js(1)—— Node.js介绍与入门
  • maven-default-http-blocker (http://0.0.0.0/)
  • 设计模式4-建造者模式
  • 【AI论文】LiveMCP-101:针对支持多主体通信协议(MCP)的智能体在复杂查询场景下的压力测试与故障诊断
  • iptables 防火墙技术详解
  • 【AI编程】如何快速通过AI IDE集成开发工具来生成一个简易留言板系统
  • 使用 HandlerMethodReturnValueHandler 在SpringBoot项目 实现 RESTful API 返回值自动封装,简化开发
  • Linux系统网络管理
  • 积分排行样式
  • 动态住宅代理:跨境电商数据抓取的稳定解决方案
  • 3785定期复盘代码实现设计模式的越识应用
  • Java接口调用第三方接口时的超时处理策略
  • 浅谈为什么尾递归更高效?——从调用栈和汇编的视角
  • 开源零信任本地化部署实战指南:Keycloak + OpenZiti 完整方案
  • 机器学习-朴素贝叶斯
  • 常用的分布式ID设计方案
  • 可信医疗大数据来源、院内数据、病种数据及编程使用方案分析