上一节我们讲解了list的基本功能c;那么本节我们就结合底层代码来分析list是怎么实现的c;那么废话不多说c;我们正式进入今天的学习:)
c="https://i-blog.csdnimg.cn/direct/a795d2d7bddd4f82a7870cee0cd9a100.png" width="515" />
我们先来看一下list的底层基本结构:
c="https://i-blog.csdnimg.cn/direct/857717053d484ac7b067213617f20bd1.png" width="1460" />
这里比较奇怪的一点是底层选择了void*来表示链表的指针c;其实可以不用这么做
接下来是迭代器部分:
c="https://i-blog.csdnimg.cn/direct/51078ed406e7454aa2226ac1a8b106e3.png" width="1942" />
模拟实现链表难就难在实现迭代器c;因为链表的color:#fe2c24">物理空间不是连续的c;所以color:#fe2c24">不能简单的通过typedef一个指针变量来达到实现迭代器的目的
链表为空的初始化:
c="https://i-blog.csdnimg.cn/direct/0e53797570e140ebbff3ce5fa48e689e.png" width="1392" />
链表为空时不是简单的把所有的指针都赋空指针c;而是color:#fe2c24">创建一个节点c;让这个节点的next和prev指针都指向自己。这就是创造了一个头节点c;这里涉及到数据结构阶段color:#fe2c24">双向带头链表的基本知识
c="https://i-blog.csdnimg.cn/direct/eb47a0d5ea9349f5a54c2ba8dc8de2c4.png" width="1914" />
get_node是申请节点c;调用了内存池c;由于我们还没有学习内存池c;所以可以简单地将这里理解为malloc
下面是源代码中的头插尾插接口:
c="https://i-blog.csdnimg.cn/direct/97d22c2312504c5fae4e6a1ada9e0cf2.png" width="1836" />
通过这些c;我们可以知道c;单纯实现链表的功能是没有什么难度的c;和我们之前实现string和vector差不多c;难就难在理解迭代器的实现
既然底层结构已经看的差不多了c;那我们现在就来实现一下list吧
首先要实现链表的基本结构c;链表的基本结构是color:#fe2c24">节点:
<code class="language-cpp"> template<class T> struct list_node { list_node(const T& data = T()) :_data(data) ,_next(nullptr) ,_prev(nullptr) {} T _data; list_node<T>* _next; list_node<T>* _prev; };code>
先来实现一下构造函数c;根据底层是color:#fe2c24">双向带头循环链表c;写出代码如下:
<code class="language-cpp"> template<class T> class list { public: typedef list_node<T> Node; list() { _head = new Node(T()); _head->_next = _head; _head->_prev = _head; _size = 0; } private: Node* _head; size_t _size; };code>
实现size和empty函数c;由于代码很简单c;就不做讲解了:
<code class="language-cpp"> size_t size() { return _size; } bool empty() { return _size == 0; }code>
因为不是每一个STL的容器都是存储在连续的物理空间之中c;所以并不是每一个容器都支持下标+[]访问c;但是color:#fe2c24">所有的容器都有迭代器c;都支持范围forc;因此实现迭代器是很重要的一个步骤c;我们来逐步分析迭代器的实现:
我们知道c;对于一个节点而言c;*解引用以后找到的不是节点的值c;而是节点的地址;因为存储空间不连续c;不能通过++来找到下一个节点c;所以就不能仅通过typedef来实现迭代器c;此时我们就应该考虑color:#fe2c24">封装+运算符重载来实现迭代器c;主要内容就是重载 * ++ != 等c;让其满足与其他迭代器相同的作用
<code class="language-cpp"> template<class T> struct list_iterator { Node* _node; };code>
<code class="language-cpp"> list_iterator(Node* node) :_node(node) {}code>
<code class="language-cpp"> T& operator*() { return _node->_data; }code>
由于color:#fe2c24">++以后返回的数据类型依旧是迭代器c;为了书写简便一点c;调用typedef:
<code class="language-cpp"> Self& operator++() { _node = _node->_next; return *this; } Self& operator--() { _node = _node->_prev; return *this; }code>
因为后置++和--是将当前的值使用以后再执行++或者--c;所以我们需要把原本的值拷贝一份c;用于完成返回操作:
<code class="language-cpp"> Self& operator++(int) { Self tmp(*this); _node = _node->_next; return tmp; } Self& operator--(int) { Self tmp(*this); _node = _node->_prev; return tmp; }code>
<code class="language-cpp"> bool operator!=(const Self& s) { return _node != s._node; } bool operator==(const Self& s) { return _node == s._node; }code>
先来分析一下为什么需要重载->这个符号:
接下来先构造一个使用->的情景:
假设有一个这样的结构体:
<code class="language-cpp"> struct AA { int _aa1 = 1; int _aa2 = 2; };code>
此时我们要在链表中存入这个结构体并且打印:
<code class="language-cpp"> list<AA> ltaa; ltaa.push_back(AA()); ltaa.push_back(AA()); ltaa.push_back(AA()); ltaa.push_back(AA()); list<AA>::iterator itaa = ltaa.begin(); while (itaa != ltaa.end()) { cout << *itaa << " "; ++itaa; } cout << endl;code>
c="https://i-blog.csdnimg.cn/direct/22772951a16d41aca73498628fa0af76.png" width="2074" />
此时运行以后发现编译不通过
这里面的itaa通过解引用会得到节点中的datac;data的数据类型是Tc;也就是自定义类型AA。因为没有重载流插入c;所以这里的自定义类型无法打印c;要想解决这个问题有两种方法:
第一种方法:给AA重载流插入
第二种方法:逐一访问结构体中的元素
<code class="language-cpp"> list<AA>::iterator itaa = ltaa.begin(); while (itaa != ltaa.end()) { cout << (*itaa)._aa1 << " and " << (*itaa)._aa2 << endl; ++itaa; } cout << endl;code>
c="https://i-blog.csdnimg.cn/direct/0d08d53fa2874b929e8415ddff216838.png" width="2316" />
此时代码就成功运行了
但是这样写就很麻烦c;此时就可以重载->这个运算符c;这里先把代码写出来再对其进行分析:
<code class="language-cpp"> T* operator->() { return &_node->_data; }code>
<code class="language-cpp"> list<AA>::iterator itaa = ltaa.begin(); while (itaa != ltaa.end()) { cout << itaa->_aa1 << " and " << itaa->_aa2 << endl; ++itaa; } cout << endl;code>
c="https://i-blog.csdnimg.cn/direct/87a9c78860084f24bc6a5b9953f3bdef.png" width="2261" />
疑难解答:为什么operator-> 操作符重载函数返回值 T* 而不是 T&
在 list_iterator 这个迭代器类模板中c;operator-> 操作符重载函数设计为返回 T* 而不是 T&c;这与 operator-> 操作符的特殊语义和 C++ 语言的规定有关:
在 C++ 中c;operator-> 操作符有特殊的处理规则。当我们使用 -> 操作符时c;编译器会尝试不断应用 -> 操作c;直到得到一个color:#fe2c24">非指针类型的对象
简单地说就是:当你使用迭代器 it 调用 it->member 时c;编译器会先调用 it.operator->()c;如果 operator->() 返回的是一个指针c;那么就直接访问该指针指向对象的成员;color:#fe2c24">如果返回的不是指针c;编译器会再次对返回值应用 ->
我们可以分两点来说明返回 T* 的合理性:
1. 符合使用习惯
迭代器的 operator-> 操作符通常用于模拟指针的行为c;返回 T* 可以让迭代器像指针一样使用
例如c;对于一个存储自定义类型 MyClass 的链表c;使用迭代器访问 MyClass 的成员时c;我们希望能够像使用指针一样直接通过 -> 操作符访问成员c;如下所示:
<code class="language-cpp">namespace xiaobai { template<class T> struct list_node { list_node(const T& data = T()) : _data(data) , _next(nullptr) , _prev(nullptr) {} T _data; list_node<T>* _next; list_node<T>* _prev; }; template<class T> struct list_iterator { typedef list_node<T> Node; typedef list_iterator<T> Self; list_iterator(Node* node) : _node(node) {} T& operator*() { return _node->_data; } Self& operator++() { _node = _node->_next; return *this; } bool operator!=(const Self& s) { return _node != s._node; } T* operator->() { return &_node->_data; } Node* _node; }; } int main() { xiaobai::list_node<MyClass> node(MyClass(10)); xiaobai::list_iterator<MyClass> it(&node); // 使用迭代器的 -> 操作符访问成员 std::cout << it->value << std::endl; return 0; }code>
在这个例子中c;it->value 能够正常工作c;color:#fe2c24">因为 operator->() 返回的是 MyClass* 类型的指针c;编译器可以直接通过该指针访问 value 成员
2. 避免额外的复杂度
如果 operator->() 返回 T&c;编译器会再次对返回的引用应用 -> 操作c;这会导致代码逻辑变得复杂c;而且可能不符合用户的预期
例如:若T是一个自定义类型c;由于T&不是指针类型c;color:#fe2c24">编译器会尝试调用 T 类型的 operator->() 函数c;这可能会引发编译错误或意外的行为
所以T* operator->() 是为了让迭代器能够像指针一样使用c;符合用户的使用习惯c;并且遵循了 C++ 中 operator-> 操作符的特殊语义。而 T& operator->() 会破坏这种语义c;导致代码color:#fe2c24">逻辑复杂且不符合预期c;因此通常不这样设计
这段代码初步看起来会很奇怪c;它这里color:#fe2c24">实际应该是两个箭头c;但是为了可读性省略了一个箭头
(具体细节请浏览上面的疑难解答)
<code class="language-cpp">cout << itaa.operator->()->_aa1 << " and " << itaa.operator->()->_aa2 << endl;code>
第一个箭头是color:#fe2c24">运算符重载c;返回的是color:#fe2c24">AA*类型的变量
第二个箭头是color:#fe2c24">原生指针c;通过这个箭头就可以访问到list中的数据了
到这里我们就完成了迭代器:
<code class="language-cpp"> template<class T> struct list_iterator { typedef list_node<T> Node; typedef list_iterator<T> Self; list_iterator(Node* node) :_node(node) {} T& operator*() { return _node->_data; } Self& operator++() { _node = _node->_next; return *this; } Self& operator++(int) { Self tmp(*this); _node = _node->_next; return tmp; } Self& operator--() { _node = _node->_prev; return *this; } Self& operator--(int) { Self tmp(*this); _node = _node->_prev; return tmp; } bool operator!=(const Self& s) { return _node != s._node; } bool operator==(const Self& s) { return _node == s._node; } T* operator->() { return &_node->_data; } Node* _node; };code>
此时再去链表的类中typedef迭代器c;简化书写:
<code class="language-cpp"> template<class T> class list { public: typedef list_node<T> Node; typedef list_iterator<T> iterator; //....... private: Node* _head; size_t _size; };code>
实现了迭代器c;就color:#fe2c24">满足了范围for遍历c;此时就可以写一个访问容器的接口c;因为所有容器都具有迭代器c;所以这个接口也可以访问其他STL容器
<code class="language-cpp"> template<class Container> void print_container(const Container& con) { for (auto e : con) { cout << e << " "; } cout << endl; }code>
现在测试一下这个打印:
<code class="language-cpp"> list<int> lt; ltaa.push_back(1); ltaa.push_back(2); ltaa.push_back(3); ltaa.push_back(4); print_container(lt);code>
此时运行代码却发现编译报错了c;为什么函数外面使用范围for可以打印c;但是print_container函数里面使用范围for却打印不了c;这是为什么呢?
因为函数里面的color:#fe2c24">参数是const参数c;函数外面是普通的参数c;我们还color:#fe2c24">没有实现const迭代器c;所以这里就会报错
再来分析一下const迭代器的特征:先思考一下const迭代器为什么是const_iterator而color:#fe2c24">不是普通的iterator加上前置的const修饰?
要想想清楚这个问题c;我们首先需要明白color:#fe2c24">const迭代器是自身不能修改还是指向的内容不能修改。这里我们结合指针不同位置的const意义来理解:
<code class="language-cpp">T* const ptr; const T* ptr;code>
第一种:color:#fe2c24">指针本身不允许改变
第二种:color:#fe2c24">指针指向的内容不允许改变
很明显的c;const迭代器是想完成第二种功能c;如果是const iterator的话c;const的作用是color:#fe2c24">确保iterator本身不被修改c;color:#fe2c24">迭代器本身不能修改的话就无法实现遍历。const迭代器的产生是为了在color:#fe2c24">保证遍历的前提之下保证迭代器指向的内容也不被修改
那么该怎么修改代码来完成这一目的呢?
我们在实现迭代器这一个类时c;对数据的修改接口是operator*以及operator->c;对于const迭代器而言c;返回类型就应该加上const:
<code class="language-cpp"> const T& operator*() { return _node->_data; } const T* operator->() { return &_node->_data; }code>
所以这里就需要color:#fe2c24">拷贝之前的普通迭代器代码c;再在*和->的重载中加上const完成const迭代器
<code class="language-cpp"> template<class T> struct list_const_iterator { typedef list_node<T> Node; typedef list_iterator<T> Self; list_iterator(Node* node) :_node(node) { } const T& operator*() { return _node->_data; } Self& operator++() { _node = _node->_next; return *this; } Self& operator++(int) { Self tmp(*this); _node = _node->_next; return tmp; } Self& operator--() { _node = _node->_prev; return *this; } Self& operator--(int) { Self tmp(*this); _node = _node->_prev; return tmp; } bool operator!=(const Self& s) { return _node != s._node; } bool operator==(const Self& s) { return _node == s._node; } const T* operator->() { return &_node->_data; } Node* _node; };code>
此时在color:#fe2c24">重载一下begin和end函数c;给一个const迭代器版本c;并且在list类中typedef const迭代器:
(普通迭代器可以调用const迭代器的begin和endc;因为color:#fe2c24">权限可以缩小;反之const迭代器无法调用普通迭代器的begin和endc;因为color:#fe2c24">权限不能放大)
<code class="language-cpp"> template<class T> class list { public: typedef list_node<T> Node; typedef list_iterator<T> iterator; typedef list_const_iterator<T> const_iterator; iterator begin() { return _head->_next; } iterator end() { return _head; } const_iterator begin() const { return _head->_next; } const_iterator end() const { return _head; } //........ private: Node* _head; size_t _size; }; code>
color:#ff9900">**************************************************************************************************************
在 list 类中重载 `begin` 和 `end` 函数(即提供 `const` 和非 `const` 版本)是为了支持对 `const` 对象和非 `const` 对象的迭代操作c;下面详细解释原因:
1. 支持非 `const` 对象的迭代
当你有一个非 `const` 的 `list` 对象时c;你可能希望能够修改列表中的元素。此时c;你需要使用非 `const` 迭代器c;因为非 `const` 迭代器color:#fe2c24">允许对其所指向的元素进行修改。非 `const` 版本的 `begin` 和 `end` 函数返回的就是非 `const` 迭代器
2. 支持 `const` 对象的迭代
当你有一个 `const` 的 `list` 对象时c;你color:#fe2c24">不应该修改列表中的元素c;因为这违反了 `const` 限定的语义。此时c;你需要使用 `const` 迭代器c;`const` 迭代器color:#fe2c24">只能用于访问元素c;而不能修改元素。`const` 版本的 `begin` 和 `end` 函数返回的就是 `const` 迭代器
3. 函数重载的实现
通过函数重载c;编译器会根据对象是否为 `const` 来选择合适的 `begin` 和 `end` 函数版本。如果对象是 `const` 的c;编译器会调用 `const` 版本的 `begin` 和 `end` 函数c;返回 `const` 迭代器;如果对象是非 `const` 的c;编译器会调用非 `const` 版本的 `begin` 和 `end` 函数c;返回非 `const` 迭代器。
重载 `begin` 和 `end` 函数是为了提供对 `const` 对象和非 `const` 对象的一致迭代接口c;同时保证 `const` 对象的元素不被意外修改c;遵循了 C++ 的 `const` 正确性原则c;使得代码更加安全和灵活。
color:#ff9900">**************************************************************************************************************
此时用一个color:#ffd900">很特别的用例来测试一下代码(这里修改了一下print_container函数):
<code class="language-cpp"> template<class Container> void print_container(const Container& con) { list<int>::const_iterator it = con.begin(); while(it != con.end()) { *it += 10; cout << *it << " "; ++it; } cout << endl; for (auto e : con) { cout << e << " "; } cout << endl; } void test_list01() { list<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3); lt.push_back(4); list<int>::iterator it = lt.begin(); while (it != lt.end()) { cout << *it << " "; ++it; } cout << endl; }code>
c="https://i-blog.csdnimg.cn/direct/fa9c9491444b446cba60a456efd52efe.png" width="1635" />
可以看到c;这里非常奇怪c;明明我们color:#ff9900">故意在打印容器代码里面修改了const迭代器指向的内容c;想让它报错c;但是代码却成功运行了c;这是为什么呢?
这就涉及到我们之前提及的color:#fe2c24">按需实例化c;这里的代码color:#fe2c24">存在着编译错误c;' *it += 10 ' 中的*it返回的数据类型应该是const T&c;对其进行+=10操作本身是应该编译报错的c;但是这里非但没有报错反而还运行成功了
之所以产生这样的结果c;是因为color:#fe2c24">模板和类模板都会走按需实例化。模板不能被直接调用c;模板用于将对应类型的代码实例化出来c;实例化出来的代码才能够使用
因为我们在主函数中没有使用print_container函数c;color:#fe2c24">编译器在编译的过程中就不会去实例化print_container函数里面的内容c;而没有实例化的代码编译器color:#fe2c24">只会对其进行很简单的语法检查c;比如:语句结束没加分号c;对于细枝末节的操作编译器不会检查。
此时如果color:#fe2c24">使用print_container函数就会报错:
<code class="language-cpp"> list<int>::iterator it = lt.begin(); while (it != lt.end()) { cout << *it << " "; ++it; } cout << endl; print_container(lt);code>
c="https://i-blog.csdnimg.cn/direct/59b9c0c2ee99405f82f999fee6ceaa01.png" width="2078" />
刚才那种实现方式我们的确能成功的完成任务c;但是const迭代器和普通迭代器除了operator*和->的代码有区别以外c;其他地方的代码一模一样c;这么设计的话在代码长度上就会非常的冗余c;我们去底层观察一下是怎么实现迭代器的:
c="https://i-blog.csdnimg.cn/direct/100c29c72fbb42e3a52b0955f19abae8.png" width="1702" />
c="https://i-blog.csdnimg.cn/direct/72e84a4106734c318e45e8aa818d97af.png" width="1586" />
c="https://i-blog.csdnimg.cn/direct/0bdc213b092d4d4eaf40934844a90512.png" width="1344" />
c="https://i-blog.csdnimg.cn/direct/a138058787484d0185000b4079a8eb76.png" width="1438" />
可以看到c;它没有将迭代器和const迭代器定义为两个类c;而是同一个类模板。而且它除了传入了 T c;color:#fe2c24">还传入了 T& 和 T* 这两个参数
如果是普通迭代器c;它的中的参数是color:#fe2c24"> T T& T* c;分别传给了参数 color:#fe2c24">T Ref Ptr c;而 Ref 和 Ptr 分别被重命名为 reference 和 pointer c;而 reference 和 pointer 分别又color:#fe2c24">是 operator* 和 operator-> 的返回值;
如果是const迭代器c;它的中的参数是 T 、color:#fe2c24">const T&、 const T* c;分别传给了参数 T Ref Ptr c; Ref 和 Ptr 分别被重命名为 reference 和 pointer c;而 reference 和 pointer 分别又color:#fe2c24">是 operator* 和 operator-> 的返回值;
通过这种形式就控制住了operator*和->的返回值
虽然这两种写法在本质上没有区别c;只是之前的写法是自己写了两个类c;而这种方法是实现了一个类模板并且传了不同的模板参数给编译器c;通过编译器来实例化出两个类。通过这样的方法c;我们就可以color:#fe2c24">实现精简代码的需求
那么就根据底层实现的原理来完善一下原来的代码吧:
<code class="language-cpp"> template<class T, class Ref, class Ptr> struct list_iterator { typedef list_node<T> Node; typedef list_iterator<T, Ref, Ptr> Self; Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } //....... Node* _node; }; code>
<code class="language-cpp"> template<class T> class list { public: typedef list_node<T> Node; typedef list_iterator<T, T&, T*> iterator; typedef list_iterator<T, const T&, const T*> const_iterator; //....... private: Node* _head; size_t _size; };code>
<code class="language-cpp"> template<class T, class Ref, class Ptr> struct list_iterator { typedef list_node<T> Node; typedef list_iterator<T, Ref, Ptr> Self; list_iterator(Node* node) :_node(node) {} Ref operator*() { return _node->_data; } Self& operator++() { _node = _node->_next; return *this; } Self& operator++(int) { Self tmp(*this); _node = _node->_next; return tmp; } Self& operator--() { _node = _node->_prev; return *this; } Self& operator--(int) { Self tmp(*this); _node = _node->_prev; return tmp; } bool operator!=(const Self& s) { return _node != s._node; } bool operator==(const Self& s) { return _node == s._node; } Ptr operator->() { return &_node->_data; } Node* _node; };code>
begin函数用于color:#fe2c24">返回链表第一个元素:
<code class="language-cpp"> iterator begin() { iterator it(_head->_next); return it; }code>
也可以用返回color:#fe2c24">匿名对象来简化代码:
<code class="language-cpp"> return iterator(_head->_next);code>
还有一种更加简单的写法:
<code class="language-cpp"> return _head->_next;code>
因为这里返回的是一个节点的指针c;节点的指针是可以隐式类型转换成iterator的c;color:#fe2c24">单参数的构造函数支持隐式类型的转换
end函数同理:
<code class="language-cpp"> iterator end() { return _head->_prev; }code>
到这里c;我们来测试一下:
<code class="language-cpp"> list<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3); lt.push_back(4); list<int>::iterator it = lt.begin(); while (it != lt.end()) { cout << *it << " "; ++it; } cout << endl;code>
c="https://i-blog.csdnimg.cn/direct/b61a540d6a6841699adb6d3ead7fd419.png" width="1972" />
这里迭代器的设计相当完美c;假设链表为空c;此时就只有一个哨兵位的头节点。由于begin是_head的nextc;此时它指向的还是自己。end是_headc;color:#fe2c24">也同样指向自己c;此时it = begin() == endcolor:#fe2c24">在条件判断的时候就不会进入循环c;造成非法访问
我们在实现迭代器的时候没有写拷贝构造函数c;这就意味着指针在与指针的拷贝的时候进行的是浅拷贝c;那么浅拷贝会不会出现问题呢?
答案是不会c;我们就以上面的测试代码为例c;我们给it赋了头节点c;此时我们期望的就是it也指向原来的头节点c;color:#fe2c24">就是需要浅拷贝c;而不是开一个新的链表c;指向新的链表中的头节点
这里的迭代器只是一个color:#fe2c24">访问和遍历链表的工具c;它不像vector、string等容器一样c;它所指向的资源并不是属于它自己的c;它指向的资源是属于list的。所以它也不要写析构函数c;它不能去释放list的空间
insert函数用于在pos位置之前插入数据c;要想实现这个功能还是比较简单的c;仅需要通过简单的修改指针指向即可:
<code class="language-cpp"> void insert(iterator pos, const T& x) { Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(x); newnode->_next = cur; cur->_prev = newnode; newnode->_prev = prev; prev->_next = newnode; }code>
可以通过color:#fe2c24">复用insert函数完成这两个函数:
<code class="language-cpp"> void push_back(const T& x) { insert(end(), x); } void push_front(const T& x) { insert(begin(), x); }code>
erase函数用于color:#fe2c24">删除pos位置的节点c;这个函数的实现也很简单c;也是只需要修改指针指向再释放节点即可:
注意erase不能删除哨兵位的头节点c;在这里加上assert断言
<code class="language-cpp"> void erase(iterator pos) { assert(pos != end()); Node* prev = pos._node->_prev; Node* next = pos._node->_next; prev->_next = next; next->_prev = prev; delete pos._node; }code>
通过color:#fe2c24">复用erase即可实现这两个函数:
<code class="language-cpp"> void pop_back(iterator pos) { erase(--end()); } void pop_front(iterator pos) { erase(begin()); }code>
因为链表很多接口的实现非常简单c;这里就没有把所有接口的实现代码一一列举出来了c;下一节我们接着分析链表c;我们将会分析迭代器失效。那么本节的内容就到此结束了c;谢谢您的浏览!!!!!!!!!
c="https://i-blog.csdnimg.cn/direct/7d4abdba29234498a817a623b7fafec5.png" width="300" />