序列式容器:list 双向链表的特性与用法
在C++ STL的序列式容器家族中,list 是与 vector 齐名却风格迥异的核心成员。如果说 vector 是"动态数组",主打随机访问高效,那么 list 就是"双向链表",凭借任意位置插入/删除的高效性,成为解决特定场景问题的"利器"。
很多开发者在日常开发中,习惯于优先使用 vector,却对 list 的特性和适用场景一知半解,导致在需要频繁插入/删除元素的场景中,写出性能低下的代码;也有不少初学者被 list 的迭代器用法、内存模型搞得晕头转向。
今天这篇博客,我们就来全面拆解 list 双向链表------从底层原理、核心特性,到基础用法、进阶实操,再到避坑指南和场景对比,用通俗的讲解+可直接运行的示例代码,让你真正吃透 list,知道"什么时候用、怎么用、怎么用得高效",做到灵活运用、规避隐患。
一、list 是什么?核心定位与底层原理
list 是 STL 标准模板库中的序列式容器,底层基于双向循环链表实现(不同编译器细节略有差异,但核心是双向链表),本质是封装了双向链表的模板类。
与 vector 的"连续内存"不同,list 的每个元素(称为"节点")都独立存储在内存中,节点之间通过两个指针(前驱指针 prev、后继指针 next)相互连接,形成双向链表结构,最后一个节点的 next 指针指向第一个节点,构成循环(部分实现为非循环,但核心逻辑一致)。
list 的底层节点结构(简化理解)
我们可以用一个简单的结构体,直观理解 list 节点的底层实现(实际 STL 实现更复杂,包含分配器等,但核心逻辑不变):
cpp
// list 节点简化结构(双向链表节点)
template <typename T>
struct ListNode {
T data; // 节点存储的数据
ListNode* prev; // 指向前驱节点的指针
ListNode* next; // 指向后继节点的指针
// 构造函数
ListNode(const T& val) : data(val), prev(nullptr), next(nullptr) {}
};
// list 本质是封装了节点的指针(头指针/尾指针)和节点个数
template <typename T>
struct List {
ListNode<T>* head; // 头节点指针
ListNode<T>* tail; // 尾节点指针
size_t size; // 节点个数(元素个数)
// ... 其他成员函数(插入、删除、遍历等)
};
这种双向链表结构,决定了 list 的核心特性------不支持随机访问,但支持任意位置高效插入/删除,这也是它与 vector 最本质的区别。
list 的核心特性(与 vector 对比,一目了然)
为了让大家快速区分 list 和 vector 的差异,这里先做一个核心对比,后续会逐一展开讲解 list 的特性细节:
| 特性 | list(双向链表) | vector(动态数组) |
|---|---|---|
| 内存结构 | 非连续内存(节点独立,指针连接) | 连续内存(动态数组) |
| 随机访问 | 不支持(只能通过迭代器遍历,O(n)) | 支持(下标访问,O(1)) |
| 插入/删除(任意位置) | 高效(仅修改指针,O(1)) | 低效(需移动元素,O(n)) |
| 插入/删除(尾部) | 高效(O(1)) | 高效(无扩容时 O(1),扩容时 O(n)) |
| 内存开销 | 较大(每个节点额外存储两个指针) | 较小(仅存储元素,无额外指针开销) |
| 迭代器特性 | 双向迭代器(支持 ++、--,不支持 +n、-n) | 随机访问迭代器(支持 ++、--、+n、-n) |
| 扩容机制 | 无扩容(节点独立分配,按需新增/释放) | 自动扩容(2倍/1.5倍,重新分配内存+拷贝元素) |
二、list 核心特性详解(为什么用、什么时候用)
结合底层双向链表结构,我们逐一拆解 list 的核心特性,搞懂它的优势、劣势,以及适用场景,避免盲目使用。
1. 核心优势:任意位置插入/删除高效(O(1))
这是 list 最核心、最独特的优势,也是它存在的核心价值。由于 list 的节点是独立存储的,插入/删除元素时,无需移动其他元素,只需修改插入/删除位置前后节点的 prev 和 next 指针,就能完成操作,时间复杂度为 O(1)。
举个直观的例子:如果有一个包含10000个元素的容器,要在第5000个位置插入一个元素:
-
vector:需要移动第5000到第9999个元素(共5000个元素),时间复杂度 O(n),数据量越大,效率越低;
-
list:只需找到第5000个节点,修改它前后节点的指针,插入新节点,时间复杂度 O(1)(找到节点的时间除外,若已找到节点,插入本身是 O(1))。
注意:这里的 O(1) 是"插入/删除操作本身"的时间复杂度,若需要先找到插入/删除的位置(如通过遍历找到第n个元素),则遍历的时间复杂度是 O(n),但插入/删除操作本身依然是 O(1)。
2. 核心劣势:不支持随机访问,遍历效率低
由于 list 的内存是非连续的,节点之间通过指针连接,因此无法像 vector 那样通过下标 [ ] 直接访问元素。要访问 list 中的第n个元素,只能从头部(或尾部)开始,通过迭代器一步步遍历,直到找到目标节点,时间复杂度为 O(n)。
这也是 list 最大的劣势------如果你的场景需要频繁访问元素(如根据下标读取、查找元素),使用 list 会导致性能严重下降,此时优先选择 vector。
3. 其他特性:无扩容、内存开销大、迭代器稳定
-
无扩容机制:list 的节点是按需分配、独立释放的,插入元素时,直接新建一个节点,修改指针即可;删除元素时,释放该节点的内存,无需像 vector 那样整体扩容、拷贝元素,因此不会出现 vector 那样的扩容性能损耗。
-
内存开销大:每个节点除了存储元素本身,还需要额外存储两个指针(prev 和 next),用于连接前后节点。如果存储的元素是占用内存较小的类型(如 int、char),指针的额外开销会非常明显(比如 int 占4字节,两个指针占8字节,额外开销100%)。
-
迭代器稳定(除了被删除节点的迭代器) :与 vector 不同,list 插入/删除元素时,不会影响其他节点的迭代器(因为无需移动元素,也无需重新分配内存)。只有指向"被删除节点"的迭代器会失效,其他迭代器依然有效,这也是 list 的一个重要优势。
list 的适用场景(精准使用,避免踩坑)
结合上述特性,list 适合以下场景,其他场景优先选择 vector 或其他容器:
-
需要频繁在任意位置插入/删除元素(如日志插入、队列操作、频繁修改的列表);
-
数据量不确定,且插入/删除操作远多于访问操作;
-
需要保证迭代器稳定(插入/删除后,其他迭代器依然可用)的场景;
-
无需随机访问元素,仅需遍历元素的场景(如遍历输出、批量处理)。
三、list 基础用法:从初始化到常用操作(附示例)
在使用 list 之前,需包含对应的头文件 #include <list>,并建议使用 std 命名空间(或显式调用 std::list)。下面从初始化开始,逐一讲解 list 的核心基础操作,搭配可直接复制运行的示例代码,上手更高效。
1. list 的初始化(5种常用方式)
list 的初始化方式与 vector 类似,但由于其内存结构不同,初始化时无需考虑容量,只需关注元素个数和初始值即可。
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
// 方式1:默认初始化,空list,元素个数为0
list<int> l1;
// 方式2:初始化n个元素,每个元素默认值为0(内置类型),自定义类型需有默认构造
list<int> l2(5); // l2: [0, 0, 0, 0, 0]
// 方式3:初始化n个元素,每个元素赋值为指定值(推荐,高效)
list<int> l3(5, 3); // l3: [3, 3, 3, 3, 3]
// 方式4:通过已有数组/vector/list初始化(拷贝构造)
int arr[] = {1, 2, 3, 4, 5};
list<int> l4(arr, arr + 5); // l4: [1, 2, 3, 4, 5](左闭右开区间)
list<int> l5(l4); // 拷贝l4,l5与l4元素完全一致
// 方式5:通过初始化列表初始化(C++11及以上支持,最简洁)
list<int> l6 = {1, 2, 3, 4, 5};
list<int> l7{10, 20, 30}; // 省略等号,同样有效
// 打印测试(后续会讲遍历方式,此处暂用简单遍历)
for (auto it = l7.begin(); it != l7.end(); ++it) {
cout << *it << " ";
}
return 0;
}
2. list 核心成员函数(必学,重点区分与 vector 的差异)
list 的成员函数与 vector 有部分重合,但由于其特性不同,部分函数的用法和效率有明显差异,重点掌握以下高频函数,避免混淆。
(1)访问元素(2种方式,无下标访问)
注意:list 不支持下标 [ ] 访问,也不支持 at() 函数访问(因为无法随机访问),只能访问头部和尾部元素,或通过迭代器遍历访问。
-
front():访问第一个元素,时间复杂度 O(1)(高频使用); -
back():访问最后一个元素,时间复杂度 O(1)(高频使用)。
cpp
list<int> l = {1, 2, 3, 4, 5};
cout << l.front() << endl; // 输出1(第一个元素)
cout << l.back() << endl; // 输出5(最后一个元素)
// 错误示例:list不支持下标访问和at()访问
// cout << l[0] << endl; // 编译报错
// cout << l.at(2) << endl; // 编译报错
(2)插入元素(3种高频方式,重点掌握)
list 的插入函数是其核心优势,支持头部、尾部、任意位置插入,操作高效,时间复杂度 O(1)(插入本身,找到位置除外)。
-
push_back():在 list 尾部插入一个元素,时间复杂度 O(1)(高频使用); -
push_front():在 list 头部插入一个元素,时间复杂度 O(1)(vector 不支持此函数,或支持但效率极低); -
insert():在指定位置插入元素/元素组,时间复杂度 O(1)(插入本身),需配合迭代器指定位置。
cpp
list<int> l = {1, 2, 3};
// 1. push_back:尾部插入
l.push_back(4); // l: [1, 2, 3, 4]
l.push_back(5); // l: [1, 2, 3, 4, 5]
// 2. push_front:头部插入(vector无此函数,list核心优势)
l.push_front(0); // l: [0, 1, 2, 3, 4, 5]
l.push_front(-1); // l: [-1, 0, 1, 2, 3, 4, 5]
// 3. insert:指定位置插入(需配合迭代器)
// 步骤1:找到插入位置(遍历找到第3个元素,下标2,迭代器指向1)
auto it = l.begin();
++it; // 指向0
++it; // 指向1(第3个元素)
// 插入单个元素:在it位置插入10
l.insert(it, 10); // l: [-1, 0, 10, 1, 2, 3, 4, 5]
// 插入多个相同元素:在it位置插入2个20(it仍指向1,插入后1在20后面)
l.insert(it, 2, 20); // l: [-1, 0, 10, 20, 20, 1, 2, 3, 4, 5]
// 插入另一个list的元素:在尾部插入l2的所有元素
list<int> l2 = {6, 7, 8};
l.insert(l.end(), l2.begin(), l2.end()); // 尾部插入,l: [-1,0,10,20,20,1,2,3,4,5,6,7,8]
(3)删除元素(4种高频方式,高效且稳定)
list 的删除函数同样高效,支持头部、尾部、任意位置、指定值删除,且删除后除了指向被删除节点的迭代器,其他迭代器均有效。
-
pop_back():删除尾部最后一个元素,时间复杂度 O(1)(高频使用); -
pop_front():删除头部第一个元素,时间复杂度 O(1)(vector 不支持此函数); -
erase():删除指定位置/指定区间的元素,时间复杂度 O(1)(删除本身); -
remove(val):删除 list 中所有值等于 val 的元素,时间复杂度 O(n)(需遍历所有节点)。
cpp
list<int> l = {-1, 0, 10, 20, 20, 1, 2, 3, 4, 5};
// 1. pop_back:删除尾部元素
l.pop_back(); // l: [-1, 0, 10, 20, 20, 1, 2, 3, 4]
// 2. pop_front:删除头部元素
l.pop_front(); // l: [0, 10, 20, 20, 1, 2, 3, 4]
// 3. erase:删除指定位置元素
auto it = l.begin();
++it; // 指向10
l.erase(it); // 删除10,l: [0, 20, 20, 1, 2, 3, 4]
// erase:删除指定区间元素(从it到l.end()-1,左闭右开)
it = l.begin() + 1; // 错误!list迭代器不支持 +n,只能一步步遍历
it = l.begin();
++it; // 指向第一个20
auto it2 = l.end();
--it2; // 指向4
l.erase(it, it2); // 删除[20, 20, 1, 2, 3],l: [0, 4]
// 4. remove:删除所有值为20的元素(上面示例中已删除,此处换个例子)
list<int> l2 = {20, 10, 20, 30, 20, 40};
l2.remove(20); // 删除所有20,l2: [10, 30, 40]
// 注意:erase删除后,被删除节点的迭代器失效,其他迭代器有效
it = l2.begin();
l2.erase(it); // it指向10,删除后it失效,不能再使用
// cout << *it << endl; // 错误,迭代器失效
(4)容量与大小相关(3个核心函数)
list 无容量(capacity)概念(因为无需扩容,节点按需分配),因此只有大小相关的函数,用法与 vector 类似。
-
size():返回 list 中当前元素的个数(实际存储的数据量),时间复杂度 O(1);
-
empty():判断 list 是否为空(size() == 0),返回 bool 值,高效,推荐使用;
-
resize(n, val):调整 list 的 size(元素个数),若 n > 当前 size,新增元素为 val;若 n < 当前 size,删除尾部多余元素(无容量,直接删除节点)。
cpp
list<int> l = {1, 2, 3};
cout << l.size() << endl; // 输出3(元素个数)
cout << l.empty() << endl; // 输出0(非空)
// resize:调整size为5,新增元素为10
l.resize(5, 10); // l: [1, 2, 3, 10, 10]
cout << l.size() << endl; // 输出5
// resize:调整size为2,删除尾部3个元素
l.resize(2); // l: [1, 2]
cout << l.size() << endl; // 输出2
(5)其他常用操作(高频)
-
swap(l2):交换两个 list 的元素、size,时间复杂度 O(1)(仅交换头指针、尾指针和 size,无需拷贝元素); -
clear():清空 list 中所有元素,释放所有节点内存,size 变为 0(与 vector 的 clear() 不同,vector 不清空容量,list 无容量,直接释放所有节点); -
reverse():反转 list 中的元素,时间复杂度 O(n)(仅修改所有节点的 prev 和 next 指针,无需移动元素,高效)。
cpp
list<int> l1 = {1, 2, 3};
list<int> l2 = {4, 5, 6};
// swap:交换两个list
l1.swap(l2); // l1: [4,5,6], l2: [1,2,3]
// reverse:反转list
l1.reverse(); // l1: [6,5,4]
// clear:清空list
l2.clear();
cout << l2.size() << endl; // 输出0(元素清空,节点内存释放)
3. list 的遍历方式(3种常用方式,适配双向迭代器)
由于 list 的迭代器是双向迭代器(仅支持 ++、-- 操作,不支持 +n、-n 操作),因此遍历方式与 vector 略有差异,重点掌握前两种方式。
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> l = {1, 2, 3, 4, 5};
// 方式1:迭代器遍历(最通用,适配list特性,推荐)
// 普通迭代器(可读可写)
list<int>::iterator it;
for (it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // *it 访问迭代器指向的元素
}
cout << endl;
// 常量迭代器(只读,不允许修改元素)
list<int>::const_iterator cit;
for (cit = l.cbegin(); cit != l.cend(); ++cit) {
cout << *cit << " ";
}
cout << endl;
// 方式2:范围for循环(C++11及以上,简洁,无需关心迭代器,推荐)
for (int val : l) {
cout << val << " "; // val是l中元素的拷贝,修改val不影响原list
}
cout << endl;
// 范围for循环(修改原元素,需用引用)
for (int& val : l) {
val *= 2; // 修改原list中的元素,l变为[2,4,6,8,10]
}
// 方式3:反向迭代器(从尾部到头部遍历)
list<int>::reverse_iterator rit;
for (rit = l.rbegin(); rit != l.rend(); ++rit) {
cout << *rit << " "; // 输出:10 8 6 4 2
}
cout << endl;
// 错误示例:list迭代器不支持 +n 操作,无法直接跳转到指定位置
// auto it2 = l.begin() + 2; // 编译报错,只能一步步遍历
auto it2 = l.begin();
++it2;
++it2; // 遍历两次,指向6(正确方式)
cout << *it2 << endl; // 输出6
return 0;
}
四、list 进阶用法:排序、去重与自定义类型
list 虽然不支持随机访问,但依然可以与 STL 算法协同使用,同时也支持存储自定义类型,实现排序、去重等高级操作。需要注意的是,list 有自己的成员函数 sort(),效率比 STL 通用算法 sort() 更高。
1. list 排序:优先使用成员函数 sort()(高效)
STL 通用算法 sort() 要求容器支持随机访问迭代器(如 vector),而 list 的迭代器是双向迭代器,因此无法使用 STL 通用算法 sort(),只能使用 list 自带的成员函数 sort()。
list 的成员函数 sort() 是基于双向链表优化的排序算法,时间复杂度 O(n log n),效率比通用算法更高,支持默认升序和自定义排序规则。
cpp
#include <iostream>
#include <list>
// #include <algorithm> // 无需包含,list自带sort()
using namespace std;
// 自定义排序规则:降序排序(函数形式)
bool cmpDesc(int a, int b) {
return a > b; // a > b 时,a排在b前面,即降序
}
int main() {
list<int> l = {3, 1, 4, 1, 5, 9, 2, 6};
// 1. 默认升序排序(成员函数 sort())
l.sort(); // 排序后:1 1 2 3 4 5 6 9
for (int val : l) {
cout << val << " ";
}
cout << endl;
// 2. 自定义降序排序(传入排序函数)
l.sort(cmpDesc); // 排序后:9 6 5 4 3 2 1 1
for (int val : l) {
cout << val << " ";
}
cout << endl;
// 错误示例:无法使用STL通用算法sort()
// sort(l.begin(), l.end()); // 编译报错,因为list迭代器不是随机访问迭代器
return 0;
}
2. list 去重:配合 sort() + unique()(成员函数)
list 的去重逻辑与 vector 类似:先排序(让重复元素相邻),再去重。但 list 同样有自己的成员函数 unique(),无需使用 STL 通用算法 unique(),用法更简洁,效率更高。
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> l = {3, 1, 4, 1, 5, 9, 5, 3};
// 步骤1:先排序,让重复元素相邻(必须先排序,否则去重无效)
l.sort(); // 排序后:1 1 3 3 4 5 5 9
// 步骤2:使用list成员函数unique()去重(删除相邻重复元素)
l.unique(); // 去重后:1 3 4 5 9
// 输出去重后的结果
for (int val : l) {
cout << val << " ";
}
return 0;
}
注意:list 的 unique() 函数仅删除相邻的重复元素,因此必须先排序,否则无法彻底去重(比如未排序的 list 中,重复元素不相邻,unique() 无法识别)。
3. list 存储自定义类型(结构体/类)
list 支持存储任意自定义类型(结构体、类),用法与 vector 类似,只需确保自定义类型有默认构造函数(如果手动定义了带参构造,需显式定义默认构造),同时可以自定义排序规则,适配 list 的 sort() 函数。
cpp
#include <iostream>
#include <list>
#include <string>
using namespace std;
// 自定义结构体:学生信息
struct Student {
string name;
int age;
int score;
// 默认构造函数(必须有,否则list<Student> l(5)会报错)
Student() : name(""), age(0), score(0) {}
// 带参构造函数(方便初始化)
Student(string n, int a, int s) : name(n), age(a), score(s) {}
};
// 自定义排序规则:按成绩降序排序(用于list.sort())
bool cmpStudent(const Student& a, const Student& b) {
return a.score > b.score;
}
int main() {
// 初始化存储自定义结构体的list
list<Student> students;
students.push_back(Student("张三", 18, 90));
students.push_back(Student("李四", 19, 85));
students.push_back(Student("王五", 18, 95));
students.push_back(Student("赵六", 19, 88));
// 遍历输出(用const引用,避免拷贝,提高效率)
cout << "排序前:" << endl;
for (const auto& stu : students) {
cout << "姓名:" << stu.name << ",年龄:" << stu.age << ",成绩:" << stu.score << endl;
}
// 按成绩降序排序(使用自定义排序规则)
students.sort(cmpStudent);
// 遍历输出排序后结果
cout << "\n排序后:" << endl;
for (const auto& stu : students) {
cout << "姓名:" << stu.name << ",年龄:" << stu.age << ",成绩:" << stu.score << endl;
}
return 0;
}
五、list 使用避坑指南(高频错误总结)
结合 list 的特性和用法,总结以下5个高频错误,以及对应的解决方案,帮你规避隐患,写出更安全、高效的代码。
1. 错误使用下标 [ ] 或 at() 访问元素
错误:习惯性地用 vector 的方式,通过下标 [ ] 或 at() 访问 list 元素,导致编译报错。
原因:list 是双向链表,内存非连续,不支持随机访问,因此不提供下标访问和 at() 函数。
解决方案:
-
访问头部/尾部元素,用 front() / back();
-
访问任意位置元素,通过迭代器遍历找到目标节点;
-
若频繁需要随机访问,放弃 list,改用 vector。
2. 用 STL 通用算法 sort() 排序 list
错误:调用 STL 通用算法 sort(l.begin(), l.end()) 对 list 排序,导致编译报错。
原因:STL 通用算法 sort() 要求容器支持随机访问迭代器,而 list 的迭代器是双向迭代器,不满足要求。
解决方案:使用 list 自带的成员函数 sort(),支持默认排序和自定义排序规则,效率更高。
3. 迭代器失效后继续使用
错误:删除 list 中的节点后,继续使用指向该节点的迭代器,导致程序崩溃或未定义行为。
原因:list 删除节点后,被删除节点的迭代器会失效(节点内存被释放),其他节点的迭代器依然有效。
解决方案:
-
删除节点后,避免使用指向该节点的迭代器;
-
erase() 函数会返回指向被删除节点下一个位置的迭代器,可利用返回值更新迭代器(如 it = l.erase(it))。
cpp
list<int> l = {1, 2, 3, 4, 5};
auto it = l.begin();
++it; // 指向2
// 正确方式:利用erase()的返回值更新迭代器
it = l.erase(it); // 删除2,it指向3(下一个节点)
cout << *it << endl; // 输出3(正常使用)
// 错误方式:删除后继续使用原迭代器
// l.erase(it);
// cout << *it << endl; // 错误,迭代器失效
4. 盲目使用 list,忽略性能差异
错误:无论场景如何,都优先使用 list,导致在频繁访问元素的场景中,性能严重下降。
原因:list 的遍历效率低(O(n)),不适合频繁访问元素的场景,盲目使用会导致性能瓶颈。
解决方案:根据场景选择容器,遵循"频繁访问用 vector,频繁插入删除用 list"的原则,不确定时,优先测试两种容器的性能。
5. 存储指针类型时,未释放内存(内存泄漏)
错误:list 存储指针类型(如 list<int*>)时,只 clear() 清空元素,未释放指针指向的内存,导致内存泄漏。
原因:list 的 clear() 函数会释放节点内存,但不会释放指针指向的内存(指针本身只是一个地址,节点存储的是地址,不是实际数据)。
解决方案:清空 list 前,遍历所有元素,手动释放指针指向的内存;或使用智能指针(shared_ptr、unique_ptr),避免手动管理内存。
cpp
// 错误示例:内存泄漏
list<int*> l;
l.push_back(new int(1));
l.push_back(new int(2));
l.clear(); // 仅释放节点内存,未释放new分配的内存,导致内存泄漏
// 正确示例:手动释放内存
list<int*> l;
l.push_back(new int(1));
l.push_back(new int(2));
// 遍历释放内存
for (int* p : l) {
delete p; // 释放指针指向的内存
p = nullptr; // 避免野指针
}
l.clear(); // 再清空元素
六、总结:list 的核心价值与使用建议
list 作为 STL 序列式容器的重要成员,其核心价值在于"任意位置插入/删除的高效性",填补了 vector 在频繁插入/删除场景中的性能短板。它的底层双向链表结构,决定了它"不支持随机访问、内存开销大,但迭代器稳定、无扩容损耗"的特性。
很多开发者对 list 的使用存在误区------要么盲目使用,要么完全不用。其实,只要找准适用场景,list 就能发挥巨大作用,成为解决问题的"利器"。最后,给大家几点使用建议,帮助你更好地运用 list:
-
场景优先:频繁在任意位置插入/删除,用 list;频繁访问元素,用 vector;两者都有需求,可考虑 deque(双端队列);
-
迭代器用法:记住 list 迭代器是双向迭代器,不支持 +n、-n,只能通过 ++、-- 遍历;删除节点后,及时更新迭代器,避免失效;
-
排序去重:优先使用 list 自带的 sort() 和 unique() 成员函数,不使用 STL 通用算法,效率更高、更适配;
-
内存管理:存储指针类型时,务必手动释放内存,或使用智能指针,避免内存泄漏;
-
性能权衡:如果插入/删除操作并不频繁,且需要偶尔访问元素,优先选择 vector(内存开销小,遍历效率更高)。
list 看似简单,但想要真正用好,需要深入理解其底层结构和特性,结合场景合理选择。希望本文的全面解析,能帮助你彻底吃透 list,在笔试面试和实际开发中,都能灵活运用这个 STL"利器",写出更高效、更安全的代码。
最后,附上本文所有核心示例代码汇总,方便大家复制测试、快速上手:
cpp
// 本文核心示例代码汇总
#include <iostream>
#include <list>
#include <string>
using namespace std;
// 1. 初始化与基础操作
void testInitAndBase() {
list<int> l1;
list<int> l2(5, 3);
list<int> l3 = {1, 2, 3, 4, 5};
l3.push_back(6);
l3.push_front(0);
auto it = l3.begin();
++it;
l3.insert(it, 10);
l3.pop_back();
l3.pop_front();
for (int val : l3) {
cout << val << " ";
}
cout << endl;
}
// 2. 排序与去重
void testSortAndUnique() {
list<int> l = {3, 1, 4, 1, 5, 9, 5, 3};
// 排序
l.sort(); // 升序
l.sort([](int a, int b) { return a > b; }); // 降序(lambda表达式)
// 去重
l.unique();
for (int val : l) {
cout << val << " ";
}
cout << endl;
}
// 3. 自定义结构体与排序
struct Student {
string name;
int score;
Student() : name(""), score(0) {}
Student(string n, int s) : name(n), score(s) {}
};
void testCustomType() {
list<Student> students = {{"张三", 90}, {"李四", 85}, {"王五", 95}};
// 按成绩降序排序
students.sort([](const Student& a, const Student& b) {
return a.score > b.score;
});
for (const auto& stu : students) {
cout << stu.name << ":" << stu.score << endl;
}
}
// 4. 指针类型list,避免内存泄漏
void testPointer() {
list<int*> l;
l.push_back(new int(1));
l.push_back(new int(2));
// 遍历释放内存
for (int* p : l) {
delete p;
p = nullptr;
}
l.clear();
cout << "内存释放完成" << endl;
}
int main() {
testInitAndBase();
testSortAndUnique();
testCustomType();
testPointer();
return 0;
}