深入解析C++ STL list容器实现
·
好的,这是一份关于 C++ STL list 容器底层实现的详细笔记。
STL list 容器详解
STL 中的 list 是一个双向链表。与 vector 和 deque 不同,它不是连续存储的线性结构,而是由一系列在内存中不一定连续存放的节点组成。每个节点包含数据和指向其前驱节点和后继节点的指针。这种结构使得 list 在任意位置的插入和删除操作非常高效,时间复杂度为 $O(1)$,但随机访问效率较低,时间复杂度为 $O(n)$。
核心数据结构:节点 _Node
list 的实现核心是一个表示节点的结构体或类模板。每个节点通常包含三个部分:
- 数据域:存储用户提供的元素值。
- 前驱指针:指向链表中前一个节点。
- 后继指针:指向链表中后一个节点。
template <typename _Tp>
struct _List_node {
_List_node* _M_next; // 后继指针
_List_node* _M_prev; // 前驱指针
_Tp _M_data; // 存储的数据
};
list 类模板
list 本身是一个类模板,主要包含以下关键成员:
-
头节点指针
_M_head:- 通常指向一个特殊的节点,称为哨兵节点或头节点。
- 这个节点本身不存储有效数据。
- 它的
_M_next指向链表的第一个有效节点(如果链表为空,则指向自身)。 - 它的
_M_prev指向链表的最后一个有效节点(如果链表为空,则指向自身)。 - 使用哨兵节点可以简化边界条件(如空链表)的处理,并使得
begin()和end()的实现更清晰。
-
节点分配器
_M_get_Node_allocator():list使用分配器来管理节点的内存分配和释放。- 分配器通常作为模板参数传入,默认使用
std::allocator。
迭代器 iterator
list 的迭代器是一个双向迭代器。它封装了一个指向 _List_node 的指针,并重载了相应的运算符(如 ++, --, *, ->)来模拟指针行为。
template <typename _Tp>
struct _List_iterator {
_List_node_base* _M_node; // 通常指向一个_List_node_base(包含指针部分)
// 运算符重载
_Tp& operator*() const { return static_cast<_List_node<_Tp>*>(_M_node)->_M_data; }
_List_iterator& operator++() { _M_node = _M_node->_M_next; return *this; }
// ... 其他运算符重载
};
begin():返回指向第一个有效节点的迭代器(即_M_head->_M_next)。end():返回指向哨兵节点的迭代器(即_M_head)。它不指向有效数据,代表末尾的下一个位置。
关键操作实现
-
插入操作
insert:- 在指定位置
pos前插入新元素。 - 创建一个新节点
__new_node。 - 调整指针:
__new_node->_M_next = __pos._M_node; // 新节点指向pos位置的节点 __new_node->_M_prev = __pos._M_node->_M_prev; // 新节点指向pos的前驱 __pos._M_node->_M_prev->_M_next = __new_node; // pos前驱的后继指向新节点 __pos._M_node->_M_prev = __new_node; // pos的前驱指向新节点 - 时间复杂度:$O(1)$。
- 在指定位置
-
删除操作
erase:- 删除指定位置
pos的元素。 - 调整指针:
__pos._M_node->_M_prev->_M_next = __pos._M_node->_M_next; // pos前驱的后继指向pos的后继 __pos._M_node->_M_next->_M_prev = __pos._M_node->_M_prev; // pos后继的前驱指向pos的前驱 - 释放
__pos._M_node的内存。 - 时间复杂度:$O(1)$。
- 删除指定位置
-
push_back/push_front:- 本质上是分别在
end()和begin()前插入元素。直接调用insert即可。 - 时间复杂度:$O(1)$。
- 本质上是分别在
-
pop_back/pop_front:- 本质上是删除
end()的前一个元素和begin()位置的元素。直接调用erase即可。 - 时间复杂度:$O(1)$。
- 本质上是删除
-
size():- 标准库实现通常会维护一个
size成员变量,在插入和删除时更新它,使得size()可以在 $O(1)$ 时间内完成。 - 部分实现(或早期实现)可能选择遍历链表计数,时间复杂度 $O(n)$。
- 标准库实现通常会维护一个
-
splice:- 将另一个链表的一部分或全部移动到当前链表的指定位置。
- 核心操作是调整相关节点的指针,将源链表中的节点链入目标位置,并断开其在原链表中的连接。
- 时间复杂度:$O(1)$ 或 $O(n)$,取决于移动的范围。
内存管理
- 节点的内存分配和释放由分配器负责。
- 分配器通常使用
new和delete,但也可以配置为使用内存池等优化策略。 - 在
clear()或析构函数中,需要遍历链表,逐个释放所有节点。
简单实现示例 (简化版)
template <typename T>
class List {
private:
struct Node {
Node* next;
Node* prev;
T data;
Node(const T& val = T(), Node* n = nullptr, Node* p = nullptr)
: data(val), next(n), prev(p) {}
};
Node* head; // 哨兵节点
size_t count; // 元素个数计数器
public:
List() : head(new Node()), count(0) {
head->next = head;
head->prev = head;
}
~List() {
clear();
delete head;
}
void push_back(const T& value) {
Node* newNode = new Node(value, head, head->prev);
head->prev->next = newNode;
head->prev = newNode;
++count;
}
void pop_back() {
if (count == 0) return;
Node* last = head->prev;
last->prev->next = head;
head->prev = last->prev;
delete last;
--count;
}
void clear() {
Node* cur = head->next;
while (cur != head) {
Node* next = cur->next;
delete cur;
cur = next;
}
head->next = head;
head->prev = head;
count = 0;
}
// ... 其他成员函数 (begin, end, insert, erase, size 等)
};
总结
- 结构:双向链表,节点包含数据、前驱指针、后继指针。
- 哨兵节点:简化边界处理,
begin()指向第一个节点,end()指向哨兵节点。 - 迭代器:双向迭代器,封装节点指针。
- 插入/删除:核心是调整节点指针,时间复杂度 $O(1)$。
- 随机访问:效率低,$O(n)$。
- 内存:非连续存储,节点动态分配。
- 适用场景:需要频繁在任意位置插入和删除元素,不需要随机访问。
理解 list 的底层实现有助于更好地运用它,并明白其与 vector、deque 在性能特性上的根本区别。
更多推荐

所有评论(0)