各位正在啃《数据结构》的朋友,今天聊点实在的。线性表是我们遇到的第一个像样的数据结构,而它的顺序存储结构,也就是用数组来存数据,是整个数据结构课程的基石。C语言版本的顺序表看起来简单,但真正写起来、用起来,里面的门道不少——从一个隐藏的野指针,到扩容策略的选择,再到插入删除时元素移动的方向,哪个都能让新手头疼一阵。这篇东西我会把顺序表从概念到代码到踩坑,完整过一遍,保证你看完可以直接照着写出能运行的代码,也能在学校考试和面试里说出个一二三。
先解决一个最基础的问题:这个内容是什么,能做什么。
线性表的顺序存储结构,本质上就是用一段地址连续的存储单元,依次存放线性表中的数据元素。 在C语言里,最常见的实现方式就是数组。它解决的问题是“把一组具有先后关系的数据,整齐、连续地存放在内存里,并支持快速的随机访问”。适合谁来学?所有正在上数据结构课、准备考研408、或者想补基础的程序员都适用。特别是那些觉得“链表看不懂,顺序表总该行了吧”的同学,这篇文章能帮你把“应该行”变成“真的行”。
大概两周前,我重新把严蔚敏版的顺序表代码敲了一遍,又用动态数组的方式实现了扩容版本,期间踩了几个经典的坑。下面我把整个过程拆开揉碎,从设计思路到核心操作,再到问题排查,一步步讲清楚。
1. 顺序存储的核心设计思路:为什么选数组,以及数组怎么用
1.1 从逻辑结构到存储结构的映射
线性表的逻辑特征是:元素之间有且仅有一个直接前驱和一个直接后继,像一串珠子一样,串起来。顺序存储要做的就是,把这串“逻辑上连续”的珠子,原封不动地放到“物理上也连续”的内存空间里。
数组天然满足这个需求。定义一个 int a[10],编译器就帮你分配了10个连续的 int 大小的空间,a[0]和a[1]在内存里就是紧挨着的。所以,逻辑上的第i个元素,直接对应数组下标i-1的位置。访问任何一个元素,都只需要一次地址计算:address = base + (i-1) * sizeof(ElemType),这就是所谓的 随机存取。这个特性,是顺序表最大的底牌,也是后面所有操作复杂度的来源。
与链式存储比,顺序表不用额外存指针,空间利用率高,但插入和删除需要移动大量元素,代价高。这个“空间换不了时间,反而时间换空间”的特点,决定了顺序表的典型应用场景:数据量相对固定、查询多但增删少、追求随机访问速度。比如实现一个城市的电话号段管理表,数据基本不变,查得特别频繁,用顺序表就非常合适。
1.2 静态分配与动态分配:一个关乎生死的选择
教材里通常会讲两种分配方式。我敢说,大部分初学者都折在静态分配上。
静态分配是这样:
c复制#define MAXSIZE 100
typedef struct {
int data[MAXSIZE];
int length;
} SqList;
数组大小写死为100,一旦数据超过100个,程序直接崩给你看。而动态分配是这样:
c复制typedef struct {
int *data;
int capacity; // 当前容量
int length; // 当前元素个数
} SqList;
结构体里只存一个指针 data,真正存储空间用 malloc 在堆上申请,不够用就 realloc 扩容。我个人强烈建议,不管是考试还是实际工程,优先理解并掌握动态分配版本。 原因有三:一是考试大题和公司面试常有“设计一个可扩容的线性表”这样的题;二是静态分配版本在真正的项目里几乎无法应对未知的数据规模;三是动态分配的实现过程能把指针、内存管理这些C语言核心技能全都练一遍。
1.3 为什么“下标”和“位序”总让人犯迷糊
这是顺序表里最基础也最容易写错的点。线性表元素的位序从1开始,但数组下标从0开始。也就是说,第i个元素,存储在下标i-1的位置。
听起来再简单不过了,但真写代码时很多人会犯迷糊。尤其是插入和删除操作里,移动元素的for循环边界,差一个1就会越界或者漏元素。比如在第i个位置插入元素,循环应该是:
c复制for (int j = length; j >= i; j--) {
data[j] = data[j-1]; // 把第i个元素(下标i-1)及之后的元素后移
}
第一次写的时候别怕错,这个循环边界问题几乎是所有初学者的一道坎,我见过太多人在 j >= i 还是 j > i 之间纠结了。我的记忆技巧是:插入时,从最后一个元素开始,逐个往后挪一格,从后往前处理;删除时,从被删元素的下一个开始,逐个往前挪一格,从前往后处理。方向搞对了,边界就成功了一半。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 顺序表核心操作逐行解析:初始化、插入、删除、查找
2.1 初始化:malloc之后别忘记三件事
动态分配版顺序表的初始化,代码不长,但坑很深:
c复制#include <stdio.h>
#include <stdlib.h>
#define INIT_CAPACITY 10
typedef struct {
int *data;
int capacity;
int length;
} SqList;
// 初始化顺序表:申请初始容量
void InitList(SqList *L) {
L->data = (int *)malloc(INIT_CAPACITY * sizeof(int));
if (L->data == NULL) {
printf("内存分配失败\n");
exit(1);
}
L->capacity = INIT_CAPACITY;
L->length = 0; // 空表
}
三个关键点:
- malloc之后必须判空。 内存分配可能失败,虽然小概率,但一旦发生,后续的写入就是操作空指针,直接段错误。稳健的代码必须处理这种情况。
- length一定要初始化为0。 如果不初始化,这个值就是未知的垃圾值,后续所有基于length的操作全部失控。C语言里局部变量不自动清零,这是很多隐蔽bug的来源。
- capacity要记录。 这是动态扩容的基础,不记的话,你无法知道当前还能存几个元素。
注意:
malloc返回的是void*,在C语言中可以直接赋值给int*,不需要强转。写L->data = (int *)malloc(...)是C++风格,在纯C里也不报错,但现代C代码更推荐不加强转,代码更干净。
2.2 插入操作:从后往前挪,才能保证数据不覆盖
插入的完整逻辑分三步:检查合法性、移动元素、写入并更新length。
c复制// 在第i个位置上(位序从1开始),插入元素e
int ListInsert(SqList *L, int i, int e) {
// 1. 合法性检查
if (i < 1 || i > L->length + 1) {
printf("插入位置不合法\n");
return 0;
}
// 2. 检查容量,满了则扩容
if (L->length >= L->capacity) {
if (!ExpandList(L)) {
printf("扩容失败\n");
return 0;
}
}
// 3. 移动元素:从最后一个元素开始,先后移一位
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1];
}
// 4. 写入新元素,长度加1
L->data[i - 1] = e;
L->length++;
return 1;
}
为什么必须从后往前移动?因为如果从前往后,比如先让 data[i-1] = data[i-2],那原来的 data[i-1] 就被覆盖了,后面的元素再移就丢了数据。从后往前移动,每次都是把一个元素复制到它后面的空位上,前面的数据始终完好。
看这个例子:现有顺序表 [10, 20, 30, 40],要在第2个位置(下标1)插入 99:
j=4:data[4] = data[3],表变成[10, 20, 30, 40, 40]j=3:data[3] = data[2],表变成[10, 20, 30, 30, 40]j=2:data[2] = data[1],表变成[10, 20, 20, 30, 40]- 此时
j=1不满足j>=2循环结束,下标1和下标2都被挪开了 - 最后
data[1] = 99,表变成[10, 99, 20, 30, 40]
完美。这个过程中,原来第2个位置被“顶”到了第3个,第3个被顶到了第4个,依次类推。插入在中间位置的时间复杂度是O(n),在表尾插入是O(1),平均是O(n)。 理解这个平均复杂度,是理解顺序表瓶颈的关键。
2.3 删除操作:从前往后挪,删完记得减长度
删除相当于插入的逆操作:把第i个元素之后的所有元素,往前挪一格。
c复制// 删除第i个位置的元素,并将被删元素通过e返回
int ListDelete(SqList *L, int i, int *e) {
// 删除位置必须合法:1 <= i <= length
if (i < 1 || i > L->length) {
printf("删除位置不合法\n");
return 0;
}
*e = L->data[i - 1]; // 先取出被删元素
// 从第i+1个元素(下标i)开始,前移一位
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
L->length--; // 长度减1
return 1;
}
删除时为什么要从前往后?因为前面被删除后空出一个位置,后面的元素要依次往前补位。如果从后往前移动,后面的元素先挪到前面,前面还占着位置,会把还没处理的元素覆盖掉。
提醒:删除位置检查是
i > length,插入位置检查是i > length + 1。很多人删一次错一次,就是因为没理清这个区别。插入可以在表尾的下一个位置(即length+1)进行,但删除不能删除不存在的元素。
2.4 按值查找:朴素的顺序扫描
c复制// 查找第一个值为e的元素,返回其位序(从1开始),找不到返回-1
int LocateElem(SqList *L, int e) {
for (int i = 0; i < L->length; i++) {
if (L->data[i] == e) {
return i + 1; // 返回位序
}
}
return -1;
}
这个很简单,时间复杂度O(n)。需要注意一点:比较 L->data[i] == e 只适用于基本数据类型。如果顺序表存的是结构体,比如学生信息,你要按姓名查找,就不能直接 == 了,得用 strcmp 或者自定义比较函数。这是后续学更复杂结构时会用到的思路,但在顺序表这里,先把基础版本搞熟练。
3. 动态扩容:从定长数组到自适应顺序表
3.1 扩容策略:一次扩多少才划算
初学静态分配时,容量写死100,数据超过100就报错。动态分配解决了这个问题:满了就扩容。但扩容不是一次只多扩一个位置,那样性能极差——每插入一个元素就要 realloc 一次,内存拷贝的开销巨大。
常用的扩容策略有两种:
- 倍增扩容:容量翻倍,比如
capacity *= 2。 - 固定增量扩容:每次增加固定大小,比如
capacity += 10。
倍增的摊还时间复杂度是O(1),均摊下来每次插入虽然偶尔会因为扩容而变慢,但总体上代价很小。固定增量在频繁扩容时,看起来是“省着点用”,实际上会让 realloc 频繁触发,反而降低效率。
我用一个简单的均摊分析解释一下倍增的优势。假设初始容量是10,插入第11个元素时扩容到20,第21个时扩容到40……每次扩容都要把原数据全部拷贝到新空间。总拷贝次数约为 10 + 20 + 40 + ... + n/2,等比数列求和,约等于2n。也就是说,插入n个元素,数据移动总次数约2n次,均摊到每个元素上,就是O(1)。而如果每次只加1,总移动次数是 1 + 2 + 3 + ... + n,约等于n²/2,均摊下来是O(n)——这差别就大了。
3.2 realloc的两种结局:原地扩容与搬家
realloc 是C语言里一个很多人用不好但又绕不开的函数。它的行为取决于堆空间的情况:
- 如果后面有足够的连续空间,直接原地扩大,返回原来的指针。
- 如果没有连续空间,重新找一块大的空间,然后把原数据拷贝过去,释放旧空间,返回新指针。
所以,用 realloc 时有个经典陷阱:不要这样写:
c复制L->data = (int *)realloc(L->data, new_capacity * sizeof(int));
因为如果 realloc 失败,它会返回NULL,但同时原来的指针仍然有效,没有被释放。这时候你把NULL赋给 L->data,就把原指针弄丢了,旧数据也没法访问,还造成内存泄漏。正确写法是先用临时变量接住,成功了再赋值:
c复制int ExpandList(SqList *L) {
int new_capacity = L->capacity * 2;
int *new_data = (int *)realloc(L->data, new_capacity * sizeof(int));
if (new_data == NULL) {
return 0; // 扩容失败,原来的数据仍然有效
}
L->data = new_data;
L->capacity = new_capacity;
return 1;
}
这样写,即使 realloc 失败,L->data 依然指向旧数据,表还能正常工作,只是插入操作返回失败。
3.3 内存释放:free和置空一个都不能少
动态分配的顺序表,用完之后必须释放内存:
c复制void DestroyList(SqList *L) {
if (L->data != NULL) {
free(L->data);
L->data = NULL; // 防悬空指针
}
L->capacity = 0;
L->length = 0;
}
释放后把指针置NULL,是为了防止后续误用——如果free了但指针还指向那块内存,它就成了“悬空指针”,一旦再次访问就是未定义行为。很多“神出鬼没”的崩溃,就是这么来的。这个习惯,从入门就要养成。
实操心得:在写测试代码时,我习惯在每个功能函数后加一打印,比如
printf("After insert length=%d\n", L.length);。这样一旦程序中途崩溃,你能快速定位是哪个操作引发的,而不是对着整个程序瞎猜。
4. 顺序表实战经验:构建一个完整的可运行示例
4.1 完整代码整合:测试驱动开发思维
光看碎片代码没用,得组装起来跑。这里我给一份完整可运行的代码,包含初始化、插入、删除、查找、打印、扩容、销毁,以及一个简单的测试用例。
c复制#include <stdio.h>
#include <stdlib.h>
#define INIT_CAPACITY 5 // 故意设小一点,方便测试扩容
typedef struct {
int *data;
int capacity;
int length;
} SqList;
// 初始化
void InitList(SqList *L) {
L->data = (int *)malloc(INIT_CAPACITY * sizeof(int));
if (L->data == NULL) {
printf("内存分配失败\n");
exit(1);
}
L->capacity = INIT_CAPACITY;
L->length = 0;
}
// 扩容(倍增)
int ExpandList(SqList *L) {
int new_capacity = L->capacity * 2;
int *new_data = (int *)realloc(L->data, new_capacity * sizeof(int));
if (new_data == NULL) {
return 0;
}
L->data = new_data;
L->capacity = new_capacity;
printf("已扩容,新容量:%d\n", L->capacity);
return 1;
}
// 插入
int ListInsert(SqList *L, int i, int e) {
if (i < 1 || i > L->length + 1) {
return 0;
}
if (L->length >= L->capacity) {
if (!ExpandList(L)) {
return 0;
}
}
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1];
}
L->data[i - 1] = e;
L->length++;
return 1;
}
// 删除
int ListDelete(SqList *L, int i, int *e) {
if (i < 1 || i > L->length) {
return 0;
}
*e = L->data[i - 1];
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
L->length--;
return 1;
}
// 按值查找
int LocateElem(SqList *L, int e) {
for (int i = 0; i < L->length; i++) {
if (L->data[i] == e) {
return i + 1;
}
}
return -1;
}
// 打印
void PrintList(SqList *L) {
printf("当前表:");
for (int i = 0; i < L->length; i++) {
printf("%d ", L->data[i]);
}
printf("(长度=%d,容量=%d)\n", L->length, L->capacity);
}
// 销毁
void DestroyList(SqList *L) {
if (L->data != NULL) {
free(L->data);
L->data = NULL;
}
L->capacity = 0;
L->length = 0;
}
int main() {
SqList list;
InitList(&list);
// 连续插入6个元素,触发一次扩容
for (int i = 1; i <= 6; i++) {
ListInsert(&list, i, i * 10);
}
PrintList(&list);
// 在第3个位置插入 99
ListInsert(&list, 3, 99);
PrintList(&list);
// 删除第4个元素
int deleted;
ListDelete(&list, 4, &deleted);
printf("删除的元素:%d\n", deleted);
PrintList(&list);
// 查找值为50的元素
int pos = LocateElem(&list, 50);
printf("50所在位置(位序):%d\n", pos);
DestroyList(&list);
return 0;
}
这个程序跑起来,你会看到扩容日志、插入删除过程、查找结果,所有操作一目了然。强烈建议你亲手把这套代码敲一遍,然后改一改初始容量、改一改插入的位置,观察输出变化。 C语言数据结构的学习,纸上谈兵没有任何意义,手指的肌肉记忆才靠得住。
4.2 参数选择的经验:initial capacity怎么定
你可能会问,初始容量设多大合适?这直接关系到程序性能。
- 设太小,比如1,则插入第二个元素就要扩容,数据少时扩容频率高。
- 设太大,比如100000,但实际只用了10个,那就是白白浪费内存。
经验法则:如果能估算数据量,初始容量设为估算值的1.5~2倍,最省事。如果完全没有头绪,设16或32是常见的折中方案。这个在纯C的容器实现中非常普遍,Redis的SDS、一些C语言哈希表实现,初始容量基本都是2的幂次(8、16、64),方便做位运算和扩容对齐。
4.3 边界条件的自测清单
做实验或者考试时,以下测试用例必须跑一遍:
| 测试场景 | 测试动作 | 期望结果 |
|---|---|---|
| 空表插入 | 在位置1插入元素 | 成功,length=1 |
| 空表删除 | 删除位置1 | 失败,返回0 |
| 在length+1处插入 | 表长5,在位置6插入 | 成功,追加到尾部 |
| 在length+2处插入 | 表长5,在位置7插入 | 失败,返回0 |
| 删除最后一个元素 | 删除位置length | 成功,length-1 |
| 删除位置0 | 删除位置0 | 失败,返回0 |
| 查找不存在的值 | 值为999 | 返回-1 |
| 插入触发扩容 | 表已满时插入 | 成功,capacity翻倍 |
| 全部删除后继续删除 | 空表再删除 | 失败,返回0 |
这些边界情况,是隐藏bug的高发区。能把上表全部测试通过的实现,才算得上合格的顺序表。
5. 常见问题与排查技巧实录:那些让人崩溃的瞬间
5.1 越界访问:看似正常,实则定时炸弹
最经典的问题是插入时循环条件写错,导致数组越界却“看起来正常”。比如,把插入的循环写成:
c复制for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j-1];
}
在 i=1 且 length=0(空表)的时候,j=0,循环条件 0>=1 为假,没执行,看似正常。但如果 i=1 且 length=5,你插入后,原本下标0处的元素被移到了下标1……等等,上面这段其实是正确的。真正错误的是很多人会写成:
c复制for (int j = L->length; j > i; j--) { // 注意是 j > i,不是 j >= i
这会导致原第i个元素没有被移动,直接写新元素时把它覆盖了。测试时如果只在表尾追加,永远发现不了这个bug。一定要测试中间位置插入。越界访问的问题在于,它不一定立刻崩溃,可能只是数据错乱,等到程序真要崩的时候,位置已经很难定位了。
5.2 忘记扩容检查:最隐蔽的“内存写入错误”
初学动态分配时,最容易漏掉的就是插入前的容量检查。我刚开始写的时候,插到第11个元素(初始容量10)后,程序没有立刻报错,反而正常输出了。我还以为代码写对了。结果有一天数据多了,直接段错误。原因就是:malloc 申请的10个 int 空间,你写入第11个、第12个元素时,写到了堆上别的内存区域,这是未定义行为。有时候那块内存是空闲的,你“侥幸”写进去了,程序还能跑;有时候那块内存是别的变量在用的,数据就被你悄悄破坏了。这个bug隐蔽性极强,排查方法就是检查插入函数里是否在 data[L->length] 写入前,先判断了 length < capacity。
5.3 动态扩容后,原来的指针还能用吗
有人问我:“realloc 之后,以前保存的 L->data[i] 的地址还能用吗?”答案是:不一定。如果 realloc 原地扩容,地址不变,还能用;如果搬家了,旧地址就失效了。所以,千万不要在顺序表扩容前后,长期保存某个元素的地址或指针,否则扩容后它可能就变成悬空指针。
这在实际项目中很常见:你拿到一个指向顺序表元素的指针,准备一会儿再用,结果中间插入操作触发了扩容,元素搬家了,你的指针就悬了。解决办法是,要么在每次操作后重新获取地址,要么不要长期持有元素指针,只持有下标,用时再取。
5.4 动态扩容与内存碎片问题
虽然倍增扩容的均摊时间复杂度是O(1),但频繁 realloc 可能带来内存碎片。尤其是在长时间运行的嵌入式系统或服务器程序里,反复扩容和释放会让堆空间变得支离破碎,最终导致 malloc 虽然内存总量足够,却分配不出连续的块。这个问题在顺序表上比链表更严重,因为数组要求连续空间。
如果遇到这种场景,有几个选项:一是预估一个合理的初始容量,尽量减少扩容次数;二是换用链表或块状列表(如分块链表);三是实现一个简单的内存池。当然,对大部分学习场景和一般应用,倍增扩容已经足够好,不需要过度设计。
5.5 速查表:顺序表常见问题与排查手段
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 程序段错误/崩溃 | 越界访问、空指针、未初始化指针 | 检查循环边界;初始化时判空;用 gdb 定位崩溃行 |
| 插入后数据丢失 | 循环方向错误或边界错误 | 检查插入循环是 j >= i 还是 j > i;测试中间位置插入 |
| 表满后插入无效 | 缺少扩容逻辑 | 检查插入函数是否有容量判断 |
| 删除后出现垃圾值 | 删除后length未减或未初始化 | 检查length是否更新 |
| 释放后访问出错 | 悬空指针 | 释放后立即置NULL |
| 数据错乱且无规律 | 扩容量设置不当 | 检查是否溢出或下标计算错误 |
realloc 后原数据丢失 |
直接把返回值赋给原指针 | 使用临时变量接住realloc返回值 |
6. 进阶拓展:顺序表还能怎么用
顺序表本身简单,但它衍生出的几个变形,在后续课程和工程中非常常用。这里提几个方向,供学有余力的朋友探索。
6.1 有序表的合并与去重
如果两个顺序表都是有序的,合并成一个有序表,可以用归并的思想:两个指针分别指向两个表头,比较大小,小的先放进新表。这个小算法在“合并两个有序数组”的LeetCode题里出现过无数次,也是归并排序的基础。核心复杂度O(m+n),且不用对元素排序。
6.2 集合的“或”操作
如果顺序表存储的是集合,那“并集”操作可以先复制一个表,然后把另一个表中不存在的元素逐个追加进去。这就用到了按值查找:LocateElem 返回-1说明不在表里。这个过程,你可以体会数据结构如何服务于业务逻辑。
6.3 动态数组在工程中的应用
很多人觉得顺序表是“教材玩具”,但真实世界里,动态顺序表无处不在。比如Python的list底层就是动态数组(并且扩容策略是约1.125倍,兼顾空间和时间),Redis的SDS也包含动态扩展的字节数组,Java的ArrayList也是动态数组。你在这里学到的扩容策略、均摊分析,在理解这些高级语言底层时完全可以复用。
有一点值得注意: 顺序表的扩容策略在不同语言里是不一样的。Python list的扩容倍数约为1.125,而Java ArrayList是1.5倍,C++ vector是2倍。为什么各不相同?因为语言设计者在“空间利用率”和“扩容频率”之间做了不同的权衡。你在设计自己的数据容器时,也需要根据业务特点做同样的权衡。
7. 学习路径建议与个人体会
写了这么多,其实核心就一句话:顺序表虽然简单,但它是理解“物理结构如何服务逻辑结构”的第一个窗口。你在这个阶段养成的习惯——malloc判空、边界检查、内存释放置空、赋值前查容量——会伴随你整个C语言编程生涯。
根据我自己的经验,有几个建议可以帮你在学习阶段少走弯路。
第一,别只看书,一定要动手敲代码。数据结构这门课,唯一的学法是“自己实现一遍”。哪怕是照抄教材,也要一行一行敲,敲完了再自己加功能、改参数、制造bug再修bug,才能内化。
第二,考试前把复杂度分析搞透。插入和删除的平均移动次数是(n/2),查找的平均比较次数是(n+1)/2。这些推导过程,比背结论重要得多。面试官问“顺序表和链表有什么区别”时,本质是在考察你对这些复杂度的理解。
第三,善用调试工具。如果程序崩溃,别瞎猜,用 gdb 跑一下,看看崩溃在哪一行,用 valgrind 查内存泄漏和越界。这些工具虽然上手有点门槛,但学会了就是效率利器。
第四,把代码留下来。我自己的习惯是给顺序表写一个单独的 .c 文件,配上简单的测试函数。之后学栈、学队列、学串的时候,还能复用这份代码做扩展。所有基于线性表的结构,底层要么是顺序存储,要么是链式存储,你把顺序表搞熟了,后面能省很大的力气。
最后再分享一个我调试时用的小技巧:在扩容函数里加打印日志。当我看到日志里的扩容次数和时机,就能直观判断初始容量设置得合不合理。比如你插入了1000个元素,结果它扩容了10次,那你就要想想是不是初始容量给得太小,或者扩容倍数太保守。这种“看日志调参”的思路,在真正的开发里也非常实用。
顺序表这一步,踩稳了,后面的路会顺很多。下一个章节点我会接着聊线性表的链式存储——那也是另一场“指针的冒险”。
