还记得我第一次学链表时,对着那堆->符号和“结点+指针”的示意图发了半天呆。等到真把代码敲出来、跑起来,才意识到链表的本质其实特别朴素:它把“数据本身”和“数据之间的关系”拆成了一个个独立的小盒子,再用指针把它们串起来。这种结构换来的是内存布局上的灵活性,代价则是访问方式上的约束——而C++里的std::list,就是标准库帮你封装好的一整条双向链表。
这篇文章会围绕“C++链表和list”展开,适合刚接触数据结构、或者学了但总觉得“手写链表”和“list”之间隔着一层纸的读者。我会先把链表的底层原理讲透,再手写一遍单链表的核心操作,接着拆解std::list的接口与实现机制,最后聊聊工程里到底该怎么选型、有哪些坑。希望能帮你把这块知识真正串成一条线。
1. 链表的本质:为什么有了数组,还要有链表
1.1 数组的先天局限
数组在C++里是最基础也最常用的容器,它把元素连续地摆在一片内存里。访问第i个元素只需一次指针偏移,时间复杂度O(1),效率极高。但这种“连续摆放”也带来了两个绕不开的问题:
- 容量固定:普通数组在栈上定义时大小就得写死,后面想扩容只能自己开新数组、拷贝旧数据、释放旧内存,一套操作下来费时费力。
- 中间操作代价高:想在数组中间插入一个元素,比如在第5个位置塞进一个新值,后面所有元素都得往后挪一位;删除同理。最坏情况下复杂度O(n),元素越多,挪动成本越高。
你可以把数组想象成一排连在一起的火车车厢,车厢和车厢之间焊死了,想在中间加一节车厢,得把后半列车厢全部拆开、整体后移,再重新接上。这种设计在“随机访问”上非常出色,但在“频繁增删元素”的场景里就非常别扭。
1.2 链表的设计思路
链表换了一种思路:不再强制要求元素在内存里挨着放,而是让每个元素自己带一个“指向下一个元素的指针”,用指针把分散的内存块串联起来。
这种结构在C++里最基本的模样是这样:
cpp复制struct Node {
int data; // 真正存的数据
Node* next; // 指向下一个结点的指针
};
Node就是一个结点,里面既有数据,也有一个指向下一个Node的指针。多个Node通过next一个接一个串下去,最后一个结点的next指向nullptr,表示链表结束。
再回看火车的类比:链表更像是用挂钩连接的火车,每节车厢独立存在,想加车厢,只需把新车厢挂到合适的位置,改两个挂钩就行;想拆车厢,摘下挂钩、把前后车厢接上即可。不需要整体移动任何东西,代价是——你想找第5节车厢,只能从车头一节一节往后数,没法像数组那样直接“跳到”第5个位置。
1.3 链表家族的三种形态
链表不是一个单一结构,按照指针数量和连接方式,常见的有三种:
| 类型 | 结构特点 | 优缺点 |
|---|---|---|
| 单链表 | 每个结点只有一个next指针 |
省内存,遍历只能从头到尾单向走 |
| 双向链表 | 每个结点有prev和next两个指针 |
可双向遍历,删除结点更方便,但每个结点多占一个指针的内存 |
| 循环链表 | 尾结点指向头结点,形成环 | 适合循环调度场景,但要注意死循环风险 |
std::list在标准库里的实现就是双向链表,而绝大多数教材和考试里让你手写的,是单链表。理解单链表是基本功,理解双向链表能帮你更快学会用std::list。后面我会把两者对照着讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手写一个单链表:结点、插入、删除与遍历的完整实现
2.1 结构体定义与内存布局
手写链表,第一步就是把结点类型定义出来。核心就两个成员:数据域和指针域。
cpp复制struct Node {
int data;
Node* next;
explicit Node(int val) : data(val), next(nullptr) {}
};
构造函数里把next初始化为nullptr是个好习惯,避免出现“野指针”。每个结点用new在堆上创建,生命周期由我们自己管理。
为了把操作封装起来,我再包一个LinkedList类,内部维护一个头指针head_和链表长度size_:
cpp复制class LinkedList {
private:
Node* head_;
int size_;
public:
LinkedList() : head_(nullptr), size_(0) {}
};
这里有三个细节值得注意:
- 头指针不是结点:
head_只是一个指向第一个结点的指针。链表为空时head_为nullptr。 - 用
size_记录长度:虽然遍历也能拿到长度,但O(n)的代价在频繁需要长度时不划算。空间换时间。 - 析构函数必须释放所有结点:这是手写链表最容易翻车的地方,后面我会单独讲。
2.2 插入操作:头插、尾插与中间插入
插入是链表最核心的操作,也是最能体现“改指针”艺术的地方。先看最简单的头插法:
cpp复制void push_front(int val) {
Node* node = new Node(val);
node->next = head_;
head_ = node;
++size_;
