1.普通迭代器(补充)
1.1 后置++和后置--
我们迭代器里面实现了前置++和前置--,还需要实现后置++和后置--。
在list.h文件的list_iterator类里面实现。
//后置++/--
Self& operator++(int)
{
Self tem(*this);//保存原来的值
_node = _node->_next;
return tem;
}
Self& operator--(int)
{
Self tem(*this);//保存原来的值
_node = _node->_prev;
return tem;
}
1.2 重载->
因为迭代器模拟的是指针的行为,所以这里我们还要重载一下->。
T* operator->()
{
return &_node->_data;
}
这个->的应用场景如下。
现在有个AA的类或结构体。
struct AA
{
int _a = 1;
int _b = 2;
};
list里面存这个AA类型的数据。
void test5()
{
list<AA> la;
}
然后给里面随便存几个数据。
list<AA> la;
la.push_back(AA());
la.push_back(AA());
la.push_back(AA());
la.push_back(AA());
再用迭代器遍历,此时就要用->来引出AA里面的_a或者_b,不能用.引出。
list<AA>::iterator ia = la.begin();
while (ia != la.end())
{
cout << ia->_a << " " << ia->_b << endl;
++ia;
}
完整地写法如下。
cout << ia.operator->()->_a << " " << ia.operator->()->_b << endl;
第一个箭头是运算符重载的->,第二个_a前面的->就是没有重载的解引用符号。
为了可读性,省略了一个箭头,这里省略的也是第二个->。
2.const迭代器
2.1 const_iterator
const修饰迭代器,我们不可以对迭代器指向的内容进行修改,但是迭代器可以改变自己的指向。
我们如何实现迭代器可以改变自己指向而不让改变指向内容?
我们修改迭代器指向的内容通过什么修改?通过迭代器类里面的两个运算符重载,operator*和operator->, 如果我们现在把operator*和operator->的返回值用const修饰,就不能通过他们两个修改指向内容了。
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
但是我们不可以直接把list_iterator里面的这两个成员函数给改了,改了的话普通迭代器就不能改了,我们只要const迭代器不能改。所以,我们重新实现一个类,叫list_const_iterator,这个类除了前面两个运算符重载函数变,其他成员函数一律不变。
在list.h文件的namespace里实现。
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)
{}
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
//前置++/--
Self& operator++() //重载++
{
_node = _node->_next;//加到下一个节点
return *this;//返回自己
}
Self& operator--() //重载--
{
_node = _node->_prev;//减到前一个节点
return *this;//返回自己
}
//后置++/--
Self& operator++(int)
{
Self tem(*this);//保存原来的值
_node = _node->_next;
return tem;
}
Self& operator--(int)
{
Self tem(*this);//保存原来的值
_node = _node->_prev;
return tem;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
};
同时,我们要在list类里面,public部分加上下面这句话。
typedef list_const_iterator<T> const_iterator;
以及迭代器的begin和end接口。
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
const迭代器部分的测试我们和接下来要说的打印函数一起测。
2.2 print_container 打印
这个函数就是为了方便我们打印数据。
template<class container>
void print_container(const container& c)
{
for (auto e : c)
{
cout << e << " ";
}
cout << endl;
}
在test.cpp中对const迭代器和print_container一起测试。
void test6()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.print_container(lt);
}
3.优化两种迭代器的实现
很容易发现,我们前面的实现方式代码过于冗余,list_iterator 和 list_const_iterator两个类重叠部分太多。于是,我们就需要用到类模板,来优化一下。
在list_iterator类的基础上优化。
我们增加两个模板参数,Ref和Ptr。
Ref是引用,或者是const引用;Ptr是地址,或者const地址。
然后把operator*返回值类型换成Ref,operator->返回值类型换成Ptr。
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
list类里面的如下部分也要改。
就可以了,迭代器的类模板就实现好了。
我们用前面的测试样例在test.cpp测试一下。
4.迭代器失效问题
list的迭代器失效在insert插入数据是按理来说应该是不会发生的,因为这里没有扩容的的问题。
但是删除数据就有迭代器失效的问题,迭代器失效问题更加详细具体的分析在【C++】vector模拟实现、迭代器失效问题(超详解)
list这里如果删除一个节点,迭代器绝对就是类似野指针了。
解决方案还是对erase进行改造,让erase有返回值,erase返回的是删除结点的下一个节点位置。
iterator erase(iterator pos)
{
assert(pos != end());//不可以删哨兵位头节点
Node* prev = pos._node->_prev;//存pos前后节点
Node* next = pos._node->_next;
prev->_next = next;//链接pos前后节点
next->_prev = prev;
delete pos._node;//释放pos节点
--_size;
return next;
}
在test.cpp中测试一下。我们删除所有偶数。
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
auto it = lt.begin();
while (it != lt.end())
{
if (*it % 2 == 0)
{
it = lt.erase(it);
}
else
{
++it;
}
}
lt.print_container(lt);
所以list的迭代器失效问题就简单很多。
但是为了和库里面保持一致,我们还是对insert也改进一下,让他也有返回值。
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
//连接起来
newnode->_next = cur;
newnode->_prev = prev;
prev->_next = newnode;
cur->_prev = newnode;
++_size;
return newnode;//返回插入的节点
}
5.析构函数
5.1 clear
清空所有数据,但是不清除哨兵位头节点。
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
5.2 析构函数
析构函数可以套用clear,clear不清除哨兵位,析构这里另外清除一下就可以了。
~list()
{
clear();
delete _head;
_head = nullptr;
}
6.构造函数(优化)
6.1 空链表初始化
我们重新写一个函数,用来初始化空的list。空的list就是只有哨兵位头节点。
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
这个代码和list默认构造代码一模一样。这样实现主要是为了方便后面的拷贝构造。
6.2 默认构造
我们现在默认构造的代码就直接复用前面写的empty_init。
list()
{
empty_init();
}
6.3 拷贝构造
这里的拷贝构造依然是复用push_back实现。
list(const list<T>& lt)
{
for (auto& e : lt)
{
push_back(e);
}
}
比如说我们用lt1拷贝构造lt2
但是此时的lt2连哨兵位都没有,这样写肯定有问题。所以我们要加上前面写的empty_init。
//lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
在test.cpp里对前面的三个一起测试。
void test8()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
list<int> lt2(lt1);//拷贝构造
lt1.print_container(lt1);
lt2.print_container(lt2);
}
7.operator= 赋值运算符重载
这里的赋值运算符重载,也是要深拷贝,所以我们还是采用现代写法,现代写法详解在【C++拓展】深拷贝现代写法
首先自己写一个swap。
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
然后再交换。
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
在test.cpp中测试一下。
list<int> lt1, lt2;
lt1.push_back(1);
lt1.push_back(2);
lt2.push_back(3);
lt2.push_back(4);
lt1.print_container(lt1);
lt2.print_container(lt2);
lt2 = lt1; //赋值
lt1.print_container(lt1);
lt2.print_container(lt2);
list的内容就结束了,感谢观看,下篇再见~