C++ STL 容器 list 底层结构详解


在 C++ 标准模板库(STL)的众多容器中,std::list 是一款以高效插入删除为核心优势的链式容器。它基于双向链表实现,不依赖连续内存空间,能够在任意位置实现常数时间复杂度的元素插入与删除,特别适合频繁修改容器结构的场景。与 vector 等连续存储容器不同,list 的元素以独立节点形式存在,通过指针相互链接,因此不会因扩容带来大量元素拷贝,也不会出现内存重新分配导致迭代器失效的问题。但受链表结构限制,它不支持随机访问,访问中间元素必须从头遍历。从迭代器设计、节点管理到异常安全机制,list 的每一处实现都体现了 "以访问效率换取修改效率" 的典型设计思想,是 STL 中极具代表性的序列式容器之一。


目录

  • [一、list 容器概述](#一、list 容器概述)
  • 二、底层数据结构:双向循环链表
    • [2.1 双向循环链表结构](#2.1 双向循环链表结构)
    • [2.2 哨兵节点(头节点)](#2.2 哨兵节点(头节点))
  • 三、节点结构实现
    • [3.1 节点结构定义](#3.1 节点结构定义)
    • [3.2 默认参数细节](#3.2 默认参数细节)
    • [3.3 类型撞碰问题](#3.3 类型撞碰问题)
  • 四、迭代器实现
    • [4.1 迭代器设计原理](#4.1 迭代器设计原理)
    • [4.2 迭代器模板定义](#4.2 迭代器模板定义)
    • [4.3 运算符重载实现](#4.3 运算符重载实现)
  • [五、list 容器类的实现](#五、list 容器类的实现)
    • [5.1 容器类结构](#5.1 容器类结构)
    • [5.2 创建头节点(空链表初始化)](#5.2 创建头节点(空链表初始化))
    • [5.3 默认构造函数](#5.3 默认构造函数)
    • [5.4 拷贝构造函数](#5.4 拷贝构造函数)
    • [5.5 赋值运算符重载](#5.5 赋值运算符重载)
    • [5.6 析构函数](#5.6 析构函数)
    • [5.7 清空操作](#5.7 清空操作)
  • 六、常用操作接口与实现
    • [6.2 insert 插入操作](#6.2 insert 插入操作)
    • [6.3 erase 删除操作](#6.3 erase 删除操作)
    • [6.4 push_back 尾插操作](#6.4 push_back 尾插操作)
    • [6.5 push_front 头插操作](#6.5 push_front 头插操作)
    • [6.6 pop_back 尾删操作](#6.6 pop_back 尾删操作)
    • [6.7 pop_front 头删操作](#6.7 pop_front 头删操作)
  • 七、常用操作与使用方法
    • [7.2 添加元素](#7.2 添加元素)
    • [7.3 删除元素](#7.3 删除元素)
    • [7.4 遍历操作](#7.4 遍历操作)
    • [7.5 splice 操作(list 独有特性)](#7.5 splice 操作(list 独有特性))
    • [7.6 其他常用操作](#7.6 其他常用操作)
  • 八、迭代器失效详细分析
    • [8.2 常见错误示例](#8.2 常见错误示例)
    • [8.3 splice 与迭代器](#8.3 splice 与迭代器)
  • 九、异常安全性
    • [9.2 需要注意的场景](#9.2 需要注意的场景)
  • 十、与其他容器的对比
  • 十一、常见错误与最佳实践
    • [11.1 访问空容器](#11.1 访问空容器)
    • [11.2 使用已失效的迭代器](#11.2 使用已失效的迭代器)
    • [11.3 修改容器时遍历](#11.3 修改容器时遍历)
    • [11.4 迭代器算术运算](#11.4 迭代器算术运算)
  • 十二、核心要点总结

一、list 容器概述

std::list 是 C++ 标准模板库(STL)中的一种双向链表容器 ,它允许在常数时间内对任意位置进行元素的插入和删除操作。与 std::vector 等连续存储的容器不同,list 中的元素存储在互相独立的节点中,通过指针连接形成链式结构。

list 容器的核心特点包括: 双向迭代、任意位置 O(1) 插入删除、但是不支持随机访问、节点动态分配等。

二、底层数据结构:双向循环链表

2.1 双向循环链表结构

list 的底层采用双向循环链表实现。每个节点包含三个核心成员:

1. 数据域(存储元素值)
2. 前驱指针(指向前一个节点)
3. 后继指针(指向后一个节点)

循环结构意味着链表的尾节点指向头节点,头节点的前驱指向尾节点,形成一个闭环。

这种结构使得 list 在任意位置进行插入和删除时,只需要调整相邻节点的两个指针即可,无需像 vector 那样移动大量元素。

2.2 哨兵节点(头节点)

为了简化边界处理,list 实现中通常会引入一个哨兵节点 (也称为头节点)。这是一个不存储实际数据的节点,仅作为链表的起点和终点。通过哨兵节点,可以统一处理空链表和非空链表的边界情况,无需特殊判断。

关键细节: 哨兵节点的存在带来了一个重要特性------end() 返回的迭代器实际上指向哨兵节点,而非最后一个数据节点。​ 这使得链表即使为空也能保持一致的迭代器行为。

其中,哨兵节点作为链表的首尾标记,实际数据节点从哨兵节点的 next 开始,到哨兵节点的 prev 结束。这种循环结构确保了无论是从头还是从尾遍历,都能方便地找到另一个端点。

三、节点结构实现

3.1 节点结构定义

list 节点的核心结构通常定义如下:

cpp 复制代码
template <typename T>
struct list_node 
{
    // 数据域:存储元素值
    T _data;
    
    // 前驱指针:指向当前节点的前一个节点
    list_node<T>* _prev;
    
    // 后继指针:指向当前节点的后一个节点
    list_node<T>* _next;
    
    // 默认构造函数
    list_node(const T& data = T(), list_node* prev = nullptr, list_node* next = nullptr)
        : _data(data)
        , _prev(prev)
        , _next(next)
    {}
};

其中,前驱指针后继指针用于维护链表的双向连接关系。数据域 _data 用于存储实际的元素值。这种结构使得每个节点能够知道它的前一个节点和后一个节点在哪里,从而实现双向遍历。

3.2 默认参数细节

节点的构造函数有三个参数:data 用于初始化数据域, prevnext 用于初始化前后指针。如果不传入参数,则使用默认值:data 调用类型 T 的默认构造函数,prevnext 为空指针。

重要细节: 默认参数 T() 对于不同类型有不同的含义。​

  • 对于内置类型(如 int),默认值 T() 即为 0
  • 对于指针类型,默认值是 nullptr
  • 对于自定义类型,会调用其默认构造函数。这确保了节点可以在不提供任何参数的情况下正确初始化。

3.3 类型撞碰问题

当节点中存储的是包含指针成员的结构体时,需要特别注意。节点的拷贝和赋值可能会导致指针成员被错误复制,从而引发悬垂指针问题。因此,如果 T 类型包含指针成员,需要确保正确处理拷贝语义。

四、迭代器实现

4.1 迭代器设计原理

由于 list 的元素不是连续存储的,普通指针无法满足迭代器的要求。list 的迭代器实际上是对节点指针的封装,通过重载运算符(如 ++--*->)来实现类似指针的操作。

关键细节: list 的迭代器是 bidirectional iterator,不支持随机访问。 这意味着迭代器不能使用 it + 5 或 itn 这样的操作。任何需要跳转的算法都必须通过逐步移动迭代器来实现,时间复杂度为 O(n)。

迭代器需要支持以下操作: 解引用(*->)访问数据、递增递减(++--)移动到下一个或上一个节点、相等性比较(==!=)判断是否指向同一位置。

4.2 迭代器模板定义

cpp 复制代码
template <class T, class Ref, class Ptr>
struct list_iterator 
{
    // 将 list_node 结构体类型重命名为 Node
    typedef list_node<T> Node;
    
    // 将 list_iterator 结构体类型重命名为 Self
    typedef list_iterator<T, Ref, Ptr> Self;
    
    // 核心成员变量:指向节点的指针
    Node* _pnode;
    
    // 默认构造函数
    list_iterator(Node* pnode = nullptr)
        : _pnode(pnode)
    {}
    
    // 拷贝构造函数(允许从普通迭代器构造 const 迭代器)
    list_iterator(const list_iterator& it)
        : _pnode(it._pnode)
    {}
};

迭代器通过三个模板参数实现通用设计:

参数 说明 普通迭代器
T 表示容器中存储的实际元素类型 int
Ref 解引用运算符 * 的返回类型 T&
Ptr 箭头运算符 -> 的返回类型 T*

重要细节: 仅用一个类模板,就能同时生成普通迭代器和常量迭代器,从而避免代码重复。​ 普通迭代器允许修改元素,常量迭代器只允许读取。这种设计利用了 C++ 的模板参数推导,使得同一份代码可以服务于两种迭代器类型。

4.3 运算符重载实现

  • 解引用运算符 *:返回当前节点数据的引用
cpp 复制代码
Ref operator*() 
{
    return _pnode->_data;
}

当使用 *it 时,返回节点中存储的数据的引用。如果迭代器是普通的,引用可用于修改数据;如果迭代器是 const 的,则只能读取数据。这种设计使得迭代器能够像指针一样使用,同时提供类型安全。

  • 箭头运算符 ->:返回当前节点数据的地址
cpp 复制代码
Ptr operator->() 
{
    return &(_pnode->_data);
}

C++ 为 -> 运算符提供了"隐式递归调用"机制。当使用 it->member 时,编译器首先调用 it.operator->() 得到一个指针,然后自动在这个指针上再调用一次箭头操作。

如果链表中存储的是结构体,可以通过迭代器直接访问其成员:it->name 实际转换为 (it.operator->())->name。

  • 前置递增运算符 ++:返回修改后的引用,效率更高
cpp 复制代码
Self operator++() 
{    
	_pnode = _pnode->_next;  
	return *this;
}

前置递增先将指针移动到下一个节点,然后返回当前迭代器的引用。这种方式避免了创建临时对象,效率高于后置版本。使用 Self& 返回可以实现链式调用,如 ++it1; ++it2;。

  • 后置递增运算符 ++:返回原值的拷贝
cpp 复制代码
Self& operator++(int) 
{    
	Self tmp = *this;
    ++(*this);
    return tmp;
}

后置递增需要创建一个临时对象保存当前状态,然后递增,最后返回原始值的拷贝。参数 int 是后置版本的标记,编译器通过它区分前置和后置版本。

  • 前置递减运算符 --:返回修改后的引用
cpp 复制代码
Self operator--() 
{
	Self tmp = *this;    
	--(*this);    
	return tmp;
}

递减操作将指针移动到前一个节点。对于双向链表,节点的 _prev 指针指向前驱节点,因此递减操作通过 _prev 指针实现。

  • 后置递减运算符 --:返回原值的拷贝
cpp 复制代码
Self operator--(int) 
{    
	Self tmp = *this;
    ++(*this);
    return tmp;
}

与后置递增类似,需要保存原始状态后再递减。

  • 相等比较运算符 ==:判断两个迭代器是否指向同一节点
cpp 复制代码
bool operator==(const Self& s) 
{    
	return _pnode == s._pnode;
}

通过比较内部节点指针来判断两个迭代器是否相等。指向同一节点的迭代器被视为相等。

  • 不等比较运算符 !=:判断两个迭代器是否指向不同节点
cpp 复制代码
bool operator!=(const Self& s) 
{    
	return _pnode != s._pnode;
}

返回与相等比较相反的结果。

五、list 容器类的实现

5.1 容器类结构

cpp 复制代码
template <class T>
class list 
{
public:
	// 将 list_node 结构体类型重命名为 Node
	typedef list_node<T> Node;
	
    // 将 list_iterator 结构体类型重命名为 iterator
	typedef list_iterator<T, T&, T*> iterator;
	
    // 将 list_iterator 结构体类型重命名为 const_iterator    
	typedef list_iterator<T, const T&, const T*> const_iterator;    
	private:    
		// 哨兵位结点指针
		Node* _head;
};

list 容器本身维护一个指向哨兵节点的指针 _head,并提供统一的接口。通过类型别名,可以方便地使用迭代器类型和节点类型。

5.2 创建头节点(空链表初始化)

cpp 复制代码
template <class T>
void list<T>::CreateHead() 
{    
	// 创建哨兵节点    
	_head = new Node;
	
	// 将哨兵节点构造成自循环结构
	_head->_next = _head;    
	_head->_prev = _head;
}

初始化时,首先创建一个哨兵节点,然后将其 _next_prev 指针都指向自己,形成一个循环结构。这意味着即使是空链表,也包含一个哨兵节点。

关键细节: 空链表的判断条件是_head->_next == _head,而不是_head == nullptr。这是因为哨兵节点始终存在,即使链表为空。

5.3 默认构造函数

cpp 复制代码
list()
 {    
 	CreateHead();
 }

创建一个空链表,仅包含哨兵节点。此时 _head->_next_head->_prev 都指向 _head 自身,表示链表为空。

5.4 拷贝构造函数

cpp 复制代码
list(const list& lt) 
{
	CreateHead();
	// 遍历原链表,将每个元素拷贝到新链表中
	for (const auto& elem : lt) 
	{
		push_back(elem);
	}
}

拷贝构造函数首先创建空链表,然后遍历原链表,将每个元素通过 push_back 添加到新链表中。这种实现简单直观,每次尾插都维持了原链表的元素顺序。

重要细节: 拷贝构造函数执行的是深拷贝。​ 每个节点都独立分配内存,节点之间通过各自的指针连接。这种设计确保了两个链表互不影响,任一链表的修改不会反映到另一个链表上。

5.5 赋值运算符重载

cpp 复制代码
template <class T>
void list<T>::swap(list& lt)
{    
	std::swap(_head, lt._head);
}
// 赋值重载函数
list& operator=(list lt)
{    
	swap(lt);    
	return *this;
}

赋值运算符使用拷贝并交换(copy-and-swap)技术。首先传入参数时进行拷贝(pass-by-value),触发拷贝构造函数创建临时对象,然后交换内部指针。函数结束时,临时对象被销毁,原链表内存随之释放。

关键细节: 赋值运算符的参数是按值传递的,这确保了自我赋值的安全性。​ 即使执行 lst = lst,也不会出现问题,因为拷贝会创建一个新的链表,然后交换指针,最后原链表作为临时对象被销毁。

5.6 析构函数

cpp 复制代码
~list() 
{   
	// 释放除哨兵结点以外的结点    
	clear();
	
	// 释放哨兵节点
	delete _head;
	
	// 将哨兵 _head 指针置空    
	_head = nullptr;
}

析构函数首先调用 clear() 释放所有数据节点,然后释放哨兵节点,最后将指针置空以避免悬垂指针。

重要细节: 必须显式释放每个节点的内存。​ 与 Python 的垃圾回收机制不同,C++ 要求开发者显式管理资源。如果忘记释放节点,将导致内存泄漏。此外,节点中的自定义类型析构函数会被自动调用,确保资源正确清理。

5.7 清空操作

cpp 复制代码
template <class T>
void list<T>::clear() 
{    
	auto it = begin();    
	while (it != end()) 
	{        
	// erase 返回的是被删结点的下一个位置,所以迭代器不会失效        
	it = erase(it);    
	}
}

clear() 函数遍历链表,逐个删除所有数据节点。由于 erase 返回下一个节点的迭代器,循环可以正常继续。注意,哨兵节点不在清除范围内。

关键细节: 清空后,链表恢复为空状态,哨兵节点的指针重新指向自身。​ _head->_next == _head && _head->_prev == _head 再次成立。

六、常用操作接口与实现

6.1 begin / end

返回指向第一个元素和尾后位置的迭代器。

cpp 复制代码
iterator begin() 
{    
	// 指向容器中第一个元素的迭代器   
	return iterator(_head->_next);
}
const_iterator begin() const 
{    
	return const_iterator(_head->_next);
}
iterator end() 
{    
	// 指向容器中最后一个元素之后位置的迭代器    
	return iterator(_head);
}
const_iterator end() const 
{    
	return const_iterator(_head);
}

begin() 返回第一个数据节点的迭代器(哨兵节点的 _next),end() 返回哨兵节点本身的迭代器,表示"最后一个元素之后"的位置。这种设计使得范围for循环和标准算法可以正常工作。

关键细节: end() 返回的是哨兵节点的迭代器,而不是最后一个数据节点。​ 这是 STL 的一致性设计,确保所有容器的迭代器行为一致。

6.2 insert 插入操作

在指定迭代器位置前插入新元素。

cpp 复制代码
template <class T>
typename list<T>::iterator list<T>::insert(iterator pos, const T& x)
{
	// 申请一个新结点
	Node* newnode = new Node(x);
	    
	// pos 位置结点指针
	Node* cur = pos._pnode;
	   
	// 建立新节点的前后连接
	newnode->_prev = cur->_prev;
	newnode->_next = cur;
	 
	// 更新相邻节点的指针
	cur->_prev->_next = newnode;
	cur->_prev = newnode;
	
	return iterator(newnode);
}

插入过程需要四个指针调整:首先保存新节点的前驱和后继,然后更新原位置节点前驱节点的 _next,最后更新原位置节点的 _prev。所有操作都在常数时间内完成。

关键细节: 插入操作不会导致任何迭代器失效。​ 这是 list 相比 vector 和 deque 的重要优势。即使其他迭代器指向链表中的任何位置,插入操作都不会使它们失效,因为节点本身的地址没有改变,只是指针连接发生了变化。

6.3 erase 删除操作

删除指定迭代器位置的元素。

cpp 复制代码
template <class T>
typename list<T>::iterator list<T>::erase(iterator pos) 
{
    // 不能删除哨兵结点
    assert(pos._pnode != _head);
    
    // pos 位置的结点指针
    Node* cur = pos._pnode;
    
    // pos 位置前一个位置的结点指针
    Node* prev = cur->_prev;
    
    // pos 位置后一个位置的结点指针
    Node* next = cur->_next;
     
    // 更新相邻节点的指针
    prev->_next = next;
    next->_prev = prev;
    
    // 释放内存
    delete cur;
    
    // 返回下一个位置的迭代器
    return iterator(next);
    }

删除操作同样只需要常数时间的指针调整:首先保存前后节点,然后调整它们的 _next 和 _prev 指针使其互相连接,最后释放被删除节点的内存。返回后继节点的迭代器,供调用者继续使用。

关键细节: 删除操作只会使被删除节点的迭代器失效,其他迭代器不受影响。​ 这是 list 迭代器失效的特点,与 vector 的所有迭代器都可能失效形成对比。erase 返回的迭代器指向被删除节点的下一个节点,可以直接用于继续遍历。

6.4 push_back 尾插操作

cpp 复制代码
template <class T>
void list<T>::push_back(const T& x)
{    
	insert(end(), x);
}

尾插操作复用了 insert,在 end() 位置(即哨兵节点)之前插入元素。哨兵节点的 _prev 指向最后一个数据节点,因此插入会添加到链表末尾。

重要细节: 尾插的时间复杂度是 O(1)。​ 由于存在哨兵节点,可以直接通过 _head->_prev 找到最后一个节点,无需遍历整个链表。

6.5 push_front 头插操作

cpp 复制代码
template <class T>
void list<T>::push_front(const T& x)
 {    
 	insert(begin(), x);
 }

头插操作复用了 insert ,在 begin() 位置(即第一个数据节点)之前插入元素。由于 begin() 返回的是 _head->_next,插入后新节点成为链表的第一个数据节点。

关键细节: 头插的时间复杂度是 O(1)。​ 直接在哨兵节点之后插入,无需遍历。

6.6 pop_back 尾删操作

cpp 复制代码
template <class T>
void list<T>::pop_back()
{    
 	erase(--end());
}

尾删操作复用了 erase,删除哨兵节点的前一个节点,即链表的最后一个数据节点。

重要细节: 删除空链表的尾部元素会导致未定义行为。​ 在调用 pop_back() 前应检查链表是否为空,可以使用 empty() 或 size() 进行判断。

6.7 pop_front 头删操作

cpp 复制代码
template <class T>
void list<T>::pop_front()
{
	erase(begin());
}

头删操作复用了 erase,删除第一个数据节点。

重要细节: 删除空链表的头部元素会导致未定义行为。​ 与 pop_back() 一样,在调用前应进行空值检查。

七、常用操作与使用方法

7.1 创建与初始化

cpp 复制代码
#include <list>
std::list<int> lst1;              // 创建空链表
std::list<int> lst2(5, 100);      // 创建包含5个元素,每个值为100
std::list<int> lst3(lst2);        // 拷贝构造
std::list<int> lst4 = {1, 2, 3};  // 使用初始化列表(C++11)
std::list<int> lst5(lst4.begin(), lst4.end());  // 使用迭代器范围构造

关键细节: 使用迭代器范围构造时,源范围中的元素顺序决定了目标链表中元素的顺序。​ 如果使用输入流迭代器(如 istream_iterator),可以方便地从标准输入构建链表。

7.2 添加元素

cpp 复制代码
lst1.push_front(10);              // 在头部插入
lst1.push_back(20);              // 在尾部插入
lst1.emplace_front(-1);          // 在头部直接构造元素(C++11)
lst1.emplace_back(30);           // 在尾部直接构造元素(C++11)

// 在指定迭代器位置前插入
auto it = lst1.begin();
lst1.insert(it, 15);

// 在尾部直接构造元素(C++11)
lst1.emplace(lst1.end(), 40);

emplace 系列函数是 C++11 引入的,它们直接在需要的位置构造元素,避免了复制或移动操作。对于复杂类型,可以提高性能。

重要细节: emplace 不会触发拷贝或移动构造函数。​ 元素直接在目标位置构造,减少了临时对象的创建开销。对于大型对象或资源管理类(如 std::string、std::vector),这可以显著提升性能。

7.3 删除元素

cpp 复制代码
lst1.pop_front();                // 删除头部元素
lst1.pop_back();                 // 删除尾部元素
lst1.erase(it);                  // 删除迭代器指向的元素
lst1.remove(100);                // 删除所有值为100的元素
lst1.remove_if([](int x) { return x % 2 == 0; });  // 删除满足条件的元素(C++11)
lst1.clear();                    // 清空链表

重要细节: remove() 会删除所有匹配的元素,而非仅删除第一个。​ 这与 std::find + erase 的行为不同,后者通常只删除第一个匹配项。

关键细节: pop_front() 和 pop_back() 对空容器调用是未定义行为。​ 必须先检查容器是否为空。

7.4 遍历操作

cpp 复制代码
// 范围 for 循环(推荐)
for (const auto& elem : lst1) 
{    
	std::cout << elem << " ";
}
// 迭代器遍历
for (auto it = lst1.begin(); it != lst1.end(); ++it) 
{
	std::cout << *it << " ";
}

// 反向遍历(C++11)
for (auto it = lst1.rbegin(); it != lst1.rend(); ++it)
{
	std::cout << *it << " ";
}

关键细节: 使用范围 for 循环时,循环变量不应在循环体内修改容器结构(如插入或删除元素),否则可能导致未定义行为。​ 如果需要修改元素,应使用普通引用而非常引用:for (auto& elem : lst)

7.5 splice 操作(list 独有特性)

splice 操作是 list 独有的特性,可以在常数时间内将另一个链表的节点移动到当前链表,无需复制或分配内存。

cpp 复制代码
lst1.splice(it, lst2);           // 将 lst2 的所有节点移动到 lst1 的 it 位置
lst1.splice(it, lst2, it2);     // 将 lst2 中 it2 位置的节点移动到 lst1
lst1.splice(it, lst2, first, last);  // 将 lst2 中 [first, last) 范围的节点移动

重要细节: splice 操作后,原链表中的元素不再存在。​ 这是"剪切"而非"复制"操作。移动的是节点本身,而非节点中的数据。

关键细节: splice 操作不会使迭代器失效。​ 被移动的迭代器仍然有效,只是现在属于不同的容器。这是 list 迭代器设计的一个重要特性。

7.6 其他常用操作

cpp 复制代码
lst1.size();                     // 获取元素个数(C++11)
lst1.empty();                    // 判断是否为空
lst1.front();                    // 获取头部元素(未定义行为:空容器)
lst1.back();                     // 获取尾部元素(未定义行为:空容器)
lst1.reverse();                  // 反转链表
lst1.sort();                     // 排序(使用归并排序,O(n log n))
lst1.unique();                   // 去重(需配合 sort 使用)
lst1.merge(lst2);                // 合并两个已排序的链表

关键细节: 访问空容器的 front() 或 back() 是未定义行为。​ 在访问前必须确保容器非空。

重要细节: list 的 sort() 成员函数效率高于 std::sort()。​ 原因在于 list 可以直接操作节点指针进行归并排序,避免了迭代器随机访问的限制。std::sort 要求随机访问迭代器,因此不能直接用于 list。

八、迭代器失效详细分析

8.1 list 迭代器失效特点

与其他容器相比,list 的迭代器失效规则相对简单和宽松:

  • 插入操作不会导致任何迭代器失效。 由于节点是动态分配且地址不变的,插入新节点只改变指针连接,不影响现有节点的位置。
  • 删除操作只会使被删除节点的迭代器失效,其他迭代器保持有效。 这是因为删除操作改变了被删除节点的邻居节点的指针,但不改变其他节点的地址。

8.2 常见错误示例

cpp 复制代码
// 错误示例:删除所有偶数
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
while (it != lst.end()) 
{    
	if (*it % 2 == 0) 
	{        
		lst.erase(it);  // erase 后 it 失效,不能继续使用    		
	}    
		++it;  // 未定义行为:使用已失效的迭代器
}

正确做法有两种:

cpp 复制代码
// 方法一:使用 erase 的返回值更新迭代器
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
while (it != lst.end()) 
{    
	if (*it % 2 == 0) 
	{        
		it = lst.erase(it);  // erase 返回下一个有效的迭代器    
	} 
	else 
	{        
		++it;    
	}
}
// 方法二:使用 remove 或 remove_if
lst.remove_if([](int x) { return x % 2 == 0; });

8.3 splice 与迭代器

cpp 复制代码
std::list<int> lst1 = {1, 2, 3}
std::list<int> lst2 = {4, 5, 6};
auto it = lst2.begin();  // 指向 lst2 的 4

lst1.splice(lst1.end(), lst2, it);  // 将 4 移动到 lst1 末尾

// it 仍然有效,现在指向 lst1 中的 4
// lst2 现在只有 {5, 6}

关键细节: splice 操作不会使迭代器失效,被移动的迭代器仍然有效。​ 这是 list 相对于其他容器的独特优势。

九、异常安全性

9.1 list 的异常安全保证

list 的设计遵循了 C++ 的异常安全原则。大多数操作提供了以下异常安全保证:

  • 插入操作要么完全成功,要么完全失败。 如果内存分配失败(如 std::bad_alloc),容器状态保持不变。
  • 删除操作几乎不会失败。 删除节点只需要调整指针和释放内存,不会触发复杂的操作。

9.2 需要注意的场景

cpp 复制代码
// 如果 T 的拷贝构造函数或移动构造函数抛出异常
lst.insert(pos, elem);  // 可能导致部分元素被插入,但整体操作失败

// 使用 emplace 可以减少异常风险
lst.emplace(pos, args...);  // 直接构造,避免拷贝/移动

重要细节: 在需要强异常安全保证的场景中,应优先使用 emplace 系列函数。​ 它们直接在目标位置构造元素,减少了可能抛出异常的中间步骤。

十、与其他容器的对比

特性 list vector duque
底层结构 双向循环链表 动态数组 块状链表
随机访问 O(n) 不支持 O(1) O(1)
头部插入/删除 O(1) O(n) O(1)
尾部插入/删除 O(1) O(1) 均摊 O(1)
中间插入/删除 O(1) O(n) O(n)
迭代器失效 仅当前元素 插入后全部失效 插入后部分失效
内存占用 每个节点额外两个指针 连续紧凑 中等
数据局部性 中等
splice 支持

list 的最大优势在于任意位置的插入和删除操作时间复杂度为 O(1),这使得它非常适合频繁进行元素增删的场景。同时,list 还提供了 splice 操作,可以在常数时间内将一个链表的全部或部分节点移动到另一个链表,这是其他容器无法提供的功能。

十一、常见错误与最佳实践

11.1 访问空容器

cpp 复制代码
std::list<int> lst;  // 空链表

// 错误:未定义行为
std::cout << lst.front() << std::endl;

// 正确做法
if (!lst.empty()) 
{    
	std::cout << lst.front() << std::endl;
}

11.2 使用已失效的迭代器

cpp 复制代码
std::list<int> lst = {1, 2, 3};
auto it = lst.begin();
// 错误:erase 后 it 失效
lst.erase(it);std::cout << *it << std::endl;  // 未定义行为

// 正确做法
it = lst.erase(it);  // 更新迭代器

11.3 修改容器时遍历

cpp 复制代码
std::list<int> lst = {1, 2, 3};

// 错误:范围 for 中修改容器可能导致未定义行为
for (auto x : lst)
{
	if (x == 2) 
	{
		lst.push_back(100);  // 未定义行为
	}
}
// 正确做法:使用迭代器手动控制
for (auto it = lst.begin(); it != lst.end();)
{    
	if (*it == 2)
	{
		it = lst.insert(it, 100);
		++it;
	}
	else
	{
		++it;
	}
}

11.4 迭代器算术运算

cpp 复制代码
std::list<int> lst = {1, 2, 3, 4, 5};

// 错误:list 迭代器不支持随机访问
auto it = lst.begin();std::advance(it, 3);  // 可以,但 O(n) 复杂度
// 错误:完全不支持
auto it2 = it + 1;  // 编译错误

11.5 内存管理

cpp 复制代码
// 包含指针成员的类需要特别注意拷贝语义
struct Node
{
int* data;
Node(int val) 
	: data(new int(val)) 
	{}
~Node() { delete data; }
Node(const Node& other)
	: data(new int(*other.data)) 
	{}  // 深拷贝    Node& operator=(const Node& other) {        if (this != &other) {            delete data;            data = new int(*other.data);        }        return *this;    }};

重要细节: 如果 list 存储的是包含动态分配内存的类型,必须正确实现拷贝构造函数和赋值运算符,否则会导致悬垂指针或双重释放。​

十二、核心要点总结

  • 底层结构: 双向循环链表 + 哨兵节点,每个节点包含数据域和前后指针,O(1) 插入删除。
  • 迭代器: 双向迭代器,不支持随机访问。通过运算符重载实现遍历,重载 -> 运算符具有"隐式递归调用"机制。
  • 迭代器失效: 插入不失效任何迭代器,删除仅失效被删节点迭代器。
  • 常用操作: push_front/back、pop_front/back、insert、erase、splice(list 独有)、sort(成员函数版更高效)。
  • 异常安全:​ 插入失败容器状态不变,删除几乎不会失败,优先使用 emplace。
  • 适用场景: 频繁插入删除、需要 splice 操作、迭代器稳定性要求高的场景。
  • 常见错误: 访问空容器 front()/back()、使用已失效迭代器、在遍历中修改容器结构。

C++ STL list 是以双向循环链表为基础实现的容器,通过哨兵节点统一处理边界,其迭代器封装节点指针并借助运算符重载实现类似指针的操作。list 的核心优势在于任意位置 O(1) 的插入删除、插入不导致任何迭代器失效的特性,以及独有的 splice 成员函数可以直接移动节点。但它不支持随机访问,每个节点需额外存储两个指针,内存不连续导致数据局部性差。实际开发中应根据是否需要频繁增删、是否依赖随机访问等场景合理选择 list、vector 或其他容器。

相关推荐
RSTJ_16251 小时前
PYTHON+AI LLM DAY SIXTY-SIX
服务器·开发语言·python
Chase_______1 小时前
【Java基础 | 11】异常处理进阶:throw、throws、自定义异常与异常链讲清楚
java·开发语言·python
BirdenT1 小时前
20260604紫题训练
c++·算法
tg:;1 小时前
Catkin 常用命令
开发语言·c++·算法
Cx330❀1 小时前
【Linux网络】一文吃透 TCP Socket 编程
linux·运维·服务器·开发语言·网络·tcp/ip
暖阳华笺1 小时前
【高频考点】回溯(暴力搜索)
数据结构·c++·算法·回溯法
hunterkkk(c++)1 小时前
学习dijkstra算法(c++)
c++·学习·算法
wb043072012 小时前
外卖大战——从阿明的“3 秒生死线“,看系统性能优化的全链路方法论
开发语言·性能优化·架构·php
小的~~2 小时前
Java线程及线程池的相关的问题
java·开发语言·多线程