在C++标准模板库(STL)中,list是一个经典的双向链表实现。与vector这种连续存储的容器不同,list采用非连续的动态存储方式,允许在序列的任何位置进行快速插入和删除操作。但它的随机访问效率较低,需要从头或从尾开始遍历。
我见过太多开发者只是简单地调用list的API,却对它的内部实现机制一知半解。今天我们就来彻底解剖这个容器,并最终实现一个简化版的list。这不仅有助于理解STL的设计哲学,更能提升你对指针操作和内存管理的掌握程度。
list的基本构建单元是节点(node),每个节点包含三个部分:
cpp复制template <typename T>
struct ListNode {
T data; // 存储的实际数据
ListNode* prev; // 指向前驱节点的指针
ListNode* next; // 指向后继节点的指针
// 构造函数
ListNode(const T& val = T(), ListNode* p = nullptr, ListNode* n = nullptr)
: data(val), prev(p), next(n) {}
};
这个设计有几个关键点值得注意:
STL的精妙之处在于它通过迭代器统一了各种容器的访问接口。对于list,我们需要实现一个双向迭代器:
cpp复制template <typename T>
class ListIterator {
public:
// 必要的类型定义
using iterator_category = std::bidirectional_iterator_tag;
using value_type = T;
using difference_type = std::ptrdiff_t;
using pointer = T*;
using reference = T&;
// 构造函数
explicit ListIterator(ListNode<T>* p = nullptr) : current(p) {}
// 解引用操作符
reference operator*() const { return current->data; }
// 成员访问操作符
pointer operator->() const { return &(current->data); }
// 前置++
ListIterator& operator++() {
current = current->next;
return *this;
}
// 后置++
ListIterator operator++(int) {
ListIterator tmp = *this;
++(*this);
return tmp;
}
// 前置--
ListIterator& operator--() {
current = current->prev;
return *this;
}
// 后置--
ListIterator operator--(int) {
ListIterator tmp = *this;
--(*this);
return tmp;
}
// 比较操作符
bool operator==(const ListIterator& rhs) const { return current == rhs.current; }
bool operator!=(const ListIterator& rhs) const { return !(*this == rhs); }
private:
ListNode<T>* current; // 当前节点指针
};
这个迭代器实现了所有必要的操作符重载,使得我们的list可以像STL容器一样使用标准算法。
现在我们可以开始构建完整的list类了:
cpp复制template <typename T>
class MyList {
public:
// 类型定义
using value_type = T;
using reference = T&;
using const_reference = const T&;
using iterator = ListIterator<T>;
using const_iterator = const ListIterator<T>;
using size_type = std::size_t;
// 构造函数和析构函数
MyList();
explicit MyList(size_type count, const T& value = T());
MyList(const MyList& other);
~MyList();
// 赋值操作
MyList& operator=(const MyList& other);
// 迭代器相关
iterator begin() { return iterator(head->next); }
iterator end() { return iterator(tail); }
const_iterator begin() const { return const_iterator(head->next); }
const_iterator end() const { return const_iterator(tail); }
// 容量相关
bool empty() const { return size_ == 0; }
size_type size() const { return size_; }
// 元素访问
reference front() { return head->next->data; }
reference back() { return tail->prev->data; }
const_reference front() const { return head->next->data; }
const_reference back() const { return tail->prev->data; }
// 修改器
void push_front(const T& value);
void push_back(const T& value);
void pop_front();
void pop_back();
iterator insert(iterator pos, const T& value);
iterator erase(iterator pos);
void clear();
private:
ListNode<T>* head; // 哨兵头节点
ListNode<T>* tail; // 哨兵尾节点
size_type size_; // 元素数量
// 辅助函数
void init(); // 初始化哨兵节点
void destroy(); // 销毁所有节点
};
让我们实现几个关键的构造函数:
cpp复制// 默认构造函数
template <typename T>
MyList<T>::MyList() : size_(0) {
init();
}
// 带参数的构造函数
template <typename T>
MyList<T>::MyList(size_type count, const T& value) : size_(0) {
init();
for (size_type i = 0; i < count; ++i) {
push_back(value);
}
}
// 拷贝构造函数
template <typename T>
MyList<T>::MyList(const MyList& other) : size_(0) {
init();
for (const auto& item : other) {
push_back(item);
}
}
// 初始化哨兵节点
template <typename T>
void MyList<T>::init() {
head = new ListNode<T>();
tail = new ListNode<T>();
head->next = tail;
tail->prev = head;
}
cpp复制template <typename T>
void MyList<T>::push_back(const T& value) {
insert(end(), value);
}
template <typename T>
void MyList<T>::push_front(const T& value) {
insert(begin(), value);
}
template <typename T>
typename MyList<T>::iterator MyList<T>::insert(iterator pos, const T& value) {
ListNode<T>* curr = pos.current;
ListNode<T>* newNode = new ListNode<T>(value, curr->prev, curr);
curr->prev->next = newNode;
curr->prev = newNode;
++size_;
return iterator(newNode);
}
cpp复制template <typename T>
void MyList<T>::pop_back() {
erase(--end());
}
template <typename T>
void MyList<T>::pop_front() {
erase(begin());
}
template <typename T>
typename MyList<T>::iterator MyList<T>::erase(iterator pos) {
ListNode<T>* curr = pos.current;
iterator ret(curr->next);
curr->prev->next = curr->next;
curr->next->prev = curr->prev;
delete curr;
--size_;
return ret;
}
cpp复制template <typename T>
void MyList<T>::clear() {
while (!empty()) {
pop_front();
}
}
template <typename T>
void MyList<T>::destroy() {
clear();
delete head;
delete tail;
head = tail = nullptr;
size_ = 0;
}
template <typename T>
MyList<T>::~MyList() {
destroy();
}
你可能注意到了我们在list中使用了两个额外的节点head和tail,它们不存储实际数据,被称为哨兵节点(sentinel nodes)。这种设计有几个优点:
在实现插入和删除操作时,我们需要特别注意异常安全。我们的实现遵循了以下原则:
与vector不同,list的迭代器在插入和删除操作后通常不会失效,除非是删除当前迭代器指向的元素。这是链表结构的天然优势。
我们可以考虑以下优化:
为了验证我们的实现,我们需要编写全面的测试代码:
cpp复制void test_MyList() {
// 测试默认构造函数
MyList<int> list1;
assert(list1.empty());
assert(list1.size() == 0);
// 测试push_back和push_front
list1.push_back(1);
list1.push_front(0);
assert(list1.size() == 2);
assert(list1.front() == 0);
assert(list1.back() == 1);
// 测试迭代器
int sum = 0;
for (auto it = list1.begin(); it != list1.end(); ++it) {
sum += *it;
}
assert(sum == 1);
// 测试插入和删除
auto it = list1.begin();
++it;
list1.insert(it, 5);
assert(list1.size() == 3);
it = list1.begin();
++it;
it = list1.erase(it);
assert(*it == 1);
assert(list1.size() == 2);
// 测试拷贝构造函数和赋值操作
MyList<int> list2(list1);
assert(list2.size() == 2);
MyList<int> list3;
list3 = list1;
assert(list3.size() == 2);
// 测试clear
list3.clear();
assert(list3.empty());
}
我们的简化实现与标准库的list相比有几个主要区别:
如果你想进一步挑战自己,可以考虑实现以下功能:
手写STL容器是深入理解C++的绝佳途径。通过这次实现,你应该对以下几点有了更深刻的认识:
记住,理解这些底层实现不是为了让你每次都自己造轮子,而是为了在需要时能够做出更明智的选择。大多数情况下,标准库的实现已经足够优秀,直接使用它们是最佳实践。