温馨提示:这篇文章已超过454天没有更新,请注意相关的内容是否还可用!
摘要:,,本文简要介绍了STL中的list容器的模拟实现。通过模拟实现list容器,可以更好地理解其内部原理和机制。模拟实现包括节点的创建、插入、删除、遍历等基本操作。还涉及到了内存管理、动态分配等重要方面。通过模拟实现,可以加深对STL中list容器的认识,提高编程能力和效率。
目录
结点类的模拟实现
迭代器类的模拟实现
构造函数
前置++与后置++
前置- -与后置 - -
== 与 !=运算符重载
* 运算符重载
-> 运算符重载
普通迭代器总体实现代码
list类的实现
list类的成员变量
构造函数
迭代器
insert()
erase()
push_front/push_back/pop_front/pop_back
front/back
clear()
empty()
swap()
拷贝构造函数
赋值运算符重载
析构函数
结点类的模拟实现
list 的底层结构为带头双向循环链表,所以结点类里的成员变量:
T _data(数据),ListNode* _prev(前驱指针),ListNode* _next(后继指针);
成员函数只需要构造函数即可(指针初始化为nullptr,数据以缺省参数的形式进行初始化);
template struct ListNode { //结点中的成员变量 T _data;//数据 ListNode* _prev;//前驱指针 ListNode* _next;//后继指针 //结点类的构造函数 ListNode(const T& val=T()) : _data(val) , _prev(nullptr) , _next(nullptr) {} };
迭代器类的模拟实现
迭代器并不关心容器底层的数据结构为顺序表、链表或者树型结构,提供统一的方式访问、修改容器中的数据并且遍历区间为左闭右开[begin,end);
vector与string的模拟实现中,迭代器均为原生指针,是因为vector和string底层物理空间连续(顺序表),那么list可否采用原生指针(结点指针)作为迭代器?
思考一:
若it为结点指针,it++,能否从链表的当前位置指向链表当前位置的下一位置? (×)
思考二:
若it为结点指针,*it,能否得到链表结点中的数据_data?(×)
解决方案:
采用原生指针作为list的迭代器均不满足需求,运算符重载可以对已有的运算符重新进行定义,赋予其另一种功能,从而满足需求,但是运算符重载的前提为自定义类型,而指针本身为内置类型,只能将结点指针封装为自定义类型,从而使用运算符重载满足需求;
迭代器的成员变量 : Node* _node;(_node为指向结点的指针)
迭代器的成员函数 : 运算符重载函数;
template //使用struct关键字封装结点指针,方便访问数据成员_node //若使用class关键字封装节点指针,需要提供函数接口访问_node struct __List_iterator { typedef ListNode Node; Node* _node; };
构造函数
//__List_iterator it(); //需要结点指针对_node进行有参构造且不能给定缺省值为nullptr, //否则解引用操作导致系统崩溃 //__List_iterator(Node* node=nullptr)(×) //因此迭代器遍历链表时必须给定有效参数,参数为结点指针; __List_iterator(Node* node) :_node(node) {}
思考:迭代器内部需要实现析构函数,拷贝构造函数吗?
1. 提供链表的结点指针给迭代器,方便迭代器访问链表结点,并不需要释放结点;
而且对于内置类型(指针)成员变量,编译器自动生成的析构函数不做任何处理;
2. 将一个迭代器拷贝给另一个迭代器,只需要两个迭代器指向同一个链表结点,
而编译器自动生成的拷贝构造函数实现了浅拷贝,所以不需要实现拷贝构造函数;
前置++与后置++
前置++,this指针出作用域销毁,但是this指针指向的对象在函数结束不会被销毁,以传引用的方式返回以提升效率;
//++it __List_iterator& operator++() { _node = _node->_next; return *this; }
返回类型太长,使用typedef重定义类型名;
typedef __List_iterator self; self& operator++() { _node = _node->_next; return *this; }
C++规定:
后置++重载时多增加一个int类型的参数,但调用函数时参数不需要传递,编译器自动传递;
后置++,tmp为临时拷贝对象,出作用域销毁,只能以传值的方式返回;
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; }
== 与 !=运算符重载
==运算符重载比较两个迭代器对象的_node指针指向是否相同;
bool operator==(const self& s) { return _node == s._node; }
!=运算符重载比较两个迭代器对象的_node指针指向是否相同;
bool operator!=(const self& s) { return _node != s._node; }
* 运算符重载
重载 * 运算符的目的是为了得到迭代器对象的_node指针所指向的数据;
T& operator*() { return _node->_data; }
-> 运算符重载
struct Date { int _year = 2024; int _month = 1; int _day = 1; }; int main() { //链表中的数据成员为自定义类型Date list lt; Date d1; lt.push_back(d1); //it为封装结点指针的迭代器类 list::iterator it = lt.begin(); //结点中的数据成员访问方式1: 结构体变量.结构体成员 cout _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) { return _node == s._node; } bool operator!=(const self& s) { return _node != s._node; } T& operator*() { return _node->_data; } T* operator->() { return &_node->_data; } };
无论是普通迭代器还是const迭代器,均需要迭代器遍历容器中的内容,因此迭代器本身可以被修改,区别仅在于迭代器指向的内容是否可以被修改,那么该如何实现const迭代器类呢?
由于const迭代器本质为保护迭代器指向的内容不允许被修改,若实现const迭代器类,只需要普通迭代器的operator*()与operator->()两个接口的返回值采用const修饰,便保护容器中的内容不会被修改,其余接口均保持不变;
template struct __List_const_iterator { typedef ListNode Node; Node* _node; __List_const_iterator(Node* node) :_node(node) {} typedef __List_const_iterator self; self& operator++(); self operator++(int); self& operator--(); self operator--(int); bool operator==(const self& s); bool operator!=(const self& s); const T& operator*() { return _node->_data; } const T* operator->() { return &_node->_data; } };
上述实现方案,在同一份文件中存在普通迭代器类与const迭代器类,两者之间仅有两个接口的返回值不同,如此便造成了代码的冗余,导致可读性变差,那么该如何改进呢?
迭代器类增加两个模版参数,使用时便可实例化出普通迭代器与const迭代器;
//迭代器实现最终总体代码,只给出函数声明与普通迭代器代码相同 template struct __List_iterator { typedef ListNode Node; Node* _node; __List_iterator(Node* node) :_node(node) {} typedef __List_iterator self; self& operator++(); self operator++(int); self& operator--(); self operator--(int); bool operator==(const self& s); bool operator!=(const self& s); Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } };
当在list类中定义两个迭代器类,普通迭代器类,const迭代器类(Ref为 T&/const T& 类型,Ptr为 T*/const T* 类型)
typedef __List_iterator iterator; typedef __List_iterator const_iterator;
当使用普通迭代器对象时,实例化出普通迭代器类(iterator),使用const迭代器对象时,实例化出const迭代器类(const_iterator) ;
list类的实现
list类的成员变量
list类的成员变量只需要一个头结点,便可通过迭代器访问其他节点元素;
template class list { typedef ListNode Node; public: typedef __List_iterator iterator;//重命名普通迭代器 typedef __List_iterator const_iterator;//重命名const迭代器 private: Node* _head; };
构造函数
list() { _head = new Node; // 申请一个头结点 _head->_next = _head; // 后继指针指向自己 _head->_prev = _head; // 前驱指针指向自己 }
迭代器
begin() : 构造出指针指向第一个结点的迭代器对象;
end() : 构造出指针指向头结点的迭代器对象;
iterator begin() { //return iterator(_head->_next); //单参数的构造函数支持类型转换__List_iterator(Node* node) //支持Node* 转换为 迭代器对象 return _head->_next; } iterator end() { return _head; } const_iterator begin() const { return _head->_next; } const_iterator end() const { return _head; }
insert()
- 新开辟一个结点newnode(值为val),得到当前结点的指针,前驱结点的指针;
- 前驱结点的_next 指向 newnode,newnode的_prev指向前驱结点;
- newnode的_next 指向当前结点,当前结点的_prev指向newnode;
iterator insert(iterator pos, const T& 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; //return iterator(newnode); return newnode; }
erase()
- 得到前驱结点和后继结点的指针;
- 前驱结点的_next 指向后继结点;
- 后继结点的_prev指向前驱结点;
- 删除当前结点,返回删除位置的下一个位置;
iterator erase(iterator pos) { assert(pos != end());//不能删除空双向循环链表 Node* cur = pos._node;//当前结点指针 Node* prev = cur->_prev;//前驱结点指针 Node* next = cur->_next;//后继结点指针 prev->_next = next; next->_prev = prev; delete cur; return next;//返回删除位置的下一位置 }
push_front/push_back/pop_front/pop_back
push_back : 尾插即在头结点前插入一个结点;
pop_back : 尾删,删除最后一个结点;
push_front : 头插即在第一个结点前(非头结点)插入一个结点;
pop_front : 头删,删除第一个结点(非头结点);
void push_back(const T& x) { insert(end(), x); } void push_front(const T& x) { insert(begin(), x); } void pop_back() { erase(--end()); } void pop_front() { erase(begin()); }
front/back
front() : 返回第一个结点数据的引用;
end() : 返回最后一个结点数据的引用;
T& front() { //*begin-->T& operator*() return *begin(); } const T& front()const { return *begin(); } T& back() { return *(--end()); } const T& back()const { return *(--end()); }
clear()
双向循环链表只保留头结点,遍历链表时调用erase接口进行删除,注意调用erase后迭代器it已经失效,使用返回值接收,自动指向下一结点;
void clear() { iterator it = begin(); while (it != end()) { it = erase(it); } }
empty()
//判断容器是否非空 bool empty()const { return begin() == end(); }
swap()
//交换容器的头指针 void swap(list& tmp) { std::swap(_head, tmp._head); }
拷贝构造函数
- 申请一个头结点,构造空双向循环链表(新容器);
- 将 lt 中的数据拷贝到新构造的容器中;
list(const list& lt) { _head = new Node; // 申请一个头结点 _head->_next = _head; // 后继指针指向自己 _head->_prev = _head; // 前驱指针指向自己 for (const auto& e : lt) // 拷贝到新构造的容器中 { push_back(e); } }
赋值运算符重载
传统写法:
- 释放除了头结点以外的所有结点;
- 将 lt 中的数据拷贝到新构造的容器中;
list& operator=(const list& lt) { // 防止自己给自己赋值 if (this != <) { clear(); // 清空数据 for (const auto& e : lt) // 拷贝到新构造的容器中 { push_back(e); } } return *this; // 支持连续赋值 }
现代写法:
- 拷贝构造出 lt 对象;
- 交换 this 和 lt 的 _head 指针,出了作用域,lt 调用析构函数,释放掉原this的结点;
list& operator=(list lt) //拷贝构造lt对象 { std::swap(_head, lt._head); //交换指针 return *this; //支持连续赋值 }
析构函数
- 使用 clear() 释放除了头结点以外的结点;
- 释放掉头结点;
~list() { clear(); delete _head; _head = nullptr; }
欢迎大家批评指正,博主会持续输出优质内容,谢谢大家观看,码字不易,希望大家给个一键三连支持~ 你的支持是我创作的不竭动力~
还没有评论,来说两句吧...