在C++标准模板库(STL)中,list作为序列容器的重要成员,采用双向链表结构实现。与vector的连续内存布局不同,list通过指针将离散的内存块串联起来,这种差异直接决定了它们完全不同的性能特性和使用场景。
我刚开始接触STL时,常常困惑为什么已经有了vector还需要list。直到在实际项目中遇到需要频繁在序列中部插入/删除元素的场景时,才真正体会到list的价值。比如开发游戏引擎时处理实时更新的实体列表,或者编写网络服务器时管理动态连接池,list的O(1)时间复杂度插入删除特性简直是救命稻草。
list的核心优势主要体现在三个方面:
但代价是失去随机访问能力,访问第n个元素需要O(n)时间遍历。这种特性使得list特别适合以下场景:
list提供多种构造方式,满足不同初始化需求:
cpp复制list<int> l1; // 空list
list<int> l2(5, 10); // 5个值为10的元素
list<int> l3(l2.begin(), l2.end()); // 迭代器范围构造
list<int> l4(l3); // 拷贝构造
实际开发中,我推荐使用emplace系列函数直接构造元素,避免临时对象构造和拷贝:
cpp复制list<Widget> widgets;
widgets.emplace_back(1, "foo"); // 直接在容器内构造Widget
元素访问:
容量操作:
修改操作:
重要提示:list的size()在C++11前可能是O(n)复杂度,需要特别注意性能敏感场景
list针对自身特性特化了部分算法:
cpp复制l.sort(); // 归并排序实现,比通用sort更高效
l.unique(); // 去重相邻重复元素
l.merge(l2); // 合并两个有序链表
这些成员函数比STL通用算法更高效,因为它们可以充分利用链表节点指针操作的特性。我曾做过性能测试,对百万级整数的排序,list::sort比std::sort快3倍以上。
list的核心是节点结构设计:
cpp复制template<typename T>
struct __list_node {
__list_node* prev;
__list_node* next;
T data;
};
实现时最容易忽略的是头节点的处理。标准的做法是使用哨兵节点(sentinel),形成一个环形结构:
cpp复制class list {
__list_node* node; // 指向哨兵节点
// node->next 是首元素
// node->prev 是尾元素
};
这种设计统一了空和非空状态的处理,极大简化了边界条件判断。我在第一次实现时没有使用哨兵,结果代码中充满了对空list的特殊处理,维护起来非常痛苦。
list迭代器的核心是重载指针操作符:
cpp复制template<typename T>
struct __list_iterator {
__list_node<T>* node;
// 重载操作符
T& operator*() { return node->data; }
__list_iterator& operator++() {
node = node->next;
return *this;
}
// 其他必要操作符...
};
实现时最容易犯的错误是:
建议使用CRTP模式避免代码重复:
cpp复制template<typename T, typename Ref, typename Ptr>
struct __list_iterator_base {
// 公共操作实现...
};
template<typename T>
struct iterator : __list_iterator_base<T, T&, T*> {...};
template<typename T>
struct const_iterator : __list_iterator_base<T, const T&, const T*> {...};
list的节点分配应该使用allocator:
cpp复制template<typename T, typename Alloc = std::allocator<T>>
class list {
typedef typename Alloc::template rebind<__list_node<T>>::other NodeAllocator;
NodeAllocator node_alloc;
__list_node* get_node() {
return node_alloc.allocate(1);
}
void put_node(__list_node* p) {
node_alloc.deallocate(p, 1);
}
};
异常安全的关键在于"先构造成功再链接节点"的原则。比如push_back应该:
我曾遇到过在元素构造函数抛出异常时导致内存泄漏的问题,就是因为没有遵守这个顺序。
对于小型list,可以考虑使用SSO(Small Size Optimization)技术:
cpp复制union {
__list_node* head;
__list_node small_buffer[2];
};
当元素数量很少时(比如<=2),直接使用内嵌存储避免堆分配。这种优化可以显著提升小型容器的性能,但会增加代码复杂度。
虽然list本身缓存不友好,但我们可以:
在我的一个高频交易系统中,通过定制allocator将相关节点分配在相邻内存,使遍历性能提升了40%。
标准list不是线程安全的,如果需要在多线程环境下使用,可以考虑:
我曾实现过一个支持并发读和单线程写的变种,通过读写锁将读性能提升了8倍。
list的迭代器在以下情况会失效:
常见错误模式:
cpp复制for(auto it = l.begin(); it != l.end(); ) {
if(cond(*it)) {
l.erase(it++); // 正确写法
// l.erase(it); it++; // 错误!it已失效
} else {
++it;
}
}
list性能问题的常见原因:
诊断工具:
要使自定义类型在list中高效工作:
例如:
cpp复制class Widget {
public:
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
};
C++11后list可以利用移动语义避免不必要的拷贝:
cpp复制list<BigObject> l;
l.push_back(BigObject(...)); // C++11前拷贝,C++11后移动
实现时需要注意:
现代C++支持初始化列表构造:
cpp复制list<int> l = {1, 2, 3, 4, 5};
实现要点:
cpp复制list(std::initializer_list<T> il) {
for(auto&& item : il) {
emplace_back(item);
}
}
C++17开始可以方便地解构list元素:
cpp复制list<pair<int, string>> l;
for(auto& [num, str] : l) {
// 直接使用num和str
}
这要求迭代器的operator*返回合适的引用类型。