文章目录
- 从零实现 `list` 容器:细粒度剖析与代码实现
- 前言
- 1. `list` 的核心数据结构
- 节点结构分析
- 2 迭代器设计与实现
- 2.1 为什么 `list` 需要迭代器?
- 2.2 实现一个简单的迭代器
- 2.3 测试简单迭代器
- 解释:
- 2.4 增加后向移动和 `->` 运算符
- 关键点:
- 实现代码:
- 2.5 测试前后移动和 `->` 运算符
- 目的:
- 测试代码:
- 输出:
- 2.6 为什么不能简单使用 `const` 修饰?
- 问题解释:
- 直接使用 `const` 修饰的限制:
- 错误示例:直接使用 `const` 修饰
- 错误代码:
- 错误分析:
- 2.6 正确解决方案:使用模板参数区分 `const` 和 `non-const`
- 为什么需要模板参数?
- 使用模板参数的好处:
- 正确的模板泛化代码实现:
- 关键点解释:
- 2.7 测试泛化后的迭代器
- 测试场景:
- 测试代码:
- 输出:
- 2.8 迭代器设计分析
- 3. `list` 容器的基本操作
- 3.1 构造函数
- 构造函数分析
- 4. 插入与删除操作
- 4.1 插入操作
- 插入操作分析
- 4.2 删除操作
- 删除操作分析
- 5. 反向迭代器
- 6. 迭代器失效问题
- 结论
从零实现 list
容器:细粒度剖析与代码实现
接上篇【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
💬 欢迎讨论:学习过程中有问题吗?随时在评论区与我交流。你们的互动是我创作的动力!
👍 支持我:如果你觉得这篇文章对你有帮助,请点赞、收藏并分享给更多朋友吧!
🚀 一起成长:欢迎分享给更多对计算机视觉和图像处理感兴趣的小伙伴,让我们共同进步!
本文详细介绍如何从零开始实现一个 C++ list
容器,帮助读者深入理解 list
的底层实现,包括核心数据结构、迭代器的设计、以及常见的插入、删除等操作。从初学者到进阶开发者都能从中受益。
前言
在 C++ 标准模板库 (STL) 中,list
是一种双向链表容器,适合频繁插入和删除操作。它与 vector
的主要区别在于不支持随机访问,且插入、删除时无需移动其他元素。这使得 list
在某些场景下具有独特优势,例如大量元素的动态操作。
为了更好地理解 list
的工作原理,我们将在本篇博客中模拟实现一个简单版的 list
,同时分析每个步骤背后的原理及其易错点。
1. list
的核心数据结构
我们首先要理解
list
的底层是由双向链表实现的。双向链表中的每个节点不仅包含数据,还包含两个指针,分别指向前一个节点和后一个节点。以下是节点结构的定义:
namespace W {// 定义链表节点template<class T>struct ListNode {T _val; // 节点存储的值ListNode* _prev; // 指向前一个节点ListNode* _next; // 指向后一个节点ListNode(const T& val = T()) : _val(val), _prev(nullptr), _next(nullptr) {}};
}
节点结构分析
_val
:存储节点的值。_prev
和_next
:分别指向前后节点,便于在链表中进行前后遍历和插入、删除操作。
2 迭代器设计与实现
2.1 为什么 list
需要迭代器?
在 C++ 中,
vector
是一种动态数组,元素在内存中是连续存储的,因此我们可以使用下标快速访问元素。例如,vec[0]
就可以直接访问到vector
的第一个元素。这是因为vector
中的每个元素都可以通过下标计算出准确的内存地址。
然而,
list
的底层是链表结构,链表节点在内存中不是连续存放的。因此,链表不能像数组那样通过下标随机访问元素。每个节点都通过指针链接到前一个节点(_prev
)和后一个节点(_next
)。这意味着,如果你想访问链表中的某个节点,必须从链表的起始位置开始,一个节点一个节点地遍历。这时我们就需要迭代器来方便地遍历链表。
迭代器的作用类似于一个指针,它指向链表中的某个节点,并允许我们通过类似指针的方式访问和操作链表节点。与普通指针不同,迭代器提供了更高级的功能,并且能够保持更好的接口一致性,因此它成为了 STL 容器中访问元素的核心工具。
2.2 实现一个简单的迭代器
为了实现最基本的链表迭代器,首先我们需要定义链表节点的结构。
namespace W {// 链表节点定义template<class T>struct ListNode {T _val; // 节点中存储的值ListNode* _prev; // 指向前一个节点ListNode* _next; // 指向后一个节点ListNode(const T& val = T()) : _val(val), _prev(nullptr), _next(nullptr) {}};
}
我们可以看到,ListNode
是一个模板结构,它包含:
- _val:存储链表节点的值。
- _prev:指向链表中前一个节点的指针。
- _next:指向链表中下一个节点的指针。
接下来,我们定义一个最基本的迭代器 ListIterator
。它内部保存了一个指向 ListNode
的指针 _node
,并且支持以下基本操作:
- 解引用:通过
*it
访问节点中的值。 - 前向移动:通过
++it
访问链表中的下一个节点。 - 比较:通过
it != end()
判断两个迭代器是否相等。
namespace W {template<class T>class ListIterator {typedef ListNode<T> Node; // 使用 Node 表示链表节点类型public:// 构造函数,接受一个指向链表节点的指针ListIterator(Node* node = nullptr) : _node(node) {}// 解引用操作,返回节点的值T& operator*() { return _node->_val; }// 前向移动操作,指向下一个节点ListIterator& operator++() {_node = _node->_next; // 将当前节点移动到下一个节点return *this; // 返回自身以支持链式调用}// 比较操作,判断两个迭代器是否相等bool operator!=(const ListIterator& other) const { return _node != other._node; }private:Node* _node; // 迭代器指向的链表节点};
}
2.3 测试简单迭代器
为了测试这个简单的迭代器,我们先创建几个链表节点,并将它们相互连接,形成一个链表。接着使用迭代器遍历链表,输出每个节点的值。
#include <iostream>int main() {// 创建三个节点,分别存储值 1、2、3W::ListNode<int> node1(1); W::ListNode<int> node2(2); W::ListNode<int> node3(3); // 链接节点形成链表node1._next = &node2; // node1 的下一个节点是 node2node2._prev = &node1; // node2 的前一个节点是 node1node2._next = &node3; // node2 的下一个节点是 node3node3._prev = &node2; // node3 的前一个节点是 node2// 创建迭代器,指向第一个节点W::ListIterator<int> it(&node1);// 使用迭代器遍历链表并输出每个节点的值while (it != nullptr) {std::cout << *it << std::endl; // 输出当前节点的值++it; // 前向移动到下一个节点}return 0;
}
输出:
1
2
3
解释:
it
初始指向第一个节点node1
。- 每次
*it
解引用获取当前节点的值,++it
将迭代器移动到链表中的下一个节点,直到链表结束。
2.4 增加后向移动和 ->
运算符
我们之前实现的迭代器只能向前移动。然而,list
是双向链表,因此我们还需要增加后向移动 --
的功能,以便迭代器可以从链表的末尾向前遍历。同时,为了让迭代器像指针一样工作,我们还要重载 ->
运算符,以支持通过 ->
访问节点的成员变量。
关键点:
-
当
_val
是基本数据类型(如int
)时,可以直接通过*it
来获取节点的值,而不需要使用*(it->)
。虽然*(it->)
语法上是正确的,但显得繁琐且不必要。为什么
*(it->)
是正确的?
因为it->
是在调用operator->()
,返回_val
的指针,然后*(it->)
解引用该指针。语法上是没有问题的,但通常我们直接使用*it
更简洁。 -
当
_val
是自定义类型时,可以使用it->x
直接访问自定义类型的成员变量x
。编译器会将it->x
优化为it.operator->()->x
,让访问更加方便。
实现代码:
namespace W {template<class T>class ListIterator {typedef ListNode<T> Node; // 使用 Node 表示链表节点类型public:ListIterator(Node* node = nullptr) : _node(node) {}// 解引用操作,返回节点的值T& operator*() { return _node->_val; }// 指针操作,返回节点的值的指针T* operator->() { return &(_node->_val); }// 前向移动,指向下一个节点ListIterator& operator++() {_node = _node->_next;return *this;}// 后向移动,指向前一个节点ListIterator& operator--() {_node = _node->_prev;return *this;}// 比较操作,判断两个迭代器是否相等bool operator!=(const ListIterator& other) const { return _node != other._node; }private:Node* _node; // 迭代器指向的链表节点};
}
2.5 测试前后移动和 ->
运算符
目的:
通过一个测试程序验证迭代器的前后移动功能,并使用 ->
运算符访问节点的值。我们将测试基本数据类型和自定义类型的情况,展示如何在不同数据类型下使用迭代器。
测试代码:
-
对于
int
类型,可以直接使用*it
访问节点的值,而不需要使用*(it->)
。虽然*(it->)
语法上是正确的,但没有必要,因为直接*it
就能得到节点的值。 -
对于自定义类型
CustomType
,可以通过it->x
来访问自定义类型CustomType
中的成员变量x
。
#include <iostream>struct CustomType {int x;
};int main() {// 创建三个节点,分别存储值 1、2、3W::ListNode<int> node1(1); W::ListNode<int> node2(2); W::ListNode<int> node3(3); // 链接节点形成链表node1._next = &node2;node2._prev = &node1;node2._next = &node3;node3._prev = &node2;// 创建迭代器,初始指向第二个节点W::ListIterator<int> it(&node2);// 对于 int 类型,直接使用 *it 访问节点的值std::cout << *it << std::endl; // 输出 2// 使用 it-> 访问 CustomType 的成员变量W::ListNode<CustomType> customNode1({1});W::ListNode<CustomType> customNode2({2});customNode1._next = &customNode2;customNode2._prev = &customNode1;W::ListIterator<CustomType> customIt(&customNode1);// 访问自定义类型 CustomType 的成员变量 xstd::cout << customIt->x << std::endl; // 输出 1return 0;
}
输出:
2
1
2.6 为什么不能简单使用 const
修饰?
问题解释:
在 vector
中,const_iterator
可以简单地通过 const
修饰实现,因为 vector
是连续内存存储的结构。const
只需要防止修改元素的值即可。但在 list
中,情况要复杂得多。list
是双向链表,迭代器不仅需要访问节点的值,还需要操作链表的前驱和后继节点(prev
和 next
)。简单使用 const
修饰的迭代器无法完全满足 list
的需求。
直接使用 const
修饰的限制:
const
修饰的迭代器会使得一些必要的操作(如前向或后向移动)无法进行。- 例如:直接对
const
迭代器执行++
或--
操作,会导致编译错误,因为这些操作需要修改迭代器的内部状态(指针),但const
修饰符禁止任何修改。
错误示例:直接使用 const
修饰
为了更清楚地说明问题,以下是一个错误示例,展示了为什么简单使用 const
修饰符会导致问题。
错误代码:
#include <iostream>template<class T>
struct ListNode {T _val;ListNode* _prev;ListNode* _next;ListNode(T val) : _val(val), _prev(nullptr), _next(nullptr) {}
};template<class T>
class ListIterator {typedef ListNode<T> Node;public:ListIterator(Node* node = nullptr) : _node(node) {}// 解引用操作,返回节点的值T& operator*() { return _node->_val; }// 前向移动ListIterator& operator++() {_node = _node->_next;return *this;}// 后向移动ListIterator& operator--() {_node = _node->_prev;return *this;}private:Node* _node;
};int main() {// 创建三个节点,分别存储值 1、2、3ListNode<int> node1(1), node2(2), node3(3);// 链接节点形成链表node1._next = &node2;node2._prev = &node1;node2._next = &node3;node3._prev = &node2;// 尝试创建一个常量迭代器const ListIterator<int> constIt(&node1);// 错误1:前向移动时,编译器报错,因为 ++ 操作符不能对 const 迭代器操作++constIt; // 编译错误// 错误2:解引用操作也无法进行修改*constIt = 5; // 编译错误
}
错误分析:
-
++constIt
无法使用:由于const
修饰,迭代器不能修改其指向的节点,因此++
操作无法进行,因为它需要修改迭代器的内部状态。 -
*constIt = 5
无法修改值:同样,由于const
修饰符,迭代器不能修改节点的值,因此编译器会报错。
总结:为什么不能简单使用 const
修饰?
- 限制过多:简单使用
const
修饰符会导致一些必要的操作无法进行,例如前向和后向移动操作++
和--
,因为const
禁止对迭代器内部状态的修改。 - 需要灵活区分:我们需要通过模板参数
Ref
和Ptr
来灵活区分哪些操作需要保持常量,哪些操作允许修改。这使得迭代器可以在常量和非常量链表中都能正确工作。 - 代码复用:使用模板参数使得我们只需要一套迭代器代码就可以处理
const
和non-const
的情况,提高了代码的简洁性和复用性。
2.6 正确解决方案:使用模板参数区分 const
和 non-const
为什么需要模板参数?
为了应对上面提到的问题,我们可以通过模板参数来区分 const
和 non-const
的情况。模板参数 Ref
和 Ptr
可以控制迭代器的行为,使得它在常量链表和非常量链表中都能够正常工作:
Ref
:控制解引用*
返回的是非常量引用T&
还是常量引用const T&
。Ptr
:控制通过->
操作符返回的是非常量指针T*
还是常量指针const T*
。
使用模板参数的好处:
- 灵活性:可以根据需要处理
const
和non-const
的迭代器场景。 - 保证安全性:对于常量链表,保证不能修改元素的值;而对于非常量链表,可以进行修改操作。
- 代码复用:通过模板参数,只需要编写一套迭代器代码,既可以用于常量链表,也可以用于非常量链表。
正确的模板泛化代码实现:
通过模板参数 Ref
和 Ptr
,我们可以实现支持 const
和 non-const
两种迭代器的代码。
namespace W {template<class T, class Ref, class Ptr>class ListIterator {typedef ListNode<T> Node; // 使用 Node 表示链表节点类型public:ListIterator(Node* node = nullptr) : _node(node) {}// 解引用操作,返回节点的值Ref operator*() const { return _node->_val; }// 指针操作,返回节点的值的指针Ptr operator->() const { return &_node->_val; }// 前向移动,指向下一个节点ListIterator& operator++() {_node = _node->_next;return *this;}// 后向移动,指向前一个节点ListIterator& operator--() {_node = _node->_prev;return *this;}// 比较操作,判断两个迭代器是否相等bool operator!=(const ListIterator& other) const { return _node != other._node; }private:Node* _node; // 迭代器指向的链表节点};
}
关键点解释:
-
模板参数
Ref
和Ptr
:这两个参数分别用于控制operator*
和operator->
的返回值类型:Ref
用于控制解引用操作的返回类型,可以是T&
或const T&
。Ptr
用于控制->
操作符的返回类型,可以是T*
或const T*
。
-
前向与后向移动:我们定义了
operator++
和operator--
,这些操作修改了迭代器的内部状态,允许它前向或后向遍历链表节点。
2.7 测试泛化后的迭代器
测试场景:
- 对非常量链表:我们测试
int
类型节点,通过*it
解引用获取节点值。 - 对自定义类型
CustomType
的链表:通过it->x
来访问自定义类型CustomType
的成员变量x
。 - 对常量链表:我们测试
const
迭代器,确保无法通过迭代器修改链表节点的值。
测试代码:
#include <iostream>struct CustomType {int x;
};int main() {// 创建三个 int 类型的节点,分别存储值 1、2、3W::ListNode<int> node1(1); W::ListNode<int> node2(2); W::ListNode<int> node3(3); // 链接节点形成链表node1._next = &node2;node2._prev = &node1;node2._next = &node3;node3._prev = &node2;// 创建一个非常量迭代器W::ListIterator<int, int&, int*> it(&node1);std::cout << *it << std::endl; // 输出 1++it; // 前向移动std::cout << *it << std::endl; // 输出 2// 创建自定义类型 CustomType 的链表节点W::ListNode<CustomType> customNode1({1});W::ListNode<CustomType> customNode2({2});customNode1._next = &customNode2;customNode2._prev = &customNode1;// 创建自定义类型的迭代器W::ListIterator<CustomType, CustomType&, CustomType*> customIt(&customNode1);std::cout << customIt->x << std::endl; // 输出 1// 创建一个常量链表const W::ListNode<int> constNode1(1);const W::ListNode<int> constNode2(2);constNode1._next = &constNode2;// 创建一个常量迭代器W::ListIterator<int, const int&, const int*> constIt(&constNode1);std::cout << *constIt << std::endl; // 输出 1// 常量迭代器不允许修改值// *constIt = 5; // 错误:无法修改常量链表节点的值return 0;
}
输出:
1
2
1
1
2.8 迭代器设计分析
通过以上步骤,我们设计了一个功能完整的 list
迭代器,支持以下功能:
- 指针操作:
*
和->
,可以访问节点的值和自定义类型的成员变量。 - 前向与后向移动:
++
和--
,可以在链表中双向遍历。 - 支持
const
和非const
:通过模板参数Ref
和Ptr
,迭代器能够根据链表是否为常量链表返回不同类型的值或指针,确保常量链表不能被修改。 - 代码复用:同一套代码可以处理常量链表和非常量链表,极大地提高了代码的灵活性和复用性。
3. list
容器的基本操作
现在我们有了节点和迭代器,接下来实现
list
的核心操作,包括构造、插入、删除和访问元素等。
3.1 构造函数
我们将实现多种构造函数,允许用户创建空链表、指定大小的链表,以及从迭代器区间构造链表。
namespace W {template<class T>class list {typedef ListNode<T> Node;public:typedef ListIterator<T, T&, T*> iterator;// 默认构造函数list() { CreateHead(); }// 指定大小的构造函数list(size_t n, const T& val = T()) {CreateHead();for (size_t i = 0; i < n; ++i)push_back(val);}// 迭代器区间构造函数template<class Iterator>list(Iterator first, Iterator last) {CreateHead();while (first != last) {push_back(*first);++first;}}// 析构函数~list() {clear();delete _head;}// 头节点初始化void CreateHead() {_head = new Node();_head->_next = _head;_head->_prev = _head;}// 清空链表void clear() {Node* cur = _head->_next;while (cur != _head) {Node* next = cur->_next;delete cur;cur = next;}_head->_next = _head;_head->_prev = _head;}private:Node* _head; // 指向头节点的指针};
}
构造函数分析
- 默认构造函数:创建一个空链表,并初始化头节点。
- 指定大小构造函数:使用
push_back
插入n
个值为val
的节点。 - 区间构造函数:从迭代器区间
[first, last)
中构造链表。
4. 插入与删除操作
list
容器的优势在于高效的插入与删除操作。我们将在指定位置插入节点,或删除指定节点,插入和删除时间复杂度均为 O(1)。
4.1 插入操作
namespace W {template<class T>class list {typedef ListNode<T> Node;typedef ListIterator<T, T&, T*> iterator;public:// 在指定位置前插入新节点iterator insert(iterator pos, const T& val) {Node* newNode = new Node(val);Node* cur = pos._node;newNode->_next = cur;newNode->_prev = cur->_prev;cur->_prev->_next = newNode;cur->_prev = newNode;return iterator(newNode);}// 在链表末尾插入新节点void push_back(const T& val) { insert(end(), val); }// 在链表头部插入新节点void push_front(const T& val) { insert(begin(), val); }};
}
插入操作分析
- 插入效率:在链表中插入时,仅需调整前后节点的指针,不涉及元素移动,因此效率为 O(1)。
- 头尾插入:通过
push_back
和push_front
,实现头部和尾部插入。
4.2 删除操作
namespace W {template<class T>class list {typedef ListNode<T> Node;typedef ListIterator<T, T&, T*> iterator;public:// 删除指定位置的节点iterator erase(iterator pos) {Node* cur = pos._node;Node* nextNode = cur->_next;cur->_prev->_next = cur->_next;cur->_next->_prev = cur->_prev;delete cur;return iterator(nextNode);}// 删除链表头部节点void pop_front() { erase(begin()); }// 删除链表尾部节点void pop_back() { erase(--end()); }};
}
删除操作分析
- 删除效率:与插入类似,删除操作仅涉及指针调整,不需要移动元素,效率为 O(1)。
- 头尾删除:通过
pop_front
和pop_back
实现头部和尾部删除操作。
5. 反向迭代器
在双向链表中,反向迭代器可以通过包装普通迭代器实现。反向迭代器的 ++
相当于正向迭代器的
--
,反之亦然。
namespace W {template<class Iterator>class ReverseListIterator {Iterator _it;public:ReverseListIterator(Iterator it) : _it(it) {}auto operator*() { Iterator temp = _it; --temp; return *temp; }auto operator->() { return &(operator*()); }ReverseListIterator& operator++() { --_it; return *this; }ReverseListIterator operator++(int) { ReverseListIterator temp = *this; --_it; return temp; }ReverseListIterator& operator--() { ++_it; return *this; }ReverseListIterator operator--(int) { ReverseListIterator temp = *this; ++_it; return temp; }bool operator==(const ReverseListIterator& other) const { return _it == other._it; }bool operator!=(const ReverseListIterator& other) const { return !(*this == other); }};
}
6. 迭代器失效问题
当我们删除一个节点时,指向该节点的迭代器会失效。如果继续使用该迭代器,会导致未定义行为。因此,在删除操作后,我们需要使用 erase
返回的迭代器。
void TestIteratorInvalidation() {W::list<int> lst = {1, 2, 3, 4, 5};auto it = lst.begin();while (it != lst.end()) {it = lst.erase(it); // 正确:使用 erase 返回的新迭代器}
}
结论
通过本次模拟实现,我们深入剖析了 C++ list
的核心功能,从双向链表的数据结构,到迭代器的设计,再到插入和删除操作的高效实现。希望通过这篇文章,大家对 list
有了更为深入的理解,并能在实际开发中灵活运用。
以上就是关于【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️