作为C++标准模板库(STL)中最基础的序列式容器之一,list以双向链表的数据结构实现,与vector的连续线性空间形成鲜明对比。我在实际项目中最常遇到这样的场景:当程序需要频繁在任意位置插入删除元素,且无法承受vector元素搬移的开销时,list就成了不二之选。
list的核心优势在于其时间复杂度特性:
这种特性使得list特别适合以下场景:
注意:虽然list的插入删除效率高,但遍历查找效率较低。在实际项目中,如果既需要频繁插入删除又需要快速查找,可以考虑使用unordered_map等关联容器。
list提供了多种构造函数,最常用的默认构造创建一个空容器:
cpp复制std::list<int> lst1; // 空list
std::list<std::string> lst2(10); // 10个默认构造的string
std::list<double> lst3(5, 3.14); // 5个3.14
内存管理方面,list的节点分配通常采用allocator机制。每个节点除了存储元素值外,还包含指向前后节点的指针:
cpp复制struct _List_node {
_List_node* _M_next;
_List_node* _M_prev;
_Tp _M_data;
};
迭代器操作:
list的迭代器属于双向迭代器,支持++和--操作但不支持随机访问(如it + 5):
cpp复制std::list<int>::iterator it = lst.begin();
++it; // 正确
it += 2; // 错误!编译不通过
元素访问操作:
修改操作:
cpp复制std::list<int> lst1{1,2,3}, lst2{4,5,6};
lst1.splice(lst1.end(), lst2);
// lst1变为{1,2,3,4,5,6}, lst2变为空
首先定义链表节点结构体:
cpp复制template<typename T>
struct __list_node {
__list_node* prev;
__list_node* next;
T data;
};
迭代器类需要重载关键操作符:
cpp复制template<typename T>
struct __list_iterator {
typedef __list_node<T> node_type;
node_type* node;
// 重载操作符
T& operator*() { return node->data; }
__list_iterator& operator++() {
node = node->next;
return *this;
}
bool operator!=(const __list_iterator& other) {
return node != other.node;
}
};
构造函数与析构函数:
cpp复制template<typename T>
class list {
public:
list() {
head = new node;
head->next = head->prev = head;
}
~list() {
clear();
delete head;
}
private:
node* head;
};
插入删除操作:
cpp复制iterator insert(iterator pos, const T& value) {
node* new_node = new node(value);
new_node->next = pos.node;
new_node->prev = pos.node->prev;
pos.node->prev->next = new_node;
pos.node->prev = new_node;
return iterator(new_node);
}
iterator erase(iterator pos) {
node* next_node = pos.node->next;
pos.node->prev->next = pos.node->next;
pos.node->next->prev = pos.node->prev;
delete pos.node;
return iterator(next_node);
}
标准库实现通常会采用以下优化手段:
所有修改操作都需要提供强异常安全保证:
cpp复制void push_back(const T& value) {
node* new_node = nullptr;
try {
new_node = new node(value);
// 链接新节点
} catch(...) {
delete new_node;
throw;
}
}
list的迭代器在以下情况会失效:
解决方案:
cpp复制std::list<int> lst{1,2,3};
auto it = lst.begin();
it = lst.erase(it); // it现在指向下一个有效元素
当list存储大对象时,频繁的构造/析构可能成为性能瓶颈。可以采用以下优化:
cpp复制lst.emplace_back(arg1, arg2); // 直接在节点中构造
cpp复制list(list&& other) noexcept; // 移动构造函数
在游戏开发中,我们常用list来管理游戏实体:
cpp复制class GameEntity {
std::list<GameEntity*> children;
public:
void addChild(GameEntity* child) {
children.push_back(child);
}
void removeChild(GameEntity* child) {
children.remove_if([=](auto p) {
return p == child;
});
}
};
这种设计允许:
在实现UI系统时,list也常用于维护控件z-order,因为需要频繁调整控件显示顺序。
| 特性 | list | vector | deque |
|---|---|---|---|
| 随机访问 | 不支持 | O(1) | O(1) |
| 头部插入删除 | O(1) | O(n) | O(1) |
| 中间插入删除 | O(1) | O(n) | O(n) |
| 迭代器失效频率 | 低 | 高 | 中等 |
| 内存使用 | 高(指针开销) | 低 | 中等 |
选型建议:
现代C++为list添加了若干新特性:
cpp复制lst1.splice(lst1.end(), lst2, lst2.begin(), lst2.end());
cpp复制lst.emplace(lst.begin(), args...);
cpp复制for (auto [iter, end] = std::pair{lst.begin(), lst.end()}; iter != end; ++iter) {
// ...
}
cpp复制auto nh = lst.extract(lst.begin());
nh.value() = 42;
lst.insert(lst.end(), std::move(nh));
使用gdb时,可以添加python脚本增强list可视化:
python复制# ~/.gdbinit
python
import gdb.libstdcxx.v6.printers as printers
printers.register_libstdcxx_printers(None)
end
然后可以直接打印list内容:
code复制(gdb) p myList
$1 = std::list = {1, 2, 3}
使用perf工具分析list热点:
优化建议:
list的节点分配策略可以通过自定义allocator优化:
cpp复制template<typename T>
class pool_allocator {
public:
using value_type = T;
pool_allocator() noexcept = default;
T* allocate(size_t n) {
if (n != 1) throw std::bad_alloc();
return static_cast<T*>(pool.allocate(sizeof(T)));
}
void deallocate(T* p, size_t n) {
pool.deallocate(p, sizeof(T));
}
private:
memory_pool pool;
};
using custom_list = std::list<int, pool_allocator<int>>;
这种allocator可以: