目录
[1.1 如何用C语言描述这个"智能数组"?](#1.1 如何用C语言描述这个“智能数组”?)
[1.2 手撕代码](#1.2 手撕代码)
[1.2.1 头文件](#1.2.1 头文件)
[1.2.2 宏定义](#1.2.2 宏定义)
[1.2.3 类型重定义](#1.2.3 类型重定义)
[1.2.4:定义"人"的结构体 ------ Person](#1.2.4:定义“人”的结构体 —— Person)
[2.1 初始化(Init)------ 买一个初始大小的"鞋柜"](#2.1 初始化(Init)—— 买一个初始大小的“鞋柜”)
[2.1.1 关键问题:函数传参的两种方式](#2.1.1 关键问题:函数传参的两种方式)
[2.1.2 为什么不能传值?](#2.1.2 为什么不能传值?)
[2.2 扩容(CheckCapacity)------ 鞋子多了,换个大鞋柜!](#2.2 扩容(CheckCapacity)—— 鞋子多了,换个大鞋柜!)
[2.2.1 扩容整体代码](#2.2.1 扩容整体代码)
[2.2.2 手撕代码](#2.2.2 手撕代码)
[第三部分 判断空间够不够](#第三部分 判断空间够不够)
[第四部分 三目操作扩容](#第四部分 三目操作扩容)
[2.2.3 画图帮助理解](#2.2.3 画图帮助理解)
[2.3 顺序表的销毁](#2.3 顺序表的销毁)
[2.3.1 先看代码(逐行解剖)](#2.3.1 先看代码(逐行解剖))
[第一部分:void SLDestroy(SL* ps)](#第一部分:void SLDestroy(SL* ps))
[3.1 尾插(PushBack)------ 往鞋柜最后空位放新鞋](#3.1 尾插(PushBack)—— 往鞋柜最后空位放新鞋)
[3.1.1 先看代码](#3.1.1 先看代码)
[3.1.2 画图理解](#3.1.2 画图理解)
[3.2 删除(Pop)------ 从鞋柜中间拿出一双鞋](#3.2 删除(Pop)—— 从鞋柜中间拿出一双鞋)
[3.2.1 先看代码:](#3.2.1 先看代码:)
[3.3 查找(Find)------ 按名字找鞋子](#3.3 查找(Find)—— 按名字找鞋子)
[3.3.1 先看代码](#3.3.1 先看代码)
[3.4 修改](#3.4 修改)
[4.1 回顾我们的核心函数](#4.1 回顾我们的核心函数)
[4.2 关键思想提炼](#4.2 关键思想提炼)
[1. 内存安全第一](#1. 内存安全第一)
[4.3 进阶之路(通讯录实战)](#4.3 进阶之路(通讯录实战))
在我们共同踏入数据结构这座宏伟殿堂的第一步,我非常理解大家的心情。我们刚刚经历了C语言和Python的洗礼,对指针、内存、结构体这些概念可能既熟悉又有些许畏惧。今天开始,我们将以C语言知识为基石,搭建起通往数据结构的第一座桥梁------顺序表。
PS:从C语言到数据结构------为什么要学这个
在我们写C语言程序时,如果我们需要管理100个学生的成绩,我们会怎么做?
你会毫不犹豫地说:用数组!
cpp
int scores[100];
没错,数组就是我们最早接触的、最原始的顺序存储结构。它非常好用,因为:
- 连续存储:所有数据在内存中紧挨在一起。
- 随机访问 :通过下标
scores[i]
可以直接访问任何一个元素,速度极快。
但是一个东西不会十全十美,当我们遇到数组的局限性时,该怎么办呢
"我申请了100个位置,但如果只有10个学生,是不是浪费了90个空间?"
"如果突然来了第101个学生,怎么办?数组大小是固定的,我无法改变啊!"
我们来举一个生活的例子:
你买了一个十个格子的鞋柜,一开始鞋子都能够放进去,但当你的鞋子慢慢积累下来,或者交了男女朋友,那十个可能就有点不够了。那这个时候你只能换一个更大的鞋柜来放这些鞋子。
数据结构要解决的,就是如何更灵活、更高效地组织和管理数据的问题。 顺序表,就是我们为了解决数组的固定大小问题,而设计的第一个"超级数组"。
一、顺序表的本质
顺序表的核心理念非常简单:"用时申请,不用时释放,不够了就扩容"。
它不再像普通数组那样在编译时就固定大小,而是在程序运行过程中,根据我们的需要,动态地申请内存、调整大小。
那么我们该如何去完成这个顺序表呢?
1.1 如何用C语言描述这个"智能数组"?
在C语言里,我们用什么来管理一块动态的内存?指针 。
我们用什么来记录当前用了多少?整型变量 。
我们用什么来记录总量有多大?另一个整型变量。
所以,我们需要把这三样东西"打包"在一起。用什么打包?结构体(struct)。
首先,我们先创建三个文件,来完成我们对顺序表的理解

现在我们现在 .h 文件里定义顺序表每个联系人的数据类型,以及这个"智能数组"本身:
cpp
//这里代码有个小错误,待我慢慢分析
//先声明等会要用到函数的头文件
#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 头文件
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
这三个是标准库头文件:
头文件 | 作用 |
---|---|
stdio.h |
提供输入输出函数,比如 printf , scanf |
stdlib.h |
提供动态内存管理函数,如 malloc , free ,还有 exit 等 |
assert.h |
提供断言宏 assert() ,用于调试时检查条件是否成立 |
PS:写 C 程序时,这三个几乎是标配,尤其是做数据结构。
1.2.2 宏定义
cpp
#define NAME_MAX 10
#define PHONE_MAX 11
意思是:把所有 NAME_MAX
替换成 10 PHONE_MAX的意思和上面一样
利用宏定义方便后面对变量的修改
1.2.3
类型重定义
cpp
typedef int SLDataType;
这是给 int
起了个别名叫 SLDataType
(Sequence List Data Type)。
为什么要这么做?
为了提高代码可扩展性!
假设今天存的是整数:
cpp
typedef int SLDataType;
明天我要把他改成字符类型:
cpp
typedef char SLDataType;
只需要改这一行,其他所有用到 SLDataType
的地方自动跟着变,不需要一个个改 int
→ char。
设计思想:解耦数据类型和容器逻辑。
这个方法大家一定要会用,不管是现在的学习还是后续就业对于项目的开发,都是必备的知识
1.2.4:定义"人"的结构体 ------ Person
cpp
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顺序表结构定义
cpp
typedef struct {
SLDataType* arr; // 指向动态数组的指针
int size; // 当前有效元素个数
int capacity; // 当前最大容量
} SL;
这个结构体是用来实现顺序表(Sequential List) 的核心。
我们来打个比方帮助记忆:
成员 | 类比 | 含义 |
---|---|---|
arr |
鞋柜的地址 | 指向一块动态分配的内存空间,用来存放数据 |
size |
鞋柜里实际有几双鞋 | 当前已经存了多少个有效元素 |
capacity |
鞋柜总共有多少格子 | 最多能存多少个元素 |
逐个解释:
SLDataType* arr;
- 这是一个指针,指向一片连续的内存空间。
这片空间是通过
malloc
动态申请的,比如:
cppps->arr = (SLDataType*)malloc(sizeof(SLDataType) * 初始容量);
- 它就是顺序表的"底层数组"
注意:虽然叫"数组",但它是动态的,可以扩容。
int size;
表示当前顺序表中有多少个有效元素。
插入一个元素,
size++
;删除一个元素,
size--
;初始值为 0。
int capacity;
表示
arr
指向的空间最多能容纳多少个元素。当
size == capacity
时,就不能再插入了,必须先扩容。
注意,其实这里有个错误:
cpp
typedef int SLDataType;
//这里我是为了方便讲解,才这么写
//但 Person 是一个结构体。如果你想要用顺序表来存"人"的信息
// 让顺序表存储 Person 类型
typedef Person SLDataType;
这样 SL
的 arr
就是指向 Person
数组的指针,才能存人!
否则你现在这个顺序表只能存整数,不能存通讯录!
所以准确代码一个是这样的:
cpp
//.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文件定义一下方法
cpp
//.h
//顺序表初始化
void SLInit(SL* ps);
//给 ps->arr 分配内存(比如 malloc 一片空间)
//把 size 设为 0
//把 capacity 设为0
cpp
//.c
void SLInit(SL *ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
在完成初始化的具体实现之前,我先抛出一个问题为什么形参是SL* ps而不是SL s呢?
这里就是涉及到我们指针的传值传址问题了
2.1.1 关键问题:函数传参的两种方式
❌ 方法1:传值(错误写法)
cpp
void SLInit(SL ps) // 传的是 SL 类型的变量(拷贝)
{
ps.arr = (SLDataType*)malloc(sizeof(SLDataType) * 4);
ps.size = 0;
ps.capacity = 0;
}
问题出在哪?
假设你在 main
函数里这样调用:
cpp
int main()
{
SL list; // 定义一个顺序表变量(在栈上)
SLInit(list); // ❌ 传的是 list 的"副本"
return 0;
}
执行过程如下:
list
被创建(在main
的栈帧中)- 调用
SLInit
时,系统会拷贝一份list
的副本 给ps
SLInit
修改的是这个副本- 函数结束,副本被销毁
- 原来的
list
什么都没变!→arr
还是野指针,size
是垃圾值!
所以这种写法是错误的,因为根本不会访问到你需要初始化的SL,而是生成一个一模一样的副本
那么该如何正确的传参呢?
答案是:传指针
cpp
void SLInit(SL* ps) // 传的是 &list(地址)
{
ps->arr = (SLDataType*)malloc(sizeof(SLDataType) * 4);
ps->size = 0;
ps->capacity = 4;
}
cpp
int main()
{
SL list; // 定义变量
SLInit(&list); // 传地址
return 0;
}
2.1.2 为什么不能传值?
再举个生活例子
假设你有个朋友叫小明,他手里拿着一个空盒子(list
)。
❌ 你把盒子复制一份给快递员(传值),说:"帮我把这盒子装满书。"
✅ 快递员装满了复制的盒子,但小明手里的原盒子还是空的。
✅ 你告诉快递员:"去小明家,地址是XXX,帮他把盒子装满。"(传指针)
✅ 快递员去了小明家,直接操作原盒子 → 成功!
2.2 扩容(CheckCapacity)------ 鞋子多了,换个大鞋柜!
这是顺序表区别于普通数组最核心的功能。
2.2.1 扩容整体代码
老规矩,先在.h文件声明一下函数
cpp
//检查 扩容
void SLCheckCapacity(SL* ps);
具体实现:
cpp
//检查 扩容
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 手撕代码
第一部分:函数名及传参
cpp
void SLCheckCapacity(SL* ps)
SL* ps
:传的是结构体的地址- 为什么传指针?因为我们要修改结构体内部的成员 (
arr
和capacity
) - 如果传
SL ps
,改的是副本,原结构体不变 → 无效操作!
大家回想,这个刚刚不是具体分析过了吗,由于博主原来高中时期是文科生,还是比较相信学习知识的过程就是重复这一道理,所以比较重要的点,遇到了我就会再讲一遍,这样认真看博客的大家对于这个点也会更加记忆深刻,从而看完这个顺序表知识的博客,不仅仅是顺序表,可以是之前知识的查漏补缺,温习复习。
第二部分:断言
cpp
assert(ps);//防止空指针
assert
是"断言",来自<assert.h>
- 作用:如果
ps == NULL
,程序直接崩溃,报错 - 相当于说:"你传个空指针来调用我?门都没有!"
类比:你让快递员去"NULL地址"送货?不可能!
因为压根没有这么一个地方
防御性编程 的第一步:先保命,再干活。(这是个好习惯,干事细心为主)
第三部分 判断空间够不够
cpp
if (ps->size == ps->capacity)
ps->size
:当前用了多少格子ps->capacity
:总共有多少格子
如果两者是相等的,其实也说明我们刚刚的初始化代码完成了使命,又或者说明目前空间不够(空间满了)需要补充
核心判断:只有"满了"才扩容!
第四部分 三目操作扩容
cpp
// 计算新容量:如果当前为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),非常高效!(这个点我们后面博客会讲到)
第五部分:扩容的具体实现
cpp
// 使用临时指针接收 realloc 结果,防止原指针丢失
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
这是"扩容"的核心操作!
我们来拆解 realloc
的三大能力(这个知识点笔者没更,下一篇续上):
realloc
能做什么?
情况 | 行为 |
---|---|
原空间后面有足够空闲 | 直接扩展原空间,arr 地址不变 |
原空间后面不够 | malloc 新空间,把旧数据拷贝过去,free 旧空间 |
申请失败 | 返回 NULL ,原空间保持不变 |
realloc
比malloc
更智能,它会尝试"就地扩容"。
那么,为什么用 tmp
接收?其实这样更为安全一点
cpp
SLDataType* tmp = realloc(...);
错误写法:
cpp
ps->arr = realloc(ps->arr, ...);
// 如果失败,ps->arr 变成 NULL,原数据丢了!
正确写法:用临时指针 tmp
接收,成功后再赋值。
思想:先拿到新房本,再搬过去,防止"旧房被拆,新房没拿到"。
紧接着下一个问题:计算要申请多大?
cpp
newCapacity * sizeof(SLDataType)
newCapacity
:新的格子数量sizeof(SLDataType)
:每个格子多大(比如Person
是10+11=21
字节)- 相乘就是总字节数
第五部分:检测扩容是否成功
cpp
if (tmp == NULL)
{
printf("realloc fail! Not enough memory.\n");
exit(-1); // 终止程序(实际项目中可返回错误码)
}
我是用条件判断一下,这时候就是使用tmp接受的好处了,这里直接调用就行,方便
ealloc
可能失败(比如内存不足)- 失败时返回
NULL
- 如果你不检查,又是扩容失败的情况下,后面用
ps->arr
访问,就会段错误崩溃!
进入函数内部:
- 如果我们建立失败了,那就打印提示程序员这里打印失败,并且exit(-1)退出程序
- 如果成功,则不会进入if函数内部,开始下面的代码
第六部分:更新结构体
那么我们扩容好了之后,不更新结构体,那就无法使用我们更新后的所有东西
cpp
ps->arr = tmp; // 指向新空间
ps->capacity = newCapacity; // 更新容量
只有成功了才更新!
ps->arr = tmp
:正式切换到新空间ps->capacity = newCapacity
:告诉别人"我现在能装这么多"size
不变:有效元素个数没变
2.2.3 画图帮助理解
由于博客不必视频讲解,不能用调试的方法给大家具体看看程序是如何走下去的
那我就想着用画图的方法来模拟一下
假设当前状态:
cpp
+---------------------+
| list |
|---------------------|
| arr --> 0x1000 |
| size --> 4 |
| cap --> 4 |
+---------------------+
0x1000: [张三] [李四] [王五] [赵六]
调用 SLCheckCapacity(&list)后:
cpp
realloc(0x1000, 8 * sizeof(Person)) → 返回 0x2000(新地址)
+---------------------+
| list |
|---------------------|
| arr --> 0x2000 | ← 更新
| size --> 4 |
| cap --> 8 | ← 更新
+---------------------+
0x2000: [张三] [李四] [王五] [赵六] [空] [空] [空] [空]
↑
原数据被自动拷贝过来!
2.3 顺序表的销毁
鞋柜坏了,鞋柜得扔掉
有始有终,从堆上申请的内存,不用时必须手动释放,否则会造成内存泄漏。
2.3.1 先看代码(逐行解剖)
cpp
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*
,因为我们要修改结构体内部的指针和成员 - 必须传地址,否则改的是副本
第二部分:条件判断
cpp
if (ps->arr)
作用 :防止对 NULL
指针调用 free
等价于:
cpp
if (ps->arr != NULL)
第三部分:释放空间
cpp
free(ps->arr);
- 释放
realloc
或malloc
分配的堆内存 - 关键点 :只释放
arr
指向的空间,不释放SL
结构体本身 SL
结构体可能是栈上变量(如SL list;
),不能free
它!
比喻 :你租了一间仓库(arr
),现在不用了就退租(free
),但你的"仓库管理本"(SL
)还可以留着下次用。
第四部分:防止野指针
cpp
ps->arr = NULL;
在我们使用free释放空间后,并不代表仓库直接平地消失了(空间),只是仓库我不用了(free),这个操作(ps->arr = NULL;)你可以视为"撕掉了仓库的地址标签"
第五部分:重新开始
cpp
ps->size = ps->capacity = 0;
size = 0
:没有有效元素了capacity = 0
:当前容量为0,下次插入会从头开始扩容(0→4)
思想 :把顺序表"归零",变成一个干净的、可复用的状态
就像你清空一个U盘后,它可以被再次使用
free
是归还资源,= NULL
是自我约束,size=0
是重新开始。三者合起来,才是真正的"优雅退出"。
三、数据的操作------增、删、查、改
有了"鞋柜",我们开始放鞋子、找鞋子、扔鞋子。
3.1 尾插(PushBack)------ 往鞋柜最后空位放新鞋
3.1.1 先看代码
cpp
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
功能一句话:
把一个元素 x
插入到顺序表的末尾。
第一部分:函数名
cpp
void SLPushBack(SL* ps, SLDataType x)
SL* ps
:顺序表指针(要修改内部数据,必须传地址)SLDataType x
:要插入的元素(值传递)
📌 注意 :这里是值传递,不是指针!
意味着 x
是 ps->arr[i]
的一个副本。
✅ 好处:安全,不会影响原数据
❌ 缺点:如果
SLDataType
很大(比如 1KB 的结构体),拷贝开销大
第二部分:扩容为插入准备
cpp
SLCheckCapacity(ps);
这是尾插的"安全阀"!
它做了什么?
- 检查当前
size == capacity
是否满了 - 如果满了,自动扩容(0→4, 4→8, 8→16...)
- 扩容后
ps->arr
可能指向新地址,capacity
更新
📌 关键点 :调用
SLCheckCapacity
后,ps->arr
可能已经变了!但
SLPushBack
不关心,它只管"现在有地方插就行"。
第三部分:插入
cpp
ps->arr[ps->size++] = x;
这是整个函数的"灵魂"!
我们来拆解这个"复合操作":
等价于三步:
cpp
// 第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 画图理解
cpp
+---------------------+
| list |
|---------------------|
| arr --> 0x1000 |
| size --> 3 |
| cap --> 4 |
+---------------------+
0x1000: [A] [B] [C] [空]
0 1 2 3
↑
size=3
调用 SLPushBack(&list, D)
:
SLCheckCapacity
:size=3 < cap=4
→ 不扩容ps->arr[ps->size++] = D
:ps->arr[3] = D
→ 写入 Dsize++
→size=4
结果为:
cpp
0x1000: [A] [B] [C] [D]
0 1 2 3
3.2 删除(Pop)------ 从鞋柜中间拿出一双鞋
3.2.1 先看代码:
cpp
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
//顺序表不为空
//ps->arr[ps->size - 1] = -1;
--ps->size;
}
和尾插的思想类似
功能一句话:
删除顺序表最后一个元素。
⚠️ 注意:它只是"逻辑上删除",不是"物理上清空"。
第三句:
cpp
assert(ps->size);
ps->size
是当前元素个数assert(ps->size)
等价于assert(ps->size > 0)
- 如果
size == 0
(空表),程序崩溃
为什么必须有这一步?
cpp
--ps->size; // 如果 size=0,变成 -1!
我们假设size = 0,那么再--则会变成-1
后果:
size
变成负数- 下次插入时
arr[-1]
→ 越界访问!段错误!- 所以必须保证:不能从空表中删元素!
最重要部分:
cpp
--ps->size;
- 把
size
减 1 - 表示"有效元素少了一个"
为什么是
--ps->size
而不是ps->size--
?
--ps->size
:先减,再用(虽然这里没用)ps->size--
:也可以,效果一样📌 但
--ps->size
更高效一点点(少一个临时值)
3.3 查找(Find)------ 按名字找鞋子
3.3.1 先看代码
cpp
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;
}
这个代码只是顺序表如何查找里面其中一个元素,不是最终版
直接看到第二步
第二步:遍历查找(线性搜索)
cpp
for (int i = 0; i < ps->size; i++)
- 从下标
0
开始,一个一个看 - 只看到
ps->size - 1
(有效元素) - 这是为了后面的判断做准备的
思想:顺序表是"紧凑存储",只需查有效数据。
第三步:判断对比
cpp
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 进阶之路(通讯录实战)
我们已经掌握了顺序表的"基本功",下一步可以探索:
- 任意位置插入/删除
- 通讯录修改数据
- 使用顺序表完成一个通讯录项目
完