一、list 的底层数据结构与核心特性
1.1 双向循环链表的物理结构
- 节点定义:每个节点包含三个部分
template <typename T> struct ListNode {T data; // 存储的数据ListNode* prev; // 指向前驱节点的指针ListNode* next; // 指向后继节点的指针ListNode(const T& val) : data(val), prev(nullptr), next(nullptr) {} };
- 头结点(哨兵节点):
- 哑元节点,不存储有效数据,类型为
ListNode<T>
- 初始状态:
head->next = head
,head->prev = head
- 作用:统一处理首尾操作的边界条件,避免空链表特判
- 哑元节点,不存储有效数据,类型为
1.2 逻辑结构示意图
1.3 关键特性对比
特性 | 技术实现 | 带来的优势 | 引入的代价 |
---|---|---|---|
双向迭代 | 每个节点维护 prev/next 指针 | 支持正向 / 反向遍历 | 每个节点增加 2 个指针开销 |
常数时间插入 | 仅需修改相邻节点的指针 | 任意位置插入 O (1) 时间 | 无内存预分配导致碎片化 |
循环结构 | 头尾节点通过头结点连接成环 | 简化边界条件处理 | 头结点额外空间消耗 |
二、迭代器系统的深度解析
2.1 正向迭代器的实现细节
模板参数设计
template <typename T, typename Ref, // 引用类型(T& 或 const T&)typename Ptr // 指针类型(T* 或 const T*)
>
struct ListIterator {using Node = ListNode<T>;using Self = ListIterator<T, Ref, Ptr>;Node* ptr; // 指向当前节点的原始指针// 解引用操作Ref operator*() const { return ptr->data; }Ptr operator->() const { return &(ptr->data); }// 前置递增:移动到下一个节点Self& operator++() { ptr = ptr->next; return *this; }// 后置递增:返回旧值后移动Self operator++(int) { Self tmp = *this; ptr = ptr->next; return tmp; }// 递减操作(双向链表特有)Self& operator--() { ptr = ptr->prev; return *this; }// 比较操作bool operator==(const Self& other) const { return ptr == other.ptr; }bool operator!=(const Self& other) const { return ptr != other.ptr; }
};
类型特化
iterator
对应:ListIterator<T, T&, T*>
const_iterator
对应:ListIterator<T, const T&, const T*>
2.2 反向迭代器的底层机制
适配器模式实现
template <typename Iterator>
class ReverseIterator {Iterator it; // 包装正向迭代器public:// 解引用:反向迭代器指向当前位置的前一个元素auto operator*() const { Iterator tmp = it; return *--tmp; // 先将正向迭代器回退,再解引用}// 前置递增:反向迭代器的++对应正向迭代器的--ReverseIterator& operator++() { --it; return *this; }// 后置递增ReverseIterator operator++(int) { ReverseIterator tmp = *this; --it; return tmp; }// 递减操作:对应正向迭代器的++ReverseIterator& operator--() { ++it; return *this; }// 比较操作直接委托给底层迭代器bool operator==(const ReverseIterator& other) const { return it == other.it; }
};
迭代器闭环关系
list.rbegin()
等价于ReverseIterator(list.end())
list.rend()
等价于ReverseIterator(list.begin())
三、核心接口的实现原理
3.1 构造函数的实现逻辑
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | list(size_type n, const value_type& val = value_type()) | 构造的 list 中包含 n 个值为 val 的元素 |
2 | list() | 构造空的 list |
3 | list(const list& x) | 拷贝构造函数,复制 x 的内容到新 list |
4 | list(InputIterator first, InputIterator last) | 用 [first, last) 区间中的元素构造 list |
详细讲解
1. list(size_type n, const value_type& val = value_type())
(构造包含 n
个值为 val
的元素的 list
)
- 参数:
size_type n
(元素个数),const value_type& val
(元素值,默认值为value_type()
)。 - 详细说明:创建一个
list
,其中包含n
个值为val
的元素。适用于需要初始化固定数量、相同值元素的场景。 - 示例:
#include <list>
#include <iostream>
int main() {std::list<int> lst(3, 5); // 创建包含 3 个值为 5 的元素的 liststd::cout << "元素个数: " << lst.size() << std::endl; // 输出:3for (int i : lst) {std::cout << i << " "; // 输出:5 5 5}return 0;
}
- 示例解释:
std::list<int> lst(3, 5);
:调用list
的构造函数,明确指定创建一个包含 3 个元素的列表,每个元素的值都初始化为 5。这里size_type
通常是无符号整数类型,用来表示容器的大小。std::cout << "元素个数: " << lst.size() << std::endl;
:size()
是list
容器的成员函数,用于返回容器中元素的数量。所以此语句会输出3
,表明列表中有 3 个元素。for (int i : lst)
:这是 C++11 引入的范围for
循环,它会遍历lst
列表中的每个元素,将元素的值依次赋给变量i
,并执行循环体。这里循环体将每个元素的值输出,最终屏幕上会显示5 5 5
。
2. list()
(构造空的 list
)
- 参数:无。
- 详细说明:创建一个空的
list
,长度为 0,后续可通过插入、赋值等操作填充内容。适用于初始不需要元素,动态管理数据的场景。 - 示例:
#include <list>
#include <iostream>
int main() {std::list<double> lst;std::cout << "空 list 长度: " << lst.size() << std::endl; // 输出:0return 0;
}
- 示例解释:
std::list<double> lst;
:调用list
的默认构造函数,创建一个空的list
对象lst
,其内部不包含任何元素。std::cout << "空 list 长度: " << lst.size() << std::endl;
:由于lst
是空的,size()
函数返回 0,所以会输出空 list 长度: 0
。这体现了默认构造的list
初始状态为空,后续可根据程序需求添加元素。
3. list(const list& x)
(拷贝构造函数)
- 参数:
const list& x
(要拷贝的list
对象)。 - 详细说明:创建一个新
list
,将x
的所有元素复制到新list
中,新list
与x
内容完全一致。适用于需要复制已有list
内容的场景。 - 示例:
#include <list>
#include <iostream>
int main() {std::list<char> lst1 = {'a', 'b', 'c'};std::list<char> lst2(lst1); // 拷贝 lst1 到 lst2std::cout << "lst2 内容: ";for (char c : lst2) {std::cout << c << " "; // 输出:a b c}return 0;
}
- 示例解释:
std::list<char> lst1 = {'a', 'b', 'c'};
:使用初始化列表的方式创建一个包含'a'
、'b'
、'c'
三个字符元素的list
对象lst1
。std::list<char> lst2(lst1);
:调用拷贝构造函数,将lst1
中的元素逐个复制到新创建的lst2
中。此时lst2
和lst1
的内容完全相同,但它们是两个独立的对象,对一个对象的修改不会影响另一个。for (char c : lst2)
:使用范围for
循环遍历lst2
中的每个元素,将元素的值赋给变量c
,并在屏幕上输出。最终会显示lst2 内容: a b c
。
4. list(InputIterator first, InputIterator last)
(用 [first, last)
区间元素构造 list
)
- 参数:
InputIterator first
(区间起始迭代器),InputIterator last
(区间结束迭代器,左闭右开)。 - 详细说明:将
[first, last)
区间内的元素插入到新list
中,可用于从其他容器(如vector
)或迭代器范围初始化list
。 - 示例:
#include <list>
#include <vector>
#include <iostream>
int main() {std::vector<int> vec = {10, 20, 30};std::list<int> lst(vec.begin(), vec.end()); // 用 vector 区间构造 liststd::cout << "lst 内容: ";for (int i : lst) {std::cout << i << " "; // 输出:10 20 30}return 0;
}
- 示例解释:
std::vector<int> vec = {10, 20, 30};
:创建一个vector
容器vec
,并使用初始化列表将其初始化为包含10
、20
、30
三个整数元素。std::list<int> lst(vec.begin(), vec.end());
:调用list
的构造函数,传入vec
的起始迭代器vec.begin()
和结束迭代器vec.end()
。迭代器定义了一个左闭右开的区间[vec.begin(), vec.end())
,构造函数会将该区间内的元素依次插入到新创建的lst
列表中。for (int i : lst)
:使用范围for
循环遍历lst
列表,将每个元素的值赋给变量i
,并在屏幕上输出。最终会显示lst 内容: 10 20 30
,表明list
成功从vector
的元素区间构造而成。
3.2 list iterator的使用
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | begin() | 返回指向列表第一个元素的迭代器,用于正向遍历的起始 |
2 | end() | 返回指向列表最后一个元素下一个位置的迭代器,作为正向遍历的结束标志 |
3 | rbegin() | 返回指向列表最后一个元素的反向迭代器,用于反向遍历的起始 |
4 | rend() | 返回指向列表第一个元素前一个位置的反向迭代器,作为反向遍历的结束标志 |
详细讲解
1. begin();
- 参数:无
- 详细说明:此函数返回一个迭代器,该迭代器指向
list
的第一个元素,是正向遍历列表的起始点。通过这个迭代器,可以逐个访问列表中的元素。 - 示例:
#include <list>
#include <iostream>
int main() {std::list<int> lst = {1, 2, 3};auto it = lst.begin();std::cout << "第一个元素: " << *it << std::endl; // 输出:1return 0;
}
- 示例解释:首先创建一个包含
1
、2
、3
的list
对象lst
。调用begin()
函数获取指向第一个元素的迭代器it
,通过解引用操作符*
访问该元素并输出,结果为1
。
2. end();
- 参数:无。
- 详细说明:该函数返回的迭代器指向列表最后一个元素的 “下一个位置”,它不指向实际元素,仅作为正向遍历结束的标志。在循环遍历列表时,常用此迭代器作为终止条件。
- 示例:
#include <list>
#include <iostream>
int main() {std::list<int> lst = {1, 2, 3};auto it = lst.end();--it; // 因 end() 指向最后元素下一个位置,需先减 1 以指向实际最后元素std::cout << "最后一个元素: " << *it << std::endl; // 输出:3return 0;
}
- 示例解释:创建
list
对象lst
后,获取end()
迭代器it
。由于end()
指向最后元素的下一个位置,通过--it
将其调整为指向最后一个元素3
,再解引用输出。
3. rbegin();
- 参数:无。
- 详细说明:返回一个反向迭代器,该迭代器指向列表的最后一个元素,是反向遍历列表的起始点。反向迭代器的移动方向与正向迭代器相反
- 示例:
#include <list>
#include <iostream>
int main() {std::list<int> lst = {1, 2, 3};auto rit = lst.rbegin();std::cout << "反向第一个元素(实际为列表最后一个元素): " << *rit << std::endl; // 输出:3return 0;
}
- 示例解释:创建
list
对象lst
后,调用rbegin()
函数获取反向迭代器rit
,它指向列表的最后一个元素3
,解引用后输出该元素。
4. rend();
- 参数:无。
- 详细说明:返回的反向迭代器指向列表第一个元素的 “前一个位置”,作为反向遍历结束的标志。在反向循环遍历时,常用此迭代器作为终止条件。
- 示例:
#include <list>
#include <iostream>
int main() {std::list<int> lst = {1, 2, 3};auto rit = lst.rend();--rit; // rend() 指向第一个元素前一个位置,减 1 以指向实际第一个元素std::cout << "反向最后一个元素(实际为列表第一个元素): " << *rit << std::endl; // 输出:1return 0;
}
- 示例解释:获取
rend()
迭代器rit
后,由于它指向第一个元素的前一个位置,通过--it
调整为指向第一个元素1
,解引用后输出该元素。
3.3 list capacity
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | empty() | 检测 list 是否为空,若为空返回 true ,否则返回 false |
2 | size() | 返回 list 中有效节点(元素)的个数 |
详细讲解
1. empty();
- 参数:无
- 详细说明:此函数用于判断
list
容器是否为空。当list
中没有任何元素时,它返回true
;若list
中包含至少一个元素,则返回false
。在对list
进行元素访问或其他依赖于元素存在的操作前,使用empty()
检测可避免潜在错误。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst1;if (lst1.empty()) {std::cout << "lst1 为空" << std::endl; // 输出:lst1 为空}lst1.push_back(1);std::list<int> lst2 = {1, 2, 3};if (!lst2.empty()) {std::cout << "lst2 不为空" << std::endl; // 输出:lst2 不为空}return 0;
}
- 示例解释:首先创建空
list
lst1
,通过empty()
检测并输出 “lst1
为空”。然后向lst1
中插入元素,再创建包含元素{1, 2, 3}
的lst2
,通过empty()
检测lst2
不为空并输出相应信息。
2. size();
- 参数:无。
- 详细说明:该函数返回
list
中当前有效元素的数量。通过获取元素个数,可方便地进行循环遍历、条件判断等操作,了解list
的数据规模。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {10, 20, 30};std::cout << "lst 中元素个数为: " << lst.size() << std::endl; // 输出:3lst.push_back(40);std::cout << "添加元素后个数为: " << lst.size() << std::endl; // 输出:4return 0;
}
- 示例解释:先创建包含
10
、20
、30
三个元素的list
lst
,调用size()
输出元素个数为3
。向lst
末尾插入元素40
后,再次调用size()
,输出结果变为4
,反映了list
中元素数量的变化。
3.4 list element access
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | front() | 返回 list 的第一个节点中值的引用 |
2 | back() | 返回 list 的最后一个节点中值的引用 |
详细讲解
1. front();
- 参数:无
- 详细说明:此函数用于返回
list
容器中第一个节点值的引用。通过这个引用,可以直接访问或修改第一个元素的值。在调用front()
之前,建议确保list
不为空,否则行为未定义。该函数在需要快速操作列表首元素时非常有用,比如获取或更新首元素的值。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {10, 20, 30};std::cout << "第一个元素的值: " << lst.front() << std::endl; // 输出:10lst.front() = 5;std::cout << "修改后第一个元素的值: " << lst.front() << std::endl; // 输出:5return 0;
}
- 示例解释:首先创建一个包含
10
、20
、30
的list
对象lst
。通过lst.front()
获取第一个元素的引用并输出其值10
。然后将lst.front()
赋值为5
,再次输出时,第一个元素的值变为5
,体现了通过该引用修改首元素的功能。
2. back();
- 参数:无。
- 详细说明:此函数返回
list
容器中最后一个节点值的引用。利用该引用能够访问或修改最后一个元素。同样,在调用back()
前,要确保list
不为空。这在处理列表尾元素时很实用,如检查或更新尾元素。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {10, 20, 30};std::cout << "最后一个元素的值: " << lst.back() << std::endl; // 输出:30lst.back() = 35;std::cout << "修改后最后一个元素的值: " << lst.back() << std::endl; // 输出:35return 0;
}
- 示例解释:先创建包含
10
、20
、30
的list
对象lst
。通过lst.back()
获取最后一个元素的引用并输出其值30
。接着将lst.back()
赋值为35
,再次输出时,最后一个元素的值变为35
,展示了通过该引用修改尾元素的过程。
3.5 插入删掉交换清理操作的原子性实现
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | push_front(const value_type& val) | 在 list 首元素前插入值为 val 的元素 |
2 | pop_front() | 删除 list 中第一个元素 |
3 | push_back(const value_type& val) | 在 list 尾部插入值为 val 的元素 |
4 | pop_back() | 删除 list 中最后一个元素 |
5 | insert(iterator position, const value_type& val) | 在 list 的 position 位置插入值为 val 的元素 |
6 | erase(iterator position) | 删除 list 中 position 位置的元素 |
7 | swap(list& x) | 交换当前 list 与 list x 中的元素 |
8 | clear() | 清空 list 中的有效元素 |
详细讲解
1. push_front(const value_type& val);
- 参数:
const value_type& val
(要插入的元素值)。 - 详细说明:在
list
首元素前插入值为val
的元素,新元素成为新首元素,原元素依次后移。适用于在列表开头添加元素。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {2, 3, 4}; // 创建一个包含 2、3、4 三个元素的 listlst.push_front(1); // 在列表开头插入元素 1,此时列表变为 {1, 2, 3, 4}for (int num : lst) { // 遍历列表输出每个元素std::cout << num << " "; // 输出:1 2 3 4}return 0;
}
- 示例解释:首先通过
#include
引入list
和iostream
头文件,前者用于使用list
容器,后者用于输入输出。在main
函数中,创建一个初始化为{2, 3, 4}
的list
对象lst
。接着调用lst.push_front(1)
,将元素1
插入到列表的开头,此时列表内容变为{1, 2, 3, 4}
。最后通过范围for
循环遍历lst
,将每个元素输出到控制台,验证元素插入效果。
2.pop_front();
- 参数:无。
- 详细说明:删除
list
中第一个元素,原第二个元素成为新首元素。调用前确保列表非空。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {1, 2, 3, 4}; // 创建包含 1、2、3、4 的 listlst.pop_front(); // 删除第一个元素 1,列表变为 {2, 3, 4}for (int num : lst) { // 遍历输出剩余元素std::cout << num << " "; // 输出:2 3 4}return 0;
}
- 示例解释:包含必要头文件后,创建
lst
并初始化为{1, 2, 3, 4}
。调用lst.pop_front()
移除第一个元素1
,此时列表中元素为{2, 3, 4}
。通过范围for
循环遍历并输出每个元素,确认第一个元素已被成功删除。
3. push_back(const value_type& val);
- 参数:
const value_type& val
(要插入的元素值)。 - 详细说明:在
list
尾部插入值为val
的元素,新元素成为新尾元素。常用于向列表末尾添加元素。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {1, 2, 3}; // 初始化 list 为 {1, 2, 3}lst.push_back(4); // 在列表末尾插入 4,列表变为 {1, 2, 3, 4}for (int num : lst) { // 遍历输出所有元素std::cout << num << " "; // 输出:1 2 3 4}return 0;
}
- 示例解释:先创建
lst
并初始化为{1, 2, 3}
。调用lst.push_back(4)
向列表末尾添加元素4
,此时列表内容更新为{1, 2, 3, 4}
。通过范围for
循环遍历每个元素并输出,验证元素成功添加到列表尾部。
4. pop_back();
- 参数:无。
- 详细说明:删除
list
中最后一个元素。调用前确保列表非空。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {1, 2, 3, 4}; // 创建列表 {1, 2, 3, 4}lst.pop_back(); // 删除最后一个元素 4,列表变为 {1, 2, 3}for (int num : lst) { // 遍历输出剩余元素std::cout << num << " "; // 输出:1 2 3}return 0;
}
- 示例解释:包含头文件后,定义
lst
并初始化为{1, 2, 3, 4}
。执行lst.pop_back()
删除最后一个元素4
,列表中剩余{1, 2, 3}
。通过范围for
循环输出每个元素,确认最后一个元素已被删除。
5. insert(iterator position, const value_type& val);
- 参数:
iterator position
(插入位置的迭代器),const value_type& val
(要插入的元素值)。 - 详细说明:在
position
迭代器所指位置前插入值为val
的元素,原位置及后元素后移。可灵活在指定位置插入元素。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {1, 3, 4}; // 初始化列表 {1, 3, 4}auto it = lst.begin(); // 获取指向第一个元素(值为 1)的迭代器++it; // 迭代器后移,指向值为 3 的元素lst.insert(it, 2); // 在迭代器 it 所指元素(3)前插入 2,列表变为 {1, 2, 3, 4}for (int num : lst) { // 遍历输出所有元素std::cout << num << " "; // 输出:1 2 3 4}return 0;
}
- 示例解释:先创建
lst
并初始化为{1, 3, 4}
。通过lst.begin()
获取指向第一个元素1
的迭代器it
,然后++it
使it
指向元素3
。调用lst.insert(it, 2)
在3
之前插入2
,列表变为{1, 2, 3, 4}
。最后通过范围for
循环输出每个元素,验证元素在指定位置插入成功。
6. erase(iterator position);
- 参数:
iterator position
(要删除元素的迭代器)。 - 详细说明:删除
position
所指位置的元素,其后元素前移。确保迭代器有效 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {1, 2, 3, 4}; // 创建列表 {1, 2, 3, 4}auto it = lst.begin(); // 获取指向第一个元素(1)的迭代器++it; // 迭代器后移,指向值为 2 的元素lst.erase(it); // 删除迭代器 it 所指的元素 2,列表变为 {1, 3, 4}for (int num : lst) { // 遍历输出剩余元素std::cout << num << " "; // 输出:1 3 4}return 0;
}
- 示例解释:包含头文件后,定义
lst
为{1, 2, 3, 4}
。获取指向第一个元素1
的迭代器it
,++it
后it
指向2
。调用lst.erase(it)
删除2
,列表中剩余{1, 3, 4}
。通过范围for
循环输出每个元素,确认指定位置元素被成功删除。
7. swap(list& x);
- 参数:
list& x
(要交换的另一个list
)。 - 详细说明:交换当前
list
与x
的所有元素。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst1 = {1, 2, 3}; // 创建 lst1 并初始化为 {1, 2, 3}std::list<int> lst2 = {4, 5, 6}; // 创建 lst2 并初始化为 {4, 5, 6}lst1.swap(lst2); // 交换 lst1 和 lst2 的元素std::cout << "lst1: ";for (int num : lst1) { // 遍历输出 lst1 交换后的元素std::cout << num << " "; // 输出:4 5 6}std::cout << "\nlst2: ";for (int num : lst2) { // 遍历输出 lst2 交换后的元素std::cout << num << " "; // 输出:1 2 3}return 0;
}
- 示例解释:首先创建
lst1
{1, 2, 3}
和lst2
{4, 5, 6}
。调用lst1.swap(lst2)
交换两个列表的元素。然后分别通过范围for
循环遍历lst1
和lst2
,输出交换后的元素,验证lst1
变为{4, 5, 6}
,lst2
变为{1, 2, 3}
,确认交换操作成功。
8. clear();
- 参数:无。
- 详细说明:清空
list
中所有有效元素,使列表为空。 - 示例:
#include <list>
#include <iostream>int main() {std::list<int> lst = {1, 2, 3, 4}; // 初始化列表 {1, 2, 3, 4}lst.clear(); // 清空列表所有元素if (lst.empty()) { // 检查列表是否为空std::cout << "列表已清空" << std::endl; // 输出:列表已清空}return 0;
}
- 示例解释:包含头文件后,创建
lst
并初始化为{1, 2, 3, 4}
。调用lst.clear()
移除所有元素。通过if (lst.empty())
检查列表是否为空,若为空则输出 “列表已清空”,验证clear
操作成功将列表置为空状态。
3.5 list的迭代器失效
1、迭代器失效的本质理解
迭代器是容器与算法之间的 “桥梁”,其行为类似指针,用于指向容器中的元素(节点)。当迭代器所指向的节点在容器中被删除时,该迭代器就失去了合法指向对象,即发生迭代器失效。此时若继续使用失效迭代器(如解引用*it
或进行自增++it
等操作),程序会出现未定义行为(可能崩溃、返回错误值或导致其他异常)。
2、list
迭代器失效的特性分析
- 插入操作不失效:
list
底层是带头结点的双向循环链表,插入新节点时,仅需修改相邻节点的prev
和next
指针。例如在节点A
后插入节点X
,只需将A->next
指向X
,X->prev
指向A
,再调整新节点X
与后续节点的指针。由于其他节点的物理存储位置和指针指向均未改变,因此所有迭代器(包括指向插入位置前后节点的迭代器)在插入操作后仍有效。 - 删除操作仅局部失效:
当删除某个节点时,仅该节点的迭代器失效,其他节点的迭代器不受影响。这是因为双向循环链表的删除操作只需修改被删节点前驱和后继的指针。例如删除节点B
,只需将B->prev->next
指向B->next
,B->next->prev
指向B->prev
,其他节点的连接关系未变,因此除了指向B
的迭代器外,其他迭代器仍指向合法节点。
3、错误代码(TestListIterator1
)深度剖析
void TestListIterator1() {int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };list<int> l(array, array + sizeof(array) / sizeof(array[0]));auto it = l.begin();while (it != l.end()) {l.erase(it); // ① 删除it指向的节点++it; // ② 对失效的it进行自增}
}
- 步骤①:
l.erase(it)
会删除it
指向的节点。此时,it
所指向的节点已从链表中移除,it
成为失效迭代器。 - 步骤②:对失效的
it
执行++it
。由于it
指向的节点已不存在,++it
操作试图访问该节点的next
指针(以移动到下一节点),但该节点的内存可能已被回收或重新分配,导致程序出现未定义行为(如访问违规、程序崩溃)。
4、修正代码(TestListIterator
)原理详解
void TestListIterator() {int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };list<int> l(array, array + sizeof(array) / sizeof(array[0]));auto it = l.begin();while (it != l.end()) {l.erase(it++); // 等价于 it = l.erase(it);}
}
- 关键逻辑
l.erase(it++)
:
后置自增it++
会先保留it
的当前值(指向待删除节点),再将it
自增。l.erase(it++)
实际执行过程如下:- 临时保存
it
的原始值(记为it_old
),此时it_old
指向待删除节点。 it
自增为指向下一节点(it = it_old + 1
,这里的+1
是逻辑上的下一节点,而非指针算术)。- 调用
l.erase(it_old)
删除节点。由于it
已自增为下一节点的迭代器,即使it_old
因删除操作失效,当前it
仍是有效的。
- 临时保存
- 等价写法
it = l.erase(it)
:
list
的erase
成员函数会返回一个迭代器,指向被删除节点的下一节点。因此,it = l.erase(it)
直接将it
更新为有效迭代器。先删除it
指向的节点(此时it
失效),但erase
返回的新迭代器会立即赋值给it
,使it
重新指向合法节点,避免后续操作使用失效迭代器。
5、总结:list
迭代器失效的核心要点
- 插入无失效:任何插入操作都不会使迭代器失效,因为其他节点的存储和指针关系未受影响。
- 删除仅局部失效:只有指向被删除节点的迭代器失效,其他迭代器仍可正常使用。
- 安全操作原则:在循环删除
list
元素时,必须通过it = l.erase(it)
或l.erase(it++)
的方式,确保每次删除后it
及时更新为有效迭代器,避免操作失效迭代器。
通过理解list
底层双向循环链表的结构特性(插入 / 删除仅修改相邻节点指针),可以更深刻地掌握其迭代器失效的规律,从而编写出安全、健壮的代码。
四、空间管理与性能特征
4.1 内存分配特点
- 动态节点分配:每个元素单独通过
new
分配内存 - 碎片化问题:
- 小对象(如 int):指针开销占比大(32 位系统占 66.7% 空间)
- 大对象:节点分散导致缓存命中率低(CPU 缓存行 64 字节,通常只能存 1 个大节点)
4.2 操作性能对比
操作 | vector 实现复杂度 | list 实现复杂度 | 关键差异点 |
---|---|---|---|
中间插入 | O (n)(元素搬移) | O (1)(指针操作) | 连续存储 vs 离散存储 |
随机访问 | O (1)(指针算术运算) | O (n)(迭代器遍历) | 地址连续性带来的优势 |
批量扩容 | O (n)(重新分配内存) | 无(按需分配节点) | 预分配策略 vs 即时分配 |
缓存局部性 | 高(连续存储) | 低(节点分散) | CPU 缓存利用率差异显著 |
五、模拟实现list的关键技术点
模拟实现双向链表(list)的详细解析
一、整体架构设计
本实现模拟C++ STL中的list
容器,采用 双向循环链表 结构,包含以下核心组件:
-
节点结构:存储数据及前后指针
-
迭代器:实现元素的访问与遍历
-
链表类:封装链表操作接口
-
内存管理:处理节点创建与销毁
二、节点结构设计(list_node)
template<class T>
struct list_node {list_node<T>* _next; // 后继指针list_node<T>* _prev; // 前驱指针T _val; // 存储的数据list_node(const T& val = T()) // 默认构造函数:_next(nullptr), _prev(nullptr), _val(val) {}
};
关键细节:
-
使用模板类支持泛型编程
-
默认构造函数使用
T()
初始化数据,确保内置类型初始化为0,自定义类型调用默认构造 -
双指针设计实现双向链表特性
三、迭代器设计(__list_iterator)
template<class T, class Ref, class Ptr>
struct __list_iterator {typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node; // 当前节点指针// 构造函数__list_iterator(Node* node) :_node(node) {}// 解引用操作符Ref operator*() { return _node->_val; }// 箭头操作符Ptr operator->() { return &_node->_val; }// 前后置++/--self& operator++() { _node = _node->_next; return *this; }self operator++(int) { /* 后置实现 */ }// 比较操作符bool operator!=(const self& it) const { return _node != it._node; }
};
三模板参数解析:详细讲解-->> (传送门)
-
T
:元素类型 -
Ref
:引用类型(T& 或 const T&) -
Ptr
:指针类型(T* 或 const T*)
设计意义:
-
代码复用:通过模板参数区分普通迭代器与const迭代器
-
普通迭代器:
__list_iterator<T, T&, T*>
-
const迭代器:
__list_iterator<T, const T&, const T*>
-
-
访问控制:
-
Ref
控制operator*
返回值的可修改性 -
Ptr
控制operator->
返回指针的常量性
-
迭代器特性:
-
双向迭代器:支持
++
/--
操作 -
浅封装:本质是节点指针的包装
-
核心操作符重载实现STL迭代器标准接口
四、链表类实现(list)
1. 类型定义与初始化
template<class T>
class list {typedef list_node<T> Node;
public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;void empty_init() {_head = new Node; // 创建头节点_head->_prev = _head; // 形成环状结构_head->_next = _head;_size = 0;}list() { empty_init(); } // 默认构造
};
关键点:
-
使用带头节点的循环链表设计
-
空链表时头节点自环
-
_size
成员优化size()
时间复杂度为O(1)
2. 迭代器相关
iterator begin() { return _head->_next; } // 首个有效节点
iterator end() { return _head; } // 头节点作为尾后const_iterator begin() const { return _head->_next; }
const_iterator end() const { return _head; }
设计特点:
-
end()
指向头节点,符合STL尾后迭代器规范 -
const版本迭代器禁止修改元素值
3. 拷贝控制
// 拷贝构造
list(const list<T>& lt) {empty_init();for (auto& e : lt) push_back(e);
}// 赋值运算符(copy-and-swap)
list<T>& operator=(list<T> lt) {swap(lt);return *this;
}void swap(list<T>& lt) {std::swap(_head, lt._head);std::swap(_size, lt._size);
}
关键机制:
-
深拷贝:通过遍历原链表逐个插入新节点
-
异常安全:swap操作保证强异常安全
-
高效赋值:参数为值传递,利用编译器优化
4. 元素访问操作
void push_back(const T& x) { insert(end(), x); }
void push_front(const T& x) { insert(begin(), x); }void pop_back() { erase(--end()); }
void pop_front() { erase(begin()); }
实现特点:
-
复用
insert
和erase
实现头尾操作 -
前置
--end()
获取最后一个有效节点
5. 核心操作insert/erase
iterator insert(iterator pos, const T& x) {Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x); // 创建新节点// 调整指针链接prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;_size++;return iterator(newnode);
}iterator erase(iterator pos) {assert(pos != end()); // 禁止删除头节点Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;// 跳过被删节点prev->_next = next;next->_prev = prev;delete cur; // 释放节点_size--;return iterator(next);
}
关键细节:
-
insert
在pos位置前插入新节点 -
erase
返回被删元素的下一个位置迭代器 -
严格的指针调整顺序防止链表断裂
-
_size
成员变量确保size()高效
6. 析构与清理
~list() {clear();delete _head;_head = nullptr;
}void clear() {iterator it = begin();while (it != end()) {it = erase(it);}_size = 0;
}
内存管理:
-
clear()
遍历删除所有有效节点 -
析构函数先清空元素再释放头节点
-
异常安全:假设元素析构不抛异常
五、关键设计解析
-
迭代器的模板参数:
-
通过
Ref
和Ptr
参数实现const迭代器与普通迭代器的代码复用 -
避免了传统
const_iterator
需要重写所有方法的冗余 -
示例:
typedef __list_iterator<T, T&, T*> iterator; // 普通迭代器 typedef __list_iterator<T, const T&, const T*> const_iterator; // const迭代器
-
-
循环链表结构:
-
头节点的
_prev
指向尾节点,_next
指向首节点 -
空链表时:
_head->_prev == _head->_next == _head
-
简化边界条件处理(无需特殊处理首尾操作)
-
-
size()优化:
-
维护
_size
成员变量替代遍历计数 -
插入/删除操作同步更新
_size
-
size()时间复杂度从O(n)优化到O(1)
-
-
异常安全保证:
-
insert操作先分配资源再修改结构
-
erase操作先调整链接再释放内存
-
遵循"资源分配可能失败,但结构修改不会失败"原则
-
六、使用示例
NJ::list<int> lst;
lst.push_back(1);
lst.push_front(0);for (auto& num : lst) {cout << num << " "; // 输出:0 1
}auto it = lst.begin();
lst.insert(++it, 5); // 在0和1之间插入5lst.pop_back(); // 删除尾元素1
list.h代码实现:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
using namespace std;namespace NJ {// 双向链表节点结构template<class T>struct list_node {list_node<T>* _next; // 指向下一个节点的指针list_node<T>* _prev; // 指向前一个节点的指针T _val; // 节点存储的值// 构造函数,使用默认值初始化节点list_node(const T& val = T()):_next(nullptr), _prev(nullptr), _val(val) {}};// 链表迭代器实现template<class T, class Ref, class Ptr>struct __list_iterator {typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node; // 迭代器当前指向的节点// 构造函数,用节点指针初始化迭代器__list_iterator(Node* node) :_node(node) {}// 解引用操作符,返回节点值的引用Ref operator*() {return _node->_val;}// 箭头操作符,返回节点值的指针Ptr operator->() {return &_node->_val;}// 前置++操作符,将迭代器移动到下一个节点self& operator++() {_node = _node->_next;return *this;}// 后置++操作符,返回当前迭代器副本,然后移动到下一个节点self operator++(int) {self tmp(*this);_node = _node->_next;return tmp;}// 前置--操作符,将迭代器移动到前一个节点self& operator--() {_node = _node->_prev; return *this; }// 后置--操作符,返回当前迭代器副本,然后移动到前一个节点self operator--(int) {self tmp(*this);_node = _node->_prev;return tmp;}// 不等于比较操作符bool operator!=(const self& it) const {return _node != it._node;}// 等于比较操作符bool operator==(const self& it) const {return _node == it._node;}};// 双向链表类template<class T>class list {typedef list_node<T> Node;public:// 定义迭代器类型typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;// 返回指向第一个元素的迭代器iterator begin() {return _head->_next;}// 返回指向尾后位置的迭代器iterator end() {return _head;}// const版本的begin()const_iterator begin() const {return _head->_next;}// const版本的end()const_iterator end() const {return _head;}// 初始化空链表void empty_init() {_head = new Node; // 创建头节点_head->_prev = _head; // 头节点的前驱指向自身_head->_next = _head; // 头节点的后继指向自身_size = 0; // 链表大小初始化为0}// 默认构造函数list() {empty_init();}// 拷贝构造函数list(const list<T>& it) {empty_init();// 遍历原链表,将每个元素添加到新链表for (auto& e : it) {push_back(e);}}// 交换两个链表的内容void swap(list<T>& it) {std::swap(_head, it._head); std::swap(_size, it._size); }// 赋值运算符重载,使用拷贝交换技术list<T>& operator=(list<T> it) {swap(it);return *this;}// 析构函数~list() {clear(); // 清空链表delete _head; // 删除头节点_head = nullptr;}// 清空链表,但保留头节点void clear() {iterator it = begin();while (it != end()) {it = erase(it); // 逐个删除节点}_size = 0;}// 在链表尾部插入元素void push_back(const T& x) {insert(end(), x);}// 在链表头部插入元素void push_front(const T& x) { insert(begin(), x);}// 删除链表尾部元素void pop_back() {erase(--end());}// 删除链表头部元素void pop_front() {erase(begin());}// 在指定位置前插入元素iterator insert(iterator pos, const T& x) {Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);// 插入新节点prev->_next = newnode; newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size; // 更新链表大小return iterator(newnode); // 返回指向新节点的迭代器}// 删除指定位置的元素iterator erase(iterator pos) {assert(pos != end()); // 确保不删除头节点Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next; // 调整指针,跳过当前节点prev->_next = next; next->_prev = prev;delete cur; // 删除节点--_size; // 更新链表大小return iterator(next); // 返回指向下一个节点的迭代器}// 返回链表大小size_t size() const { // 添加const修饰符,使其可以被const对象调用return _size;}private:Node* _head; // 头节点指针size_t _size; // 链表大小};
}
测试代码Test.cpp:
#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"
#include <iostream>
#include <string>using namespace std;int main() {// 测试默认构造函数NJ::list<int> lst;cout << "测试默认构造函数: 初始大小 = " << lst.size() << endl;// 测试push_backcout << "\n测试push_back: ";for (int i = 0; i < 5; ++i) {lst.push_back(i);cout << i << " ";}cout << "\n大小 = " << lst.size() << endl;// 测试迭代器遍历cout << "\n测试迭代器遍历: ";for (auto it = lst.begin(); it != lst.end(); ++it) {cout << *it << " ";}cout << endl;// 测试push_frontcout << "\n测试push_front: ";lst.push_front(-1);lst.push_front(-2);for (auto val : lst) { // 使用范围for循环cout << val << " ";}cout << "\n大小 = " << lst.size() << endl;// 测试pop_back和pop_frontcout << "\n测试pop_back和pop_front: ";lst.pop_back();lst.pop_front();for (auto val : lst) {cout << val << " ";}cout << "\n大小 = " << lst.size() << endl;// 测试insertcout << "\n测试insert: 在位置2插入100";auto it = lst.begin();++it; ++it; // 移动到位置2lst.insert(it, 100);for (auto val : lst) {cout << val << " ";}cout << "\n大小 = " << lst.size() << endl;// 测试erasecout << "\n测试erase: 删除位置2的元素";it = lst.begin();++it; ++it; // 移动到位置2lst.erase(it);for (auto val : lst) {cout << val << " ";}cout << "\n大小 = " << lst.size() << endl;// 测试拷贝构造函数cout << "\n测试拷贝构造函数: ";NJ::list<int> lst2(lst);cout << "lst2: ";for (auto val : lst2) {cout << val << " ";}cout << endl;// 测试赋值运算符cout << "\n测试赋值运算符: ";NJ::list<int> lst3;lst3 = lst;cout << "lst3: ";for (auto val : lst3) {cout << val << " ";}cout << endl;// 测试const迭代器cout << "\n测试const迭代器: ";const NJ::list<int>& clst = lst;for (auto it = clst.begin(); it != clst.end(); ++it) {cout << *it << " ";}cout << endl;// 测试clearcout << "\n测试clear: ";lst.clear();cout << "清空后大小 = " << lst.size() << endl;// 测试自定义类型cout << "\n测试自定义类型(string): ";NJ::list<string> strList;strList.push_back("Hello");strList.push_back("World");for (auto& str : strList) {cout << str << " ";}cout << endl;cout << "\n所有测试完成!" << endl;return 0;
}
六、典型应用场景与最佳实践
6.1 适用场景
- 频繁中间插入删除:如实现队列 / 栈的混合结构
- 无序数据的快速重组:如使用
splice
进行链表片段迁移 - 需要双向遍历但无需随机访问:如图的邻接表存储
6.2 性能优化建议
- 避免小对象存储:对 char/short 等类型,考虑使用
vector
+ 自定义内存池 - 迭代器本地化:尽量在局部作用域内使用迭代器,减少无效引用
- 批量操作替代单步操作:利用
insert(first, last)
替代多次insert
6.3 常见错误陷阱
- 空链表访问:调用
front()/back()
前必须检查empty()
- 迭代器失效未处理:删除操作后必须使用返回的新迭代器
- 错误使用随机访问:试图通过
it + n
访问元素(需改用循环advance(it, n)
)
七、与 vector 的深度对比表格
维度 | vector | list | 技术本质差异 |
---|---|---|---|
存储模型 | 连续内存块 | 离散节点 | 数组 vs 链表 |
迭代器类型 | 原生指针(T*) | 包装迭代器(封装节点指针) | 直接地址 vs 对象封装 |
扩容策略 | 按需倍增(分摊 O (1)) | 逐个节点分配 | 预分配 vs 即时分配 |
插入时迭代器失效 | 可能全量失效(扩容时) | 永不失效(仅修改指针) | 内存重分配 vs 指针操作 |
典型用例 | 数组模拟、科学计算 | 链表操作、事件队列 | 随机访问优先 vs 动态操作优先 |
空间效率(对象大小 S) | 100%(仅数据)+ 扩容冗余 | S + 2 * 指针(32 位系统额外 8 字节) | 紧凑存储 vs 元数据开销 |
遍历速度(10 万元素) | ~10μs(CPU 缓存友好) | ~100μs(缓存不命中) | 局部性原理的影响 |
八、总结:list 的设计哲学
8.1 设计权衡
- 用空间换时间:通过额外的指针空间,换取 O (1) 的插入删除性能
- 接口统一:遵循 STL 迭代器协议,与其他容器保持操作一致性
- 异常安全:插入操作保证强异常安全(失败时不修改容器状态)
8.2 学习价值
理解 list 的实现有助于:
- 掌握双向链表的工程化实现(带哨兵节点的循环结构)
- 深入理解迭代器适配器模式(反向迭代器的实现原理)
- 领悟 STL 容器的共性与特性(序列式容器的接口规范)
通过本文的深度解析,读者应能全面掌握 list 容器的底层机制、正确使用方法以及与其他容器的性能差异,从而在实际编程中做出更优的技术选择。