《链表的优雅封装:C++ list 模拟实现与迭代器之美》
list的优缺点
list的底层是双向链表,相比于之前底层是数组的string,vector,list的缺点是无法通过位置来直接访问元素,因为内存分配不连续;以及底层的迭代器需要封装指针才能实现++,*等逻辑的正确性。优点是 当已知插入/删除位置的迭代器,进行插入,删除元素时仅需改变前后指针的指向;以及list迭代器稳定,插入新节点时不必移动已有节点,迭代器不会失效。
对list双向链表进行拆分成三类,
第一类list_node
,链表中的节点list_node,包括本身存储的数据,以及指向下一个,上一个 节点的指针
本身存储的数据类型是T,创建一个模板,
template<class T>
struct list_node//链表中的每个节点
{T _data;list_node<T>* _prev;list_node<T>* _next;
};
第二类list
,链表本身,包含多个节点list_node,以及链表中节点的个数_size
此时节点仅仅维护一个哨兵位_head,剩下的节点利用push_back等方法进行插入
template<class T>
class list
{
private:list_node<T>* _head;size_t _size;
};
第三类一会再说,先进行节点插入到链表的操作,实现push_back,
namespace sxm
{template<class T>struct list_node//链表中的每个节点{T _data;list_node<T>* _prev;list_node<T>* _next;list_node(const T& data)//节点的构造函数:_data(data),_prev(nullptr),_next(nullptr){ }};template<class T>class list{public:void push_back(const T& x){list_node<T>* newnode = new list_node<T>(x);//先创建一个节点,并将存储的元素x传入,这时候//就要对节点弄构造函数(第一个类),用struct方便访问//接下来对节点进行链接,tail newnode _head list_node<T>* tail = _head->_prev;tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;++_size;}list()//链表的构造函数{_head = new list_node<T>(T()); //创建一个_head的哨兵节点,注意!_head->_next = _head;_head->_prev = _head;_size = 0;}private:list_node<T>* _head;size_t _size;};void test(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);list<int>::iterator it = a.begin();while (it != a.end()){cout << *it << " ";++it;}cout << endl;}
}
注意,list链表的构造函数是先创建一个对象list_node,不能写成_head = new list_node(),而是要写成_head = new list_node(T()); 需要调用list_node的构造函数,而list_node中如果没有无参的构造函数,因为list_node一般要初始化_data,有参,会编译失败。这里不推荐list_node的无参构造,会导致_data的未定义行为
而T () 会创建一个T类型的临时对象,并调用T的默认构造函数,然后这个临时对象会作为list_node构造函数的参数,传递给 list_node(const T& val)
,用于初始化节点的_data
_head = new list_node<int>(int());
// 等价于:new list_node<int>(0),因为 int() 会初始化为 0_head = new list_node<string>(string());
// 调用 string 的默认构造函数,初始化 data 为空字符串
那这时如何用迭代器来遍历链表呢?
第三类,迭代器类(非const类)
,模拟指针行为,封装节点指针
为什么要有这一类?
拿刚刚的迭代器遍历来说,对节点指针进行解引用得到的是节点本身,拿不到节点里面存储的数据;对节点指针进行++,得不到下一个节点,节点在内存中存储是不连续的。
迭代器的本质是对遍历行为的抽象,让用户可以忽略底层的差异,用相同的方法遍历数组,链表等结构。
因此进行封装指针,重载++,*等运算符,提供begin(),end()等方法,供外部使用
先实现一个迭代器模板
template<class T>
struct list_iterator
{typedef list_node<T> Node;Node* _node;
};
迭代器中的成员变量是一个当前节点指针,存储当前迭代器指向的节点,可以进行向前移动(_node>_prev),向后移动(_node->_next),访问当前元素(_node->_data),通过这一个链表节点对运算符重载,来达到我们想要的目的
template<class T>struct list_iterator{typedef list_node<T> Node;Node* _node;Node* operator++(){_node = _node->_next;return _node;}};
可以这样写吗?返回一个list_node *_node?
不可以,因为这样会返回的是 节点指针,如果再次对这个指针进行++操作,不会是下一个指针,即无法再对迭代器进行后续操作。
将返回类型改成迭代器类型,返回值应返回迭代器本身,而不是结点指针
template<class T>struct list_iterator{typedef list_node<T> Node;Node* _node;list_iterator<T>& operator++(){_node = _node->_next;//迭代器的内部状态被修改,指向下一个节点return *this;//为外部提供接口}list_iterator(Node *node)// // 接受节点指针的构造函数,允许从节点指针构造迭代器:_node(node){}};
同理operator--
list_iterator<T>& operator--()
{_node = _node->_prev;//更新内部迭代器return *this;//返回给外部更新后的迭代器
}
对*进行运算符重载
,直接对节点指针进行解引用得到的是节点本身,而非节点内存储的数据
T& operator*()
{return _node->_data;
}
以及operator!=/operator==
bool operator!=(const list_iterator<T>& s){return _node != s._node;}bool operator==(const list_iterator<T>& s){return _node == s._node;}
接着进行插入在list类中实现迭代器begin(),end()
template<class T>
class list
{typedef list_node<T> Node;
public:typedef list_iterator<T> iterator;iterator begin(){iterator it = _head->_next;//调用 iterator 的构造函数创建一个临时对象,再将这个临时对象拷贝到 it 中return it;//return iterator(_head->_next);//直接调用迭代器的构造函数,创建一个临时的匿名迭代器对象//return _head->next;}iterator end(){iterator it=_head;return it;//最后一个元素的下一个位置}list(){_head = new list_node; //创建一个_head的哨兵节点_head->_next = _head;_head->_prev = _head;_size = 0;}private:list_node<T>* _head;size_t _size;
};
此时第三种写法return _head->_next;原本的类型是list_node<T>*,但函数的返回值类型是iterator,编译器会在list_iterator<T>
类中查找可以接收 list_node<T>*
作为参数的构造函数,会自动调用这个构造函数构造一个临时的 list_iterator<T>
对象(head->_prev
作为参数),返回一个转换后的iterator
对list进行完善
insert
在list类中实现指定位置之前的插入insert
void insert(iterator pos, const T& x)//prev newnode cur{Node* newnode = new Node(x);Node* cur = pos._node;Node* prev= pos._node->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;}
push_back
那这时,push_back可以变为
void push_back(const T& x)
{insert(end(), x);
}
push_front
push_front,在开头增加一个新节点
void push_front(const T& x){insert(begin(), x);//_head->_next之前插入,即_head的下一个节点}
erase
erase的实现,从list中删除某个节点信息
void erase(iterator pos)// pos._node->_prev pos->_node pos._node->next{Node* prev = pos._node->_prev;Node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;--_size;}
pop_back
接着实现pop_back,删除链表中最后一个节点
void pop_back(){erase(--end());//_head->_prev}
pop_front
pop_front,删除第一个有效节点
void pop_front()
{erase(begin());//_head->_next
}
size
以及size
size_t size(){return _size;}
operator++(int),operator--(int)
后置--/++,返回值而不是迭代器的引用,因为核心是操作“先使用自增前的迭代器,再进行自增”,应该先返回自增前的值
list_iterator<T> operator--(int)
{list_iterator<T> temp = *this;//this 是一个指向当前对象的指针list_iterator<T>* ; *this 代表的是当前的 list_iterator<T> 对象、
//浅拷贝就行,让新的迭代器 _node 指向同一个节点_node = _node->_prev;return temp;
}list_iterator<T> operator++(int)
{list_iterator<T> temp = *this;_node = _node->_next;return temp;
}
我们看下面一段代码,我们定义了一个AA类类型,如果不重载流运算符<<的话,需要写成(*ita)._aa1才能插入到流中
struct AA{int _aa1 = 1;int _aa2 = 1;AA(int aa1,int aa2):_aa1(aa1),_aa2(aa2){ }};void test(){list<AA> lta;lta.push_back(AA(1, 1));lta.push_back(AA(2, 2));lta.push_back(AA(3, 3));lta.push_back(AA(4, 4));list<AA>::iterator ita = lta.begin();while (ita != lta.end()){cout << (*ita)._aa1 << ":" << (*ita)._aa2;
//ita是一个迭代器,lta->_data=AA,即*ita 返回的是 AA&,这个元素是一个 AA 类型的对象,
//而对象需要通过 . 运算符访问其内部成员,如_a1,_a2++ita;}cout << endl;}
还有一种方法,用箭头运算符(ita->_aa1
)访问。不过ita是迭代器类型,为了让迭代器像指针一样用 ->
访问元素成员,必须通过重载 operator->
来实现
operator->
步骤1:迭代器的 operator->
必须返回一个指针(如 AA*
)
步骤2:编译器会用这个指针再次调用 ->
访问 _aa1
成员。
当写 ita->_aa1
时,编译器会自动展开为 (ita.operator->())->_aa1
。但为了可读性,省略了后一个->
所以返回值应该是T*,&表示取地址
T* operator->()
{return &(_node->_data);//返回 _node->_data 这个对象的内存地址,结果是 T* 类型的指针
}
这样就可以进行->访问了
cout << ita->_aa1 << ":" << ita->_aa2<<" ";
接下来我们看这样一段代码
//打印
template<class T>
void Print(const list<T>& v)
{for (auto it = v.begin(); it != v.end(); ++it){cout << *it << " ";}cout << endl;
}
v是const容器时,表示容器里面的数据不能修改,调用v.begin(),应该返回一个const迭代器,但我们之前只实现了普通迭代器,v.begin()返回普通迭代器iterator,auto it自动推导成普通迭代器类型,编译会报错,v.begin()应该返回一个const_iterator类型的迭代器,保证不能修改。接下来我们进行const迭代器的操作。
刚刚介绍了非const迭代器,成员函数operator*/operator-> 的返回值都可以被修改;而非const迭代器要求不能通过迭代器修改元素
那我们是新建一个const_iterator类型,还是在iterator的基础上+const 呢?const iterator
const iterator:指迭代器本身不能修改
const_iterator:指向内容不能修改
我们想要的非const迭代器是 指向的内容不能修改,而不是迭代器本身不能修改,因为迭代器需要进行++/--等操作进行移动,移动到下一个节点,因此定义一个const_iterator 迭代器
const迭代器只需要将operator*/operator-> 的返回值前面+const,表示返回值的指向内容不能修改
const迭代器
template<class T>struct list_const_iterator{typedef list_node<T> Node;typedef list_const_iterator<T> Self;Node* _node;list_const_iterator(Node* node):_node(node){}Self& operator++(){_node = _node->_next;//更新迭代器内部存储的节点指针return *this;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self temp = *this;_node = _node->_prev;return temp;}Self operator++(int){Self temp = *this;_node = _node->_next;return temp;}const T& operator*()//返回你的别名,但返回的是const别名,将权限缩小了之后再返回,修改不了{return _node->_data;}const T* operator->(){return &_node->_data;//返回const指针}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}};
begin,end
以及const迭代器的begin,end
template<class T>
class list
{typedef list_node<T> Node;
public:typedef list_iterator<T> iterator;typedef list_const_iterator<T> const_iterator;iterator begin(){iterator it = _head->_next;return it;}iterator end(){return _head;}//const迭代器const_iterator begin()const{const_iterator it = _head->_next;return it;}const_iterator end()const{const_iterator it = _head;return it;}
};
返回 const_iterator
解决的是 “迭代器能否修改元素” 的问题,而成员函数加 const
解决的是 “const
容器能否调用这个函数” 的问题。
但是这两类迭代器const与非const大部分代码都相似,单独写两个类太麻烦了,因此在这两个类的基础上再实现一个类,同一个类模板,增加两个模板参数来控制。
template<class T,class Ref,class Ptr>
struct list_iterator
{typedef list_node<T> Node;
//T表示迭代器的元素类型,Ref表示operator*的返回值,T& or const T&,Ptr表示的是operator->的返回值,T* or const T*typedef list_iterator<T, Ref, Ptr> Self;Node* _node;list_iterator(Node* node):_node(node){}Self& operator++(){_node = _node->_next;//更新迭代器内部存储的节点指针return *this;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self temp = *this;_node = _node->_prev;return temp;}Self operator++(int){Self temp = *this;_node = _node->_next;return temp;}Ref operator*()//返回你的别名返回的是const别名,or 非const别名{return _node->_data;}Ptr operator->(){return &_node->_data;//返回const指针}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{typedef list_node<T> Node;public:
//通多指定不同的Ref 和 Ptr,让同一个list_iterator模板实例化出两种迭代器类型typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;iterator begin(){iterator it = _head->_next;return it;}iterator end(){return _head;}//const迭代器const_iterator begin()const{const_iterator it = _head->_next;return it;}const_iterator end()const{const_iterator it = _head;return it;}
接下来看迭代器失效的问题
1.insert
不会引起迭代器失效的问题,每个节点是单独储存的,在pos节点之前插入新节点,但本身迭代器pos指向的节点_node仍然不变。
规范写的话,insert加上返回值,返回新插入节点的迭代器
iterator insert(iterator pos, const T& x)//prev newnode cur{Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = pos._node->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return newnode;//隐式类型转换}
2.erase
void erase(iterator pos)
被删除节点的迭代器pos会失效,pos->_node在函数结束后会被释放,但形参仍保留着pos迭代器的地址,如果进行++等操作,操作对象就是野指针。
比如删除一个偶数
void test()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);Print(lt);list<int>::iterator it = lt.begin();while (it != lt.end()){if (*it % 2 == 0){lt.erase(it);}it++;//it是野指针}
}
这样我们可以将erase函数设置一个返回值,返回下一个节点的迭代器。
iterator erase(iterator pos)// pos._node->_prev pos->_node pos._node->next{Node* prev = pos._node->_prev;Node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;--_size;return next;}
void test()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);Print(lt);list<int>::iterator it = lt.begin();while (it != lt.end()){if (*it % 2 == 0){it=lt.erase(it);}else{it++;}}Print(lt);
}
但其他迭代器不受影响,因为其他迭代器指向的节点地址都不会被改变。
继续完善list类
~list与clear
~list()//将节点一个一个的释放{clear();delete _head;_head = nullptr;}void clear()//不清除哨兵位{auto it = begin();while (it != end()){it=erase(it);//it接收删除后的下一个迭代器}}
实现深拷贝
//lt2(lt1),this是list2,lt1是list1list(const list<T>& lt){for (auto& e : lt){push_back(e);//遍历原链表的每个元素,为每个节点创建新空间,}}
不可以这样写,push_back的前提是得有一个哨兵位,使得_head不为nullptr,否则_head是一个野指针,无法进行插入
因为调用的是拷贝构造list<int>lt1(lt2),构造函数不会再调用其他构造函数,拷贝构造也是构造函数,
因此我们委托构造,让拷贝构造先调用默认构造的逻辑,实现空链表的初始化
void empty_init()
{_head = new list_node<T>(T()); //创建一个_head的哨兵节点_head->_next = _head;_head->_prev = _head;_size = 0;
}//lt2(lt1),this是list2,lt1是list1
list(const list<T>& lt)
{empty_init();for (auto& e : lt){push_back(e);}
}
以及赋值的深拷贝
//lt1=lt3
list<T>& operator=(list<T> lt)//lt是lt3的拷贝(拷贝构造函数创建的深拷贝),lt与lt3是两个完全独立的链表
{swap(lt);//交换这个深拷贝lt 和 lt1return *this;
}
void swap(list<T>& lt)
{std::swap(_head, lt._head);//交换两个变量的值std::swap(_size, lt._size);
}
函数结束后,lt3 的副本 lt 也会被销毁。
initializer_list初始化链表
C++11支持initializer_list初始化链表,允许使用花括号 {}
传递一组同类型的值。
initializer_list<int> lt = {1,2,3};
用initializer_list<T>类型,是为了支持花括号 {}
初始化
list(initializer_list<T> il){empty_init();for (auto& e : il){push_back(e);}}
initializer_list<int> lt1({ 1,2,3 });
当写 list<int> lt = {1,2,3};
时,编译器会自动将 {1,2,3}
隐式类型转换为initializer_list<int>
类型的临时对象,然后调用参数类型为initializer_list<T>
的构造函数,用这个临时的对象初始化lt1。
list的迭代器与vectoe的迭代器的对比
list进行插入,迭代器不会失效。
std::list<int> l = {1, 3, 4};
auto it = ++l.begin(); // it 指向 3(第二个节点)l.insert(it, 2); // 在 3 前面插入 2,链表变为 {1,2,3,4}
// it 仍然指向 3(节点地址未变),*it 结果仍为 3(有效)
而vector进行插入,有时会触发扩容,将原有元素全部复制到新内存中,导致指向旧内存的原迭代器失效
std::vector<int> v = {1, 3, 4};
v.reserve(3); // 固定容量为 3(避免自动扩容,仅演示移动导致的失效)
auto it = ++v.begin(); // it 指向 3(第二个元素)v.insert(it, 2); // 插入后变为 {1,2,3,4},原 3、4 向后移动
// it 原本指向旧位置的 3,但插入后该位置变为 2,it 现在指向 2(失效,因为指向的元素变了)
扩容
std::vector<int> v = {1, 2, 3};
auto it = v.begin(); // 指向 1(旧内存地址)v.push_back(4); // 若容量不足,触发扩容(分配新内存)
// it 指向旧内存(已释放),彻底失效
进行删除操作时,list中被删除的节点会失效,其他迭代器仍然有效
std::list<int> l = {1, 2, 3, 4};
auto it1 = l.begin(); // 指向 1
auto it2 = ++l.begin(); // 指向 2(准备删除的节点)
auto it3 = ++++l.begin(); // 指向 3l.erase(it2); // 删除节点 2,链表变为 {1,3,4}
// it2 失效(指向已释放的节点)
// it1 仍指向 1,it3 仍指向 3(均有效)
而vector进行删除时,删除位置后的元素向前挪动,进行覆盖,被删除元素的迭代器失效,以及被删除之后的元素的迭代器也会失效
std::vector<int> v = {1, 2, 3, 4};
auto it1 = v.begin(); // 指向 1
auto it2 = ++v.begin(); // 指向 2(准备删除的元素)
auto it3 = ++++v.begin(); // 指向 3v.erase(it2); // 删除 2 后,3、4 向前移动,变为 {1,3,4}
// it2 失效(指向被删除的位置)
// it3 原本指向 3,但移动后 3 的位置变了,it3 现在指向的是原 4 的位置(值为 4),失效