C++ list容器模拟实现:迭代器、构造与STL风格编程

目录

  • 前言
  • 一、list的模拟实现
    • [1.1 list基本结构](#1.1 list基本结构)
    • [1.2 构造 + 尾插](#1.2 构造 + 尾插)
    • [1.3 迭代器 + 范围for](#1.3 迭代器 + 范围for)
      • [1.3.1 迭代器结构的优势](#1.3.1 迭代器结构的优势)
      • [1.3.2 范围for](#1.3.2 范围for)
    • [1.4 insert + erase](#1.4 insert + erase)
    • [1.5 头插 + 尾插 + 尾删 + 头删 + size](#1.5 头插 + 尾插 + 尾删 + 头删 + size)
    • [1.6 析构 + clear](#1.6 析构 + clear)
    • [1.7 多种构造](#1.7 多种构造)
      • [1.7.1 { }初始化](#1.7.1 { }初始化)
      • [1.7.2 迭代器区间构造](#1.7.2 迭代器区间构造)
      • [1.7.3 n个value的构造](#1.7.3 n个value的构造)
      • [1.7.4 拷贝构造 + 赋值构造](#1.7.4 拷贝构造 + 赋值构造)
        • [1.7.4.1 传统写法](#1.7.4.1 传统写法)
        • [1.7.4.2 现代写法](#1.7.4.2 现代写法)
    • [1.8 模板类的简化写法](#1.8 模板类的简化写法)
    • [1.9 迭代器成员的补充](#1.9 迭代器成员的补充)
    • [1.10 const 迭代器](#1.10 const 迭代器)
      • [1.10.1 传统实现](#1.10.1 传统实现)
      • [1.10.2 现代实现](#1.10.2 现代实现)
    • [1.11 迭代器operator*/operator->重载与./->运算符](#1.11 迭代器operator*/operator->重载与./->运算符)
  • 二、完整源码
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、list的模拟实现

1.1 list基本结构

list的结构是个带头双向循环链表,每个数据是存储在一个单独的节点内,这个节点除了存储数据还有两个指针分别指向前一个和后一个节点

这里定义节点的类用struct,定义list的类用class的原因是一个默认的共识,一个类如果它的所有成员都不期望用访问限定符限制的时候,习惯上就用struct定义,这里的list_node通常作为链表的一个子结构,是存储每个数据的一个最小单元,链表内是要大量访问内部数据的,所以这里不用访问限定符限制。虽然这样写后别人就可以随便访问这些结构了,但是有迭代器之后,在外层的角度是看不到节点的(例如从使用的角度,在接口使用的时候是不用链表的节点的,无论是访问修改还是插入删除,都是直接用迭代器的),即虽然list_node是公有的,但是其是一种隐形的封装,平时看不到也不会/不需要访问

这里 list_node< T >* _prev; 这个写法是C++ 模板语法 + 指针语法的组合

  1. list_node< T >是语法规定:
    list_node是定义的模板结构体,C++ 语法要求:使用模板类 / 结构体时,必须通过<模板参数>指定具体的泛型类型(这里T是模板参数),所以list_node< T >是模板结构体的 "实例化类型写法",属于语法强制要求。
  2. *是语法规定:
    *是 C++ 中 "指针类型" 的声明符号,list_node< T >*表示 "指向list_node< T >类型对象的指针",这是指针的标准语法。

补充一下这里模板声明的作用范围

模板声明的作用范围

template< class T > 是 "模板参数声明",它的作用域仅限于紧跟在它后面的那个类(或结构体、函数)。例如:

cpp 复制代码
// 第一个类模板:list_node
template<class T>  // 作用范围:下面的 struct list_node
struct list_node { ... };

// 第二个类模板:list
template<class T>  // 作用范围:下面的 class list
class list { ... };

这里的两个 template< class T > 是独立的,分别服务于 list_node 和 list 两个类,彼此不影响。

新类模板需要重新声明

每个类模板都是独立的实体,即使两个类的功能相关(比如链表的节点和链表本身),定义新的类模板时也必须重新写template< class T >。

原因是:模板参数 T 是 "当前类模板的局部参数",只在当前类的范围内有效。比如 list_node 中的 T 和 list 中的 T 虽然名字相同,但实际上是两个独立的模板参数(只是习惯上用相同的字母表示)。

特殊情况:嵌套类模板

如果一个类模板内部嵌套了另一个类,嵌套类可以直接使用外部类的模板参数,无需重复声明:

cpp 复制代码
template<class T>
class outer {
    // 嵌套类,直接使用外部的 T
    class inner {
        T data;  // 这里的 T 继承自 outer 的 template<class T>
    };
};

但如果嵌套类需要自己的独立模板参数(比如同时支持 T 和 U),则需要单独声明:

cpp 复制代码
template<class T>
class outer {
    // 嵌套类有自己的模板参数 U
    template<class U>  // 单独声明,作用范围:下面的 inner
    class inner {
        T a;  // 来自 outer 的 T
        U b;  // 来自 inner 自己的 U
    };
};

1.2 构造 + 尾插

尾插逻辑如图

这里运行不了,运行不了的原因是

list_node 缺少无参构造函数

在 List.h 中,list_node 结构体的构造函数只有带参数的版本(list_node(const T& x)),但没有无参构造函数。

而在 list 类的构造函数中,执行了 _head = new Node; ------ 这里尝试调用 list_node 的无参构造函数,但该构造函数并不存在,因此会触发编译错误。

这里有三种解决办法:
方法 1:利用list_node的带参构造显式传入默认值

在 list 类的构造函数中,创建头结点时,显式调用 list_node 的带参构造,并传入 T 类型的默认值(通过 T() 触发 T 的默认构造,也就是使用匿名对象,T有可能是任意类型)。这样无需为 list_node 新增无参构造函数。

cpp 复制代码
template<class T>
class list
{
public:
    list()
    {
        // 调用list_node的带参构造,传入T的默认值
        _head = new Node(T()); 
        _head->_prev = _head;
        _head->_next = _head;
    }
    // 其余成员保持不变...
};

方法2:给 list_node 添加无参构造函数,让头结点的创建合法

cpp 复制代码
template<class T>
struct list_node
{
    list_node<T>* _prev;
    list_node<T>* _next;
    T _data;

    // 无参构造(用于创建头结点)
    list_node() : _prev(nullptr), _next(nullptr), _data() {}
    // 带参构造(用于创建数据节点)
    list_node(const T& x) : _prev(nullptr), _next(nullptr), _data(x) {}
};

方法3:使用委托构造函数(C++11 及以后)

让带参数的构造函数也能处理无参数的情况,通过委托给自己并提供一个默认值。

cpp 复制代码
template<class T>
struct list_node
{
    list_node<T>* _prev;
    list_node<T>* _next;
    T _data;

    // 带参数的构造函数,并提供一个默认参数 T()
    list_node(const T& x = T())
        : _prev(nullptr)
        , _next(nullptr)
        , _data(x)
    {}
};

这里依旧是用匿名对象做缺省参数

1.3 迭代器 + 范围for

在string和vector都是使用原生指针T* 做迭代器(这是容器结构导致原生指针符合迭代器效果,可以说是天生丽质),迭代器对象解引用就是迭代器指向的数据,++就是下一个位置

而list就不能把指向节点的指针(Node* )定义为迭代器了,若是这样做,用迭代器定义一个对象,该对象指向整个节点,而不是节点内对应的数据,且list 是「双向循环链表」,它的节点在内存中是 分散存储 的(每个节点通过 new 分配在堆上,地址不连续),节点之间仅通过 _prev 和 _next 指针关联,原生指针 ++ 是地址偏移,找不到链表的下一个节点

所以迭代器也许是指针,也不一定是,迭代器是一种封装,底层有可能是数组有可能是链表,是用类似的方式去访问数据,修改数据

链表的迭代器需要用类去封装(库中的迭代器实现也是如此),类有一个巨大的特点,其可以自己实现运算符重载去控制具体的行为,利用这个特点重载解引用* ,让其解引用可以直接访问当前节点的数据。重载加加/减减两种运算符让其可以指向下一个/上一个节点(让当前节点的地址变为下一个节点的地址),可以说是逆天改命了

这里将迭代器实现为struct就是公有的,实现为class是私有的不方便访问数据。例如迭代器需要让外部代码能够访问它的成员函数(如 operator* 、operator++ 等)。

补充几点:

public: using iterator = list_iterator< T >;为什么是公有 (public)?

  • iterator 是 list 类对外提供的公共接口的一部分。
  • 目的是让 list 的使用者(客户端代码)能够创建和使用迭代器来遍历、访问或修改 list 中的元素。
  • 如果你不把它设为 public,用户就无法在类的外部声明迭代器变量,例如:
cpp 复制代码
yunze::list<int> mylist;
// 如果 iterator 是 private,下面这行代码会编译失败,因为外部无法访问 private 成员
yunze::list<int>::iterator it = mylist.begin(); 
  • begin() 和 end() 等成员函数返回的也是 iterator 类型。为了让用户能接收这个返回值,iterator 的类型定义必须是公有的。

再补充一下:

在这段代码中,list 类的 begin() 和 end() 是成员函数,它们的核心作用是创建并返回 list_iterator< T > 类的对象(即迭代器)。

  • list_iterator< T > 是专门封装了链表节点指针的类,实现了迭代器所需的操作(如 operator* 、operator++、operator!= 等),负责 "遍历节点" 的行为。
  • 而 list 类的 begin() 和 end() 是对外提供迭代器的接口:
    • begin() 返回指向 "第一个有效节点" 的 list_iterator 对象;
    • end() 返回指向 "哨兵节点(头节点)" 的 list_iterator 对象,标记遍历的结束位置。

因此,begin() 和 end() 本质上是 list 类中用于创建并返回 "迭代器类(list_iterator)实例" 的成员函数------ 通过调用它们,外部代码可以获取到封装了节点遍历逻辑的迭代器对象,从而遍历链表。

这里还涉及一个职责分离原则

在面向对象设计中,一个类应该只负责一项核心职责。这让代码更清晰、更易于维护。

  • list 类的职责:管理一个元素的集合,提供添加、删除、查找等操作。它代表的是整个容器。
  • list_iterator 类的职责:代表容器中的一个位置(或指针),并提供访问该位置元素的方法。它的核心目的是遍历和访问。

这也是例如operator* ()等成员函数实现在封装迭代器的类里面的原因,其的作用是访问当前位置的元素...,这些行为显然属于迭代器的职责范围,而不是列表的职责范围。

cpp 复制代码
yunze::list<int>::iterator it = lt.begin();

细究一下这行代码,该代码中的函数调用

cpp 复制代码
yunze::list<int>::iterator it = lt.begin();
  1. lt.begin() 被调用。
  2. 在 begin() 内部,程序使用链表第一个节点的指针(如 head->next)作为参数,调用 list_iterator< int > 的构造函数,创建一个迭代器对象(list_iterator< int >类型)。
  3. begin() 返回这个迭代器对象(可能是临时对象,也可能被优化)。
  4. 用返回的迭代器对象初始化 it(可能调用拷贝构造,也可能被优化)。

这句代码的意思是:定义一个名为 it 的变量,它的类型是 yunze::list< int >::iterator(即 list_iterator< int >),并用 lt.begin() 返回的那个迭代器对象来初始化它。

*this 指针和 this 是什么?

在 C++ 的非静态成员函数中,编译器会自动给函数传递一个隐含的指针参数 ------this,它的作用是:指向调用这个成员函数的「对象本身」。

举个具体例子:假设你定义了迭代器对象 it,然后调用 ++it,此时 it 就是调用 operator++() 函数的对象,函数里的 this 指针就指向 it 这个对象(this 的类型是 list_iterator< T >*)。

而 *this 就是对 this 指针做「解引用」,得到的是:调用该函数的那个迭代器对象本身(类型是 list_iterator< T >)。

*为什么前置 ++ 要返回 this?

  1. 类型匹配(编译层面)
    函数声明的返回类型是 Self&(迭代器引用),而 * this 就是当前迭代器对象,返回它的引用(而非拷贝),刚好和返回类型匹配;
  2. 符合前置 ++ 的语义(逻辑层面)
    前置 ++ 的核心语义是:先让对象自身完成 "自增",再返回自身(支持连续操作,比如 ++(++it))。
    比如执行 ++it 后,it 本身已经指向了下一个节点,返回 *this 就是把更新后的 it 以引用形式返回,这样后续能继续对这个返回值做操作(比如 ++(++it)),完全符合 C++ 内置类型前置 ++ 的行为(比如 ++(++i) 对 int 也成立)。

1.3.1 迭代器结构的优势

C++实现迭代器这样的结构的优势有很多

  1. 统一遍历接口
    迭代器提供了一种统一的方式来遍历不同类型的容器(如 vector、list、map、set 等),而无需关心容器内部的数据结构。

优势:

同样的 print 函数可以遍历 vector、list 等不同容器。

用户不需要为每种容器编写不同的遍历逻辑。

  1. 解耦数据结构和算法
    迭代器使得算法(如排序、查找)可以独立于具体的数据结构存在。
    例如,std::sort 算法可以用于 vector、array,甚至是自定义容器,只要它们提供随机访问迭代器。
cpp 复制代码
std::vector<int> vec = {3, 1, 4, 1, 5, 9};
std::sort(vec.begin(), vec.end()); // 使用迭代器作为参数

优势:

算法可以复用在各种容器上。

容器的内部实现变化时,算法代码无需修改

用类封装迭代器还有类型上的魅力

cpp 复制代码
Node* it1;
yunze::list<int>::iterator it2;

上面两个对象定义出来的大小是一样的(比如说都存了1节点的地址,32位下都是四个字节),也就是说用一个类去封装节点的指针,其对象大小并不会变大,it1是个指针,4个字节存了一个地址,it2是个对象,但是对象内是一个指向节点的指针,也存了一个地址,但是二者的类型是不一样的

编译器编译代码生成指令是根据类型来做的,内存一样,内存上的值也一样,但是因为类型不一样,编译之后就会转换为不同的指令,达到不一样的行为

1.3.2 范围for

支持迭代器就支持范围for

范围 for 的底层原理

当写 for (auto e : lt) 时,编译器会自动将其替换为 "基于迭代器的遍历代码",等价于:

cpp 复制代码
// 1. 用容器的begin()获取起始迭代器
auto __it = lt.begin();
// 2. 用容器的end()获取结束迭代器
auto __end = lt.end();
// 3. 用迭代器的!=判断是否遍历结束
for (; __it != __end; ++__it) {
    // 4. 用迭代器的*获取元素
    auto e = *__it;
    // 你写的循环体逻辑(比如cout << e << " ")
}

范围 for 的 "必要条件"

要让范围 for 生效,容器和迭代器需要满足:

  1. 容器提供 begin() 和 end() 成员函数,返回迭代器对象;
  2. 迭代器需要支持:
    operator++:迭代器移动到下一个元素;
    operator!=:判断迭代器是否到达结束位置;
    operator*:获取迭代器指向的元素。

1.4 insert + erase

insert过程中就不存在迭代器失效的问题(list的插入不涉及扩容问题),这里的pos从插入前到插入后始终指向一个位置

erase过程就会存在迭代器失效问题,erase之后pos指向的节点直接被delete掉

1.5 头插 + 尾插 + 尾删 + 头删 + size

这一部分就是复用前面insert和erase的代码,库中的实现也是如此


cpp 复制代码
cout << lt.size() << endl;

这里运行是会报错的

当一个成员函数被const修饰时(比如size() const),这个函数内部的 * this,不是普通的类对象,而是「const 版本的类对象」。

cpp 复制代码
class list {
public:
    // size是const成员函数
    size_t size() const {
        // 这里的*this,类型是 const list<T>&(const版本的list对象)
        // 而不是普通的 list<T>&
    }
};

而范围 for 循环的「底层原理」如下

cpp 复制代码
// 1. 调用容器的begin(),拿到起始迭代器
auto it = *this.begin();
// 2. 调用容器的end(),拿到结束迭代器
auto end_it = *this.end();
// 3. 遍历迭代器
while (it != end_it) {
    auto& e = *it;
    ++it;
}

也就是说:范围 for 循环必须调用容器的begin()和end()函数

「普通版本的 begin/end」是「没有被 const 修饰的成员函数」,它的隐含 this 指针类型是 list< T > *(指向非 const 对象的指针)

而size()是 const 成员函数,里面的 * this是const list< T >对象。C++ 有个规则:
const 对象只能调用 const 成员函数,不能调用非 const 成员函数。

后面再补const版本的成员函数,除此之外,求size每次循环遍历太挫了,这里实现的不够好,直接定义一个效果更好,在insert的时候++_size,在erase的时候--_size

1.6 析构 + clear

这里析构的写法还是有点说法的,虽然erase之后迭代器会失效,但是可以利用其的返回值会返回删除数据的下一个位置的特性重新定位新的节点

且在erase函数代码中,有两种返回值的写法,这两种写法的核心区别在于返回值的类型匹配:

  1. return iterator(next);
    next是Node * 类型(链表节点的指针),而erase的返回值要求是iterator类型(迭代器类)。
    这里通过iterator(next)显式调用了list_iterator的构造函数,将Node * 包装成iterator对象,类型完全匹配,是标准、安全的写法。
  2. return next;
    next是Node * 类型,但erase的返回值是iterator类型。
    这种写法能 "编译通过",是因为list_iterator的构造函数是非 explicit 的(默认允许隐式类型转换) :编译器会自动调用list_iterator(Node * )构造函数,把Node * 隐式转换成iterator。
    但这属于隐式类型转换,可读性较差(别人看代码时可能不清楚 "为什么返回Node * 却能匹配iterator"),且如果后续把构造函数改成explicit,这段代码会直接编译失败。

上面析构最后哨兵位的节点最后还没有删,下面clear的作用就是清除所有数据,但是哨兵位不能删,因为清除干净后续还有可能插入数据,析构就是把整个链表对象销毁,哨兵位也要解决

1.7 多种构造

1.7.1 { }初始化


当代码执行到yunze::list< int > lt1 = {1,2,3,4,5,6}; 时,编译器会按以下步骤执行:

  1. 编译器识别到{1,2,3,4,5,6}是给list< in t>初始化的列表,会自动把这个列表打包成一个临时的initializer_list< int >对象
  2. 调用list的initializer_list构造函数,把上面的临时initializer_list< int >对象,传给构造函数的形参il
  3. 构造函数遍历il里的元素(1、2、3...),通过push_back逐个插入空链表,最终创建出完整的lt1对象

1.7.2 迭代器区间构造

说一下几个比较容易误解和容易存在疑问的点:

  1. C++ 中不同容器(比如vector、array、list自己)的迭代器类型是不同的 (比如vector::iterator和list< int >::iterator是完全不同的类型)。

    这个构造函数用模板参数InputIterator ,是为了实现泛化兼容:只要传入的first、last满足「输入迭代器的概念」(能解引用 * first、能递增++first、能比较first != last),不管它是哪个容器的迭代器,这个构造函数都能接收,并遍历[first, last)区间的元素来初始化当前list。

  2. InputIterator不是关键字 ,InputIterator只是模板参数的名字,是 C++ 开发者约定俗成的命名习惯 ------ 用InputIterator来表示 "这个模板参数需要满足「输入迭代器」的行为规范"(比如能读元素、能单向遍历)。

cpp 复制代码
类似的命名还有:
OutputIterator:输出迭代器(能写元素)
ForwardIterator:前向迭代器(能单向遍历 + 读写)
BidirectionalIterator:双向迭代器(能双向遍历 + 读写)

这里迭代器区间构造内部不用范围 for核心原因是:「范围 for 的适用场景和迭代器区间的本质不匹配」,我们从范围 for 的底层原理和迭代器区间构造的需求两个维度拆解:

  1. 先明确:范围 for 的底层依赖什么?
    范围 for 循环(for (auto& e : 容器/对象))的本质是编译器自动帮你做了两件事:
cpp 复制代码
// 你写的代码
for (auto& e : container) { ... }

// 编译器实际展开的代码(简化版)
auto __begin = container.begin(); // 必须能调用begin()
auto __end = container.end();     // 必须能调用end()
for (; __begin != __end; ++__begin) {
    auto& e = *__begin;
    ...
}

范围 for 的前提是 ------存在一个 "可遍历的对象"(比如容器),且这个对象能通过begin()/end()(成员函数或 std::begin/std::end)获取迭代器。

  1. 迭代器区间构造的核心需求:遍历「两个独立迭代器组成的区间」
    迭代器区间构造函数的参数是两个独立的迭代器first和last (比如可能是vector的迭代器、数组的指针、甚至istream_iterator),它要遍历的是[first, last)这个区间 ------ 但这个区间没有对应的 "可遍历对象":
    • 没有一个名为container的对象能调用begin()/end();
    • first和last只是两个孤立的迭代器,不是 "对象",没法写for (auto& e : first)或for (auto& e : (first, last))。

1.7.3 n个value的构造

这里也有一个问题,若T为int,但是n的类型是size_t,在实际构造时会优先使用迭代器区间的构造,所以要写两个版本出来

1.7.4 拷贝构造 + 赋值构造

list的拷贝构造也面临着经典的浅拷贝问题

如图值拷贝就会让两个对象指向同一块链表,list的深拷贝就是一个节点一个节点的拷贝

1.7.4.1 传统写法


1.7.4.2 现代写法

过程如下:

  1. 执行empty_init() → lt2 变成 "空链表(仅哨兵)";
  2. 构造临时对象tmp → 复用迭代器区间构造,tmp 拷贝 lt1 的所有元素;
  3. 执行swap(tmp) → 交换 lt2 和 tmp 的_head/_size:
    • lt2:拿到 tmp 的资源(即 lt1 的完整拷贝);
    • tmp:拿到 lt2 原来的 "空链表资源";
  1. 函数结束,tmp 析构 → 自动释放 "空链表资源",无内存泄漏。

这个 "现代写法" 的核心优势:拷贝操作在临时对象tmp上完成,若拷贝失败,当前对象仍保持empty_init()后的合法空状态,不会处于 "半拷贝、不一致" 的危险状态。


  1. 传值传参的关键作用
  2. 为什么要自己实现swap函数?
    代码中自定义了swap成员函数,而不是直接用std::swap,原因是 std::swap对自定义类型的默认行为低效且不安全:
    若用std::swap(默认行为)
    std::swap(a, b)对自定义类型的默认逻辑是:
cpp 复制代码
template<class T>
void swap(T& a, T& b) {
    T tmp(a);  // 1. 拷贝构造tmp(深拷贝,链表要复制所有节点)
    a = b;     // 2. 赋值a = b(再次深拷贝)
    b = tmp;   // 3. 赋值b = tmp(第三次深拷贝)
}

对链表来说,默认std::swap会做三次深拷贝,涉及大量内存分配 / 释放,效率极低,且过程中可能抛异常。

自定义swap的优势:

代码中自定义的swap是 "浅交换",只交换链表的核心资源指针和大小:

cpp 复制代码
void swap(list<T>& lt) {
    std::swap(_head, lt._head);  // 交换哨兵节点的指针(仅交换地址,无内存操作)
    std::swap(_size, lt._size);  // 交换元素个数(仅交换数值)
}

1.8 模板类的简化写法

对类模板来说,在类外面,类名不是类型,类名要加上模板参数才是类型

cpp 复制代码
list<int>//类外面必须类名+模板参数使用

但是在类里面做了简化,类名可以代表类型

但是我的个人建议还是写长一点,更清楚

1.9 迭代器成员的补充


前置++与后置++的写法区别




有节点的指针是不一定要写析构的,例如用类封装的迭代器中就有一个指向节点的指针,其不仅不用写析构,也不用写拷贝构造和赋值重载,迭代器主要是借助这个节点的指针去访问链表的数据

但是迭代器销毁后并不会销毁节点,节点是归链表管,若把链表传给printf函数去打印,函数内迭代器作用结束,函数结束后若迭代器销毁了,链表内的节点也跟着销毁就麻烦了,不是说有节点的指针指向一块资源就一定要写析构,而且迭代器也不用写深拷贝,他的特性需要的就是浅拷贝

cpp 复制代码
bit::list<int>::iterator it = lt.begin();

begin返回一个指向一个位置的迭代器对象之后,所期望的就是让it也指向该节点(浅拷贝)

1.10 const 迭代器

const迭代器与普通迭代器最大的区别就是不能修改,对于迭代器来说,修改主要借助解引用,而对一个用类封装的迭代器来说,解引用就依靠函数调用,operator * 的返回值是T&,返回的是一个别名所以可以修改,其若返回const T&就不能修改了,核心的差异就在这里

1.10.1 传统实现

传统的const迭代器的实现思路,就是单独定义一个类型来定义const迭代器,其他部分都不变,只有重载的 * 返回值改为const T

1.10.2 现代实现

也可以看出前面的写法太浪费了,就一个重载函数的返回类型不一样,其余都一样。所以现代写法的思路就是写一个类模板,其有两个模板参数,这样就可以达到写一个类模板,编译器实例化出两个类的效果


1.11 迭代器operator*/operator->重载与./->运算符

假设现在链表中要存储一些复杂的类型,比如这里是A类型,A类型内包含多个数据类型,此时如下图若要用迭代器遍历链表就有所不同了,就会编译不通过

补充:这里会触发多参数构造函数隐式类型转换,当自定义类型的多参数构造函数未被explicit修饰时,编译器会自动用 "匹配构造函数参数的源数据(比如初始化器列表)" 构造该类型的临时对象,从而将源数据隐式转换为自定义类型。

这里编译不通过的主要原因是struct A未重载operator<<,无法直接用cout输出

另一种解决方案是绕过operator<<重载、直接访问自定义类型公有成员的解决方案 ,用来解决 "无法直接用cout输出A对象" 的编译问题,具体逻辑拆解如下:

再细节补充这里写法上容易出现的错误

lt.push_back(1, 1); 就完全不行

cpp 复制代码
void push_back(const T& x); // T=A,参数只有1个:const A&

这个函数的设计目标是:接收一个A类型的对象(或能隐式转换为A的数据源),将其尾插到链表。

lt.push_back(1, 1)是向push_back传递两个int类型的参数(1和1),但push_back只定义了 "接收 1 个参数" 的版本 ------ 编译器会直接报「参数数量不匹配」的错误:

如果想 "直接传两个 int 构造 A 对象",正确写法如下
写法 1:显式构造A对象(最直观)

cpp 复制代码
lt.push_back(A(1, 1)); // 显式调用A的构造函数,生成A对象,作为push_back的单个参数

写法 2:初始化器列表(原可行写法)

cpp 复制代码
lt.push_back({1, 1}); // 隐式构造A对象,本质还是传递单个初始化器列表参数

但是迭代器的主要作用是模拟指针的行为,指针要解引用数据有两种方式,第一种对于A类型就如上面那般解引用,另一种就是箭头

下面说一下两者的区别

在list_iterator类模板中,针对T=A、Ref=A&(list< A >的迭代器类型),两个运算符的核心定义如下:

cpp 复制代码
template<class T, class Ref>
struct list_iterator
{
    using Node = list_node<T>;
    Node* _node; // 迭代器指向的链表节点指针

    // 1. 重载operator*:返回T对象的引用,适配.的使用规则
    Ref operator*()
    {
        return _node->_data; // T=A时,返回A对象的引用(A&)
    }

    // 2. 重载operator->:返回T对象的指针,适配->的使用规则
    T* operator->()
    {
        return &_node->_data; // T=A时,返回A对象的地址(A*)
    }
};



五、.和->的核心区别(原生 + 重载场景)

这里也是一样,除了普通迭代器还有const迭代器,const迭代器去访问数据就不是T* 了而是const T*,这里写为Ptr,就如前面的const T&是Ref一样

库中也是这样写了三个模板参数来达到这样的效果

看到这里可能会对这一块的模板代码的逻辑有些混乱,下面看一下结合实际场景的这种模板写法的实例化情况

cpp 复制代码
#include "list.h"
using namespace yunze;

int main() {
    // 场景1:创建普通list<int>对象
    list<int> lt;
    lt.push_back(10);
    lt.push_back(20);

    // 场景2:普通迭代器遍历+修改
    list<int>::iterator it = lt.begin();
    *it = 100;
    ++it;

    // 场景3:const list<int>对象 + const迭代器
    const list<int> clt = lt;
    list<int>::const_iterator cit = clt.begin();
    cout << *cit << endl;

    return 0;
}
cpp 复制代码
// 编译器生成的list<int>核心代码(简化版)
class list<int> {
    // 内部别名:Node = list_node<int>
    using Node = list_node<int>;
public:
    // 迭代器别名:实例化list_iterator的两个版本
    using iterator = list_iterator<int, int&, int*>;
    using const_iterator = list_iterator<int, const int&, const int*>;

    // 成员函数声明(此时还未实例化,调用时才实例化)
    iterator begin();
    iterator end();
    const_iterator begin() const;
    const_iterator end() const;
    void empty_init();
    list<int>(); // 构造函数
    void push_back(const int& x);
    // ... 其他成员
private:
    Node* _head;
    size_t _size = 0;
};
cpp 复制代码
// 编译器生成的list_node<int>代码
struct list_node<int> {
    list_node<int>* _prev;
    list_node<int>* _next;
    int _data;

    list_node(const int& x = int())
        : _prev(nullptr), _next(nullptr), _data(x) {}
};
cpp 复制代码
// 编译器生成的普通迭代器代码
struct list_iterator<int, int&, int*> {
    using Self = list_iterator<int, int&, int*>;
    using Node = list_node<int>;
    Node* _node;

    // 构造函数
    list_iterator(Node* node) : _node(node) {}

    // 解引用:返回int&(可修改)
    int& operator*() { return _node->_data; }

    // 箭头:返回int*
    int* operator->() { return &_node->_data; }

    // 前置++
    Self& operator++() {
        _node = _node->_next;
        return *this;
    }

    // 后置++、--、==、!=等逻辑(编译器生成对应代码)
    Self operator++(int) { /* 具体逻辑 */ }
    bool operator!=(const Self& s) const { return _node != s._node; }
    // ...
};
cpp 复制代码
// 编译器生成的const迭代器代码
struct list_iterator<int, const int&, const int*> {
    using Self = list_iterator<int, const int&, const int*>;
    using Node = list_node<int>;
    Node* _node;

    list_iterator(Node* node) : _node(node) {}

    // 解引用:返回const int&(只读)
    const int& operator*() { return _node->_data; }

    // 箭头:返回const int*
    const int* operator->() { return &_node->_data; }

    // ++/--/==/!=等逻辑和普通迭代器完全一致(编译器复用逻辑,仅修改返回类型)
    Self& operator++() {
        _node = _node->_next;
        return *this;
    }
    // ...
};
cpp 复制代码
// 编译器生成的list<int>::push_back代码
void list<int>::push_back(const int& x) {
    insert(end(), x); // 调用insert
}

// 同时实例化insert函数(因为push_back调用了insert)
list<int>::iterator list<int>::insert(iterator pos, const int& x) {
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    Node* newnode = new Node(x);
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    ++_size;
    return iterator(newnode);
}


二、完整源码

List.h

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;

namespace yunze
{
	// 泛型
	// 定义链表节点结构
	template<class T>
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _data;
		//节点的构造函数
		list_node(const T& x = T())
			:_prev(nullptr)
			, _next(nullptr)
			, _data(x)
		{
		}
	};

	//Ref(引用)
	template<class T, class Ref, class Ptr>
	//用类封装迭代器
	//list_iterator 本身是一个类模板,list_iterator<T> 是这个类模板的一个实例化
	struct list_iterator
	{
		//自身类型别名 
		using Self = list_iterator<T, Ref, Ptr>;
		//链表节点类型别名
		using Node = list_node<T>;
		//指向链表节点的指针(迭代器的抓手)
		Node* _node;

		//构造函数接收一个Node*类型的指针,通过初始化列表将_node指向该节点
		//作用是确定迭代器在链表中的初始位置(比如指向第一个有效节点、哨兵节点等)
		list_iterator(Node* node)
			:_node(node)
		{
		}

		//*it = 1
		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		//前置++
		Self& operator++()
		{
			_node = _node->_next;
			//返回迭代器对象自身
			return *this;
		}

		//后置++
		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			//返回迭代器对象自身
			return tmp;
		}

		//--it
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		//it--
		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}

	};

	//template<class T>
	////用类封装迭代器
	//struct list_const_iterator
	//{
	//	//自身类型别名
	//	using Self = list_const_iterator<T>;
	//	//链表节点类型别名
	//	using Node = list_node<T>;
	//	//指向链表节点的指针(迭代器的抓手)
	//	Node* _node;

	//	//构造函数接收一个Node*类型的指针,通过初始化列表将_node指向该节点
	//	//作用是确定迭代器在链表中的初始位置(比如指向第一个有效节点、哨兵节点等)
	//	list_const_iterator(Node* node)
	//		:_node(node)
	//	{
	//	}

	//	//*it
	//	const T& operator*()
	//	{
	//		return _node->_data;
	//	}

	//	//前置++
	//	Self& operator++()
	//	{
	//		_node = _node->_next;
	//		//返回迭代器对象自身
	//		return *this;
	//	}

	//	//后置++
	//	Self operator++(int)
	//	{
	//		Self tmp(*this);
	//		_node = _node->_next;
	//		//返回迭代器对象自身
	//		return tmp;
	//	}

	//	//--it
	//	Self& operator--()
	//	{
	//		_node = _node->_prev;
	//		return *this;
	//	}

	//	//it--
	//	Self operator--(int)
	//	{
	//		Self tmp(*this);
	//		_node = _node->_prev;
	//		return tmp;
	//	}

	//	bool operator!=(const Self& s) const
	//	{
	//		return _node != s._node;
	//	}

	//	bool operator==(const Self& s) const
	//	{
	//		return _node == s._node;
	//	}

	//};

	//定义链表类本身
	template<class T>
	class list
	{
		using Node = list_node<T>;
	public:
		using iterator = list_iterator<T, T&, T*>;
		using const_iterator = list_iterator<T, const T&, const T*>;
		//using const_iterator = list_const_iterator<T>;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		void empty_init()
		{
			//创建一个哨兵节点(头节点),不存储任何有效数据
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
		}

		// list类的默认构造函数 
		list()
		{
			empty_init();
		}

		//{ }初始化
		list(initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		template <class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		list(size_t n, T val = T())
		{
			empty_init();
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		list(int n, T val = T())
		{
			empty_init();
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		// 传统写法
		// lt2(lt1)
		list(const list<T>& lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		//支持连续赋值
		list<T>& operator=(const list<T>& lt)
		{
			//判断是不是自己给自己赋值
			if (this != &lt)
			{
				//释放lt1
				clear();
				for (auto& e : lt)
				{
					push_back(e);
				}
			}
			return *this;
		}

		//// 现代写法
		//// lt2(lt1)
		////list(list<T>& lt)
		//list(const list& lt)
		//{
		//	empty_init();
		//	//list<T> tmp(lt.begin(), lt.end());
		//	list tmp(lt.begin(), lt.end());
		//	swap(tmp);
		//}

		//// 现代写法
		//// lt1 = lt3
		////list<T>& operator=(list<T> tmp)
		//list& operator=(list tmp)
		//{
		//	swap(tmp);
		//	return *this;
		//}

		//void swap(list<T>& lt)
		void swap(list& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
			_size = 0;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		////尾插一个新元素
		//void push_back(const T& x)
		//{
		//	Node* newnode = new Node(x);
		//	// 之前的尾节点
		//	Node* tail = _head->_prev;
		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	newnode->_next = _head;
		//	_head->_prev = newnode;
		//}

		void push_back(const T& x)
		{
			//在end前面插入一个值
			insert(end(), x);
		}

		void push_front(const T& x)
		{
			//在begin前面插入一个值
			insert(begin(), x);
		}

		void pop_back()
		{
			//找到end的前一个节点
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		void insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
		}

		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			//prev next
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			--_size;
			//return iterator(next);
			return next;
		}

		size_t size() const
		{
			//size_t n = 0;
			////不知道数据类型,用引用
			//for (auto& e : *this)
			//{
			//	++n;
			//}
			//return n;
			return _size;
		}
	private:
		//链表的唯一私有成员,指向哨兵节点(头节点)的指针
		//通过该指针可访问整个链表
		Node* _head;
		size_t _size = 0;
	};
}

test.c

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 666
#include"List.h"

//void test_list1()
//{
//	bit::list<int> lt;
//	lt.push_back(1);
//	lt.push_back(2);
//	lt.push_back(3);
//	lt.push_back(4);
//
//	bit::list<int>::iterator it = lt.begin();
//	while (it != lt.end())
//	{
//		cout << *it << " ";
//		++it;
//	}
//	cout << endl;
//
//	for (auto e : lt)
//	{
//		cout << e << " ";
//	}
//	cout << endl;
//}

void test_list2()
{
	yunze::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_front(-1);
	lt.push_front(-2);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	lt.pop_back();
	lt.pop_back();
	lt.pop_front();
	lt.pop_front();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << lt.size() << endl;
}

void Print(const yunze::list<int>& lt)
{
	yunze::list<int>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
		//*it = 1; const迭代器不能修改
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

void test_list3()
{
	yunze::list<int> lt1 = { 1,2,3,4,5,6 };
	for (auto e : lt1)
	{
		cout << e << " ";
	}
	cout << endl;

	yunze::list<int> lt2(lt1);
	for (auto e : lt2)
	{
		cout << e << " ";
	}
	cout << endl;

	yunze::list<int> lt3 = { 10, 20, 30 };
	lt1 = lt3;
	for (auto e : lt1)
	{
		cout << e << " ";
	}
	cout << endl;

	Print(lt1);
}

struct A
{
	int _a1;
	int _a2;
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{
	}
};

void test_list4()
{
	yunze::list<A> lt;
	lt.push_back({ 1, 1 });
	lt.push_back({ 2, 2 });
	lt.push_back({ 3, 3 });
	yunze::list<A>::iterator it = lt.begin();
	while (it != lt.end())
	{
		//cout << *it << " ";
		cout << (*it)._a1 << ":" << (*it)._a2 << endl;
		cout << it->_a1 << ":" << it->_a2 << endl;
		//显式调用的完整写法
		cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;
		++it;
	}
	cout << endl;
}

int main()
{
	//test_list1();
	//test_list2();
	//test_list3();
	test_list4();
	return 0;
}

结语

相关推荐
LFly_ice2 小时前
Next-1-启动!
开发语言·前端·javascript
2201_757830872 小时前
条件分页查询
java·开发语言
努力学习的小廉2 小时前
【QT(六)】—— 常用控件(三)
开发语言·qt
Z.yping2 小时前
qt语言家一键更新或发布多个模块且多个国家的语言
开发语言·qt·restful
MSTcheng.2 小时前
【C++】set / multiset 保姆级教程:从底层原理到实战应用!
开发语言·c++·set
历程里程碑2 小时前
C++ 16:C++11新特化
c语言·开发语言·数据结构·c++·经验分享
巴拉巴拉~~2 小时前
KMP 算法通用步进器组件:KmpStepperWidget 横向 / 纵向 + 匹配进度 + 全样式自定义
java·服务器·开发语言
weixin_307779132 小时前
赋能插件,驱动图表:Jenkins ECharts API插件详解
运维·开发语言·自动化·jenkins·echarts
IMPYLH2 小时前
Lua 的 Math(数学) 模块
开发语言·笔记·lua