在C++标准模板库(STL)中,list是一个非常重要的容器,它实现了双向链表的数据结构。与vector这样的顺序容器不同,list在任意位置插入和删除元素的时间复杂度都是O(1),这使得它在某些场景下具有独特的优势。本文将深入探讨如何从零开始模拟实现一个功能完整的list类,重点关注其核心设计理念和实现细节。
list的实现基于双向带头链表,这种结构具有以下特点:
这种设计使得链表操作更加统一,无需对空链表、首节点、末节点等特殊情况做额外处理。哨兵节点作为链表的"锚点",始终存在,即使链表为空时也是如此。
节点类是list的基础构建块,我们使用模板类来实现:
cpp复制template <typename T>
struct ListNode {
T data;
ListNode* prev;
ListNode* next;
ListNode(const T& val = T())
: data(val), prev(nullptr), next(nullptr) {}
};
节点类包含三个成员:
构造函数提供了默认参数,允许创建空节点或带初始值的节点。
由于链表节点在内存中不是连续存储的,我们不能像数组那样直接对指针进行算术运算。因此需要封装一个迭代器类,重载相关运算符,使其表现得像指针一样。
迭代器类的核心设计思路:
我们使用模板技术实现一个通用的迭代器类:
cpp复制template <typename T, typename Ref, typename Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* node;
ListIterator(Node* x = nullptr) : node(x) {}
// 解引用运算符
Ref operator*() const { return node->data; }
// 成员访问运算符
Ptr operator->() const { return &(node->data); }
// 前置++
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& x) const { return node == x.node; }
bool operator!=(const Self& x) const { return node != x.node; }
};
这种设计通过模板参数Ref和Ptr来区分const和非const迭代器,避免了代码重复。
list类的主要成员变量包括:
cpp复制private:
Node* sentinel; // 哨兵节点
size_t size; // 元素数量
构造函数需要初始化哨兵节点:
cpp复制void init() {
sentinel = new Node();
sentinel->prev = sentinel;
sentinel->next = sentinel;
size = 0;
}
list() { init(); }
list(size_t n, const T& value = T()) {
init();
while (n--) push_back(value);
}
begin()和end()函数返回指向首元素和尾后位置的迭代器:
cpp复制iterator begin() { return iterator(sentinel->next); }
const_iterator begin() const { return const_iterator(sentinel->next); }
iterator end() { return iterator(sentinel); }
const_iterator end() const { return const_iterator(sentinel); }
insert()是核心操作,其他插入删除操作都可以基于它实现:
cpp复制iterator insert(iterator position, const T& x) {
Node* tmp = new Node(x);
tmp->next = position.node;
tmp->prev = position.node->prev;
position.node->prev->next = tmp;
position.node->prev = tmp;
++size;
return iterator(tmp);
}
iterator erase(iterator position) {
Node* next_node = position.node->next;
position.node->prev->next = position.node->next;
position.node->next->prev = position.node->prev;
delete position.node;
--size;
return iterator(next_node);
}
基于insert和erase,我们可以实现push_back、push_front等常用操作:
cpp复制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()); }
list需要正确处理拷贝构造、赋值和析构:
cpp复制void clear() {
Node* cur = sentinel->next;
while (cur != sentinel) {
Node* tmp = cur;
cur = cur->next;
delete tmp;
}
sentinel->next = sentinel;
sentinel->prev = sentinel;
size = 0;
}
~list() {
clear();
delete sentinel;
}
list(const list& x) {
init();
for (auto it = x.begin(); it != x.end(); ++it)
push_back(*it);
}
list& operator=(const list& x) {
if (this != &x) {
list tmp(x);
swap(tmp);
}
return *this;
}
void swap(list& x) {
std::swap(sentinel, x.sentinel);
std::swap(size, x.size);
}
哨兵节点的引入带来了几个重要优势:
list的迭代器在以下情况下会失效:
与vector不同,list的插入操作不会导致其他迭代器失效,这是由链表的内存分配特性决定的。
我们的实现保证了基本的异常安全:
每个元素需要额外的两个指针空间(前驱和后继),在64位系统上通常是16字节。此外还有一个固定的哨兵节点开销。
cpp复制void test_list() {
list<int> lst;
// 测试插入
for (int i = 0; i < 10; ++i)
lst.push_back(i);
// 测试遍历
for (auto it = lst.begin(); it != lst.end(); ++it)
std::cout << *it << " ";
std::cout << std::endl;
// 测试删除
lst.erase(++++lst.begin());
// 测试拷贝
list<int> lst2 = lst;
// 测试大小
assert(lst.size() == 9);
assert(lst2.size() == 9);
}
list在以下场景中表现优异:
在实际应用中,可以为list实现自定义分配器,这在某些特定场景下可以显著提高性能:
STL的list提供了rbegin()和rend()接口,我们可以通过适配器模式实现:
cpp复制template <typename Iterator>
class ReverseIterator {
Iterator current;
public:
// 实现反向迭代器的各种操作
};
reverse_iterator rbegin() { return reverse_iterator(end()); }
reverse_iterator rend() { return reverse_iterator(begin()); }
splice是list特有的高效操作,可以在常数时间内将元素从一个list转移到另一个list:
cpp复制void splice(iterator position, list& x, iterator first, iterator last) {
if (first == last) return;
// 计算转移的元素数量
size_t n = 0;
for (iterator it = first; it != last; ++it, ++n);
// 调整指针
last.node->prev->next = position.node;
first.node->prev->next = last.node;
position.node->prev->next = first.node;
Node* tmp = position.node->prev;
position.node->prev = last.node->prev;
last.node->prev = first.node->prev;
first.node->prev = tmp;
// 调整大小
size += n;
x.size -= n;
}
list不能使用标准库的sort算法(需要随机访问迭代器),需要实现自己的排序:
cpp复制void sort() {
// 空链表或单元素链表已经有序
if (size < 2) return;
// 使用归并排序算法
list carry;
list counter[64];
int fill = 0;
while (!empty()) {
carry.splice(carry.begin(), *this, begin());
int i = 0;
while (i < fill && !counter[i].empty()) {
counter[i].merge(carry);
carry.swap(counter[i++]);
}
carry.swap(counter[i]);
if (i == fill) ++fill;
}
for (int i = 1; i < fill; ++i)
counter[i].merge(counter[i-1]);
swap(counter[fill-1]);
}
实现自定义的节点分配器可以帮助检测内存泄漏:
cpp复制template <typename T>
class DebugAllocator {
static int count;
public:
T* allocate(size_t n) {
count += n;
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, size_t n) {
count -= n;
::operator delete(p);
}
static int get_count() { return count; }
};
可以增加调试代码验证迭代器有效性:
cpp复制void check_iterator(iterator it) const {
#ifdef DEBUG
Node* p = sentinel->next;
while (p != sentinel) {
if (p == it.node) return;
p = p->next;
}
throw std::runtime_error("Invalid iterator");
#endif
}
基本实现不是线程安全的,可以通过以下方式增强:
通过完整实现一个list类,我们深入理解了以下关键点:
在实际项目中使用list时,建议: