《STL list容器终极解析:底层实现、迭代器失效与高性能应用(下)》

前言:在 C++ 中,list 是基于双向链表实现的容器,其迭代器失效情况相对简单,主要与删除操作相关,插入操作一般不会导致迭代器失效 。

目录

一、迭代器失效问题

二、反向迭代器实现问题

三、vector和list的选择问题

拓展:list迭代器的模板参数为什么是三个?


一、迭代器失效问题

前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

1. 删除操作(erase)

当使用 list 的 erase 成员函数删除元素时,仅指向被删除节点的迭代器会失效,其他迭代器不受影响。

核心原理:list 的底层是双向链表,节点通过指针连接。

删除一个节点时,只会断开该节点与前后节点的链接,其他节点的位置和指针不受影响。
因此,只有指向被删除节点的迭代器会因为所指节点被销毁而失效,指向其他节点的迭代器仍能正常使用。

 list<int> mylist = {1, 2, 3, 4};

 auto it = mylist.begin(); // it 指向 1

 mylist.erase(it); // 删除 1,it 失效
 // 此时,指向 2、3、4 的迭代器仍有效
 auto it2 = mylist.begin(); // it2 指向 2,可正常使用

2. 插入操作(insert、push_front、push_back 等)

由于 list 是链表结构,插入新节点时,只需调整相邻节点的指针,不会移动其他节点的位置

因此,插入操作不会导致任何迭代器失效(包括指向插入位置附近节点的迭代器)

list<int> mylist = {1, 3, 4};
auto it = mylist.begin();

++it; // it 指向 3

// 在 3 前插入 2,it 仍指向 3(节点 3 未被移动,指针未变)
mylist.insert(it, 2); 
// 遍历结果:1 2 3 4,所有迭代器(包括原来指向 3 的 it)都有效

3. 清空操作 (clear)

clear 会删除 list 中所有元素,所有指向该 list 元素的迭代器都会失效(因为没有元素可指向了)调用 clear 后,若再使用之前的迭代器,会导致未定义行为
 

4. 赋值 / 交换操作 (assign、swap 等)

assign:会替换 list 的内容,原 list 所有元素被删除,原迭代器全部失效,需重新获取新 list 的迭代器。
 

swap:交换两个 list 的内容后,原 list 的迭代器会指向另一个 list 的元素(逻辑上 “转移” 了指向),若原 list 被销毁或内容改变,需注意迭代器的有效性。

#include <iostream>
#include <list>
using namespace std;

// 错误示例:迭代器失效问题
void TestListIterator01()
{
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> lt(array, array + sizeof(array) / sizeof(array[0]));

    auto it = lt.begin();
    while (it != lt.end())
    {

        lt.erase(it);
        ++it; //未定义行为:对失效迭代器进行递增操作

        //错误分析:
        // erase会删除it指向的节点,并使it失效
        // 失效的迭代器不能再进行++操作
    }
}

// 正确示例:处理迭代器失效
void TestListIterator02()
{
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> lt(array, array + sizeof(array) / sizeof(array[0]));

    auto it = lt.begin();
    while (it != lt.end())
    {
        /*-------方法1:利用后置++的特性-------*/
 
        //lt.erase(it++);

        //正确分析:
        // it++ 返回旧值(当前有效迭代器)
        // erase删除旧值指向的节点
        // it 被递增为下一个有效节点

        /*-------方法2:显式接收erase返回的迭代器-------*/

        //it = lt.erase(it); // erase返回下一个有效迭代器
    }
}


int main()
{
    //TestListIterator01();
    TestListIterator02();


    return 0;
}

二、反向迭代器实现问题

通过前面的例子可知:

反向迭代器的 ++ 操作,对应正向迭代器的 -- 操作
反向迭代器的 -- 操作,对应正向迭代器的 ++ 操作

基于此,反向迭代器的实现可借助正向迭代器来完成,具体来说:

反向迭代器内部可包含一个正向迭代器,通过对该正向迭代器的接口进行封装、调整逻辑,就能实现反向迭代器的功能 。

//反向迭代器适配器:将正向迭代器转换为反向迭代器
template<class Iterator>
class ReverseListIterator
{
private:
    Iterator _it;  //底层维护的正向迭代器

public:
    /*--------------------------第一部分:定义类型别名--------------------------*/
    
    //1.重命名“list正向迭代器中的引用类型”:Iterator::Ref ---> Ref
    typedef typename Iterator::Ref Ref;

    //2.重命名“list正向迭代器的指针类型”:Iterator::Ptr ---> Ptr
    typedef typename Iterator::Ptr Ptr;

    //3.重命名“list反向迭代器”的类型:ReverseListIterator ---> Self
    typedef ReverseListIterator<Iterator> Self;

    //注意:
    //1.此处typename的作用是明确告诉编译器,Ref是“Iterator类中的类型”,而不是“静态成员变量”
    //2.否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量
    //3.因为静态成员变量也是按照 “类名::静态成员变量名” 的方式访问的

    /*--------------------------第二部分:定义成员变量--------------------------*/

    //1.实现:“有参数构造函数”
    ReverseListIterator(Iterator it) 
        : _it(it)  //用正向迭代器初始化反向迭代器
    {}

    //2.实现:“*运算符的重载函数”
    Ref operator*()  //注意:返回当前位置前一个元素的引用(反向迭代器的特性)
    {
        //1.创建临时迭代器
        Iterator temp(_it);

        //2.前移一位(注意:这是正向迭代器的--)
        --temp;  

        //3.返回临时迭代器的解引用
        return *temp; 
    }

    //3.实现:“->运算符的重载函数”
    Ptr operator->()  //注意:返回当前位置前一个元素的指针
    { 
        return &(operator*());  //复用解引用运算符,取地址
    }


    //4.实现:“前置++运算符的重载函数”
    Self& operator++()  //(反向迭代器递增 = 正向迭代器递减)
    {
        --_it;   //注意:调整底层正向迭代器向前移动
        return *this;
    }

    //5.实现:“前置--运算符的重载函数”
    Self& operator--()  //(反向迭代器递减 = 正向迭代器递增)
    {
        ++_it;   //注意:调整底层正向迭代器向后移动
        return *this;
    }

    //6.实现:“后置++运算符的重载函数”
    Self operator++(int)
    {
        Self temp(*this);

        --_it;  //注意:(底层正向迭代器递减)
        return temp;
    }

    //7.实现:“后置--运算符的重载函数”
    Self operator--(int) 
    {
        Self temp(*this);

        ++_it;  //注意:(底层正向迭代器递增)
        return temp;
    }

    //8.实现:“==运算符的重载函数”
    bool operator==(const Self& lt) const
    {
        return _it == lt._it;  //比较底层正向迭代器
    }


    //8.实现:“!=运算符的重载函数”
    bool operator!=(const Self& lt) const 
    { 
        return _it != lt._it;  //比较底层正向迭代器
    }
};

三、vector和list的选择问题

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

拓展:list迭代器的模板参数为什么是三个?

template <class T, class Ref, class Ptr>
struct list_iterator
{
	/*--------------------定义类型别名--------------------*/

	//1.重命名“list节点”的类型:list_node<T> ---> Node
	typedef list_node<T> Node;

	//2.重命名“list迭代器”的类型:list_iterator<T,Ref,Ptr> ---> Self
	typedef list_iterator<T, Ref, Ptr> Self;

	/*--------------------定义成员变量--------------------*/

	Node* _node; //迭代器内部存储的节点指针,指向当前位置


	/*--------------------定义成员函数--------------------*/

	//1.实现:“有参构造函数”
	list_iterator(Node* node) :
		_node(node)
	{}
}

在 C++ 标准库中,list容器的迭代器模板参数设计为三个(通常是:T, Ref, Ptr),主要是为了支持 常量迭代器非常量迭代器 的区分。

这种设计允许通过模板参数的不同组合,让同一套迭代器代码同时服务于普通迭代器和常量迭代器,避免代码冗余。

为了分离值类型、引用类型和指针类型

普通迭代器:需要返回T&和T*(可修改元素)
常量迭代器:需要返回const T&和const T*(不可修改元素)

通过三个模板参数,可以灵活指定引用和指针的常量性:

Ref = T&,Ptr = T* 时,是普通迭代器。
Ref = const T&,Ptr = const T* 时,是常量迭代器

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值