list类
list类的难点在于对迭代器的理解,所以其他链表相关的接口,与学习数据结构时的做法基本一致,不再赘述
节点:
struct list_node
{
list_node(const T&val = T())
:_val(val)
,_prev(nullptr)
,_next(nullptr){}
T _val;
list_node<T>* _prev;
list_node<T>* _next;
};
list类:
template<class T>
class list
{
typedef list_node<T> Node;
public:
list() :_head(new Node()) { _head->_prev = _head->_next = _head; }
void push_back(const T& val)
{
Node* newnode = new Node(val);
Node* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
void pop_back()
{
Node* tail = _head->_prev;
tail->_prev->_next = _head;
_head->_prev = tail->_prev;
delete tail;
}
private:
Node* _head;
};
void test1()
{
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);
lt.pop_back();
lt.pop_back();
lt.pop_back();
lt.pop_back();
}
如上,是一个实现了尾插和尾删的链表,实现了基本的插入,如果我们想要遍历和访问链表,就需要迭代器。
对于 string 和 vector,原生的指针就够用了。但是对于list是无法使用原生指针的。
举个例子:
如果我们使用原生指针,然后对节点解引用:
typedef list_node<int> Node
Node* p = new Node(10);
*p //错误
每个Node类中都有三个数据,编译器不知道 *p 要的是哪个数据。
换句话说,我们没有重载 operator*(),明确地告诉编译器,我要的是Node里面的哪个数。
封装,就是在已有事物的基础上,扩展它的功能,原生的节点指针用不了,就对节点指针进行封装,重载运算符。
这也就是为什么list不使用原生指针,而是创造迭代器类,封装节点指针
list_iterator类(迭代器)
list<int> lt;
auto it = lt.begin();
while(it != lt.end())
{
cout << *it << " ";
++it
}
如上代码,为了实现这份代码,我们需要:
- 创建list_iterator类、begin函数 和 end 函数
- it != lt.end() :重载迭代器比较
- *it :重载operator *
- ++it :重载operator++
list_iterator类:
template<class T>
struct __list_iterator
{
typedef list_node<T> Node;
__list_iterator<T>(Node* node) :_node(node){}
typedef __list_iterator<T> iterator;
T& operator*()
{
return _node->_val;
}
iterator& operator++() //前置++
{
_node = _node->_next;
return *this;
}
bool operator==(const iterator& cmp)
{
return _node == cmp._node;
}
bool operator!=(const iterator& cmp)
{
return !(*this == cmp);
}
private:
Node* _node;
};
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T> iterator;
list() :_head(new Node()) { _head->_prev = _head->_next = _head; }
iterator begin()
{
return iterator(_head->_next);//把第一个节点封装成迭代器,然后返回
}
iterator end()
{
return iterator(_head); //把头节点封装成迭代器,然后返回
}
private:
Node* _head;
};
样例:
list<int> lt;
auto it = lt.begin();
while(it != lt.end())
{
*it *= 2; //可修改节点value值
cout << *it << " ";
++it
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vL60Vjco-1679755761151)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918085924327.png)]
const迭代器
使用const迭代器,我们希望节点value值无法被修改
template<class T>
void const_print(const list<T>& lt)
{
list::const_iterator it = lt.begin();
while (it != lt.end())
{
*it += 2;
cout << *it << " ";
++it;
}
}
void test2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
}
最重要的就是,operator*的返回值必须被const修饰,这样就能达到无法修改节点值的效果了。
但是要怎么实现?
T& operator*()
{
return _node->_val;
}
const T& operator*() //错误,不构成重载
{
return _node->_val;
}
第一种方法就是再创建一个类,类名可以叫 const_list_iterator(叫什么其实不重要,因为最后都是直接一个typedef把名字改了,重要的是实现。
template<class T>
sturct const_list_iterator
{
typedef const_list_iterators<T> iterator;
....
const T& operator*()
{
return _node->val;
}
...
Node* node;
};
这样当然是可行的,但是这样,该类的接口与 list_iterator类的接口实现基本一致,无非是多了个const。
代码复用着实有点差。
因此,参照STL源码中list的实现,我们有第二种方法,在了解这种方法之前,我们必须确认对于模板足够了解。
模板是一种泛型编程,使用它意味着你将要描述的东西是模糊的、多样的、不确定的。
因此,不要局限于参数或者类的成员变量,只要是模糊的数据,都应该想到模板!
带着这样的想法,我们再返回去看看,我们需要的是:
operator 的返回值,可能是T&,也可能是const T&*,这取决于我们定义的 list 使用的是什么样的迭代器。
想到了吗,operator*的返回值就是模糊的、不确定的,所以我们何不给返回值设置一个模板呢?
template<class T,class Ref>
struct __list_iterator
{
typedef list_node<T> Node;
__list_iterator<T, Ref>(Node* node) :_node(node) {}
typedef __list_iterator<T, Ref> iterator;
Ref operator*() //返回值为模板参数
{
return _node->_val;
}
}
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&> iterator;
typedef __list_iterator<T, const T&> const_iterator; //const迭代器,就传带const参数
list() :_head(new Node()) { _head->_prev = _head->_next = _head; }
iterator begin()
{
return iterator(_head->_next);//把第一个节点封装成迭代器,然后返回
}
iterator end()
{
return iterator(_head); //把头节点封装成迭代器,然后返回
}
const_iterator begin()const
{
return const_iterator(_node->_next);//用const迭代器封装
}
const_iterator end()const
{
return const_iterator(_node); //用const迭代器封装
}
private:
Node* _head;
};
最后当我们再次编译这段代码时,编译器报错就不允许我们给常量赋值了:
template<class T>
void const_print(const list<T>& lt)
{
auto it = lt.begin();
while (it != lt.end())
{
*it += 2; //错误
cout << *it << " ";
++it;
}
}
void test2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
const_print(lt);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ibcipd5U-1679755761152)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918101744399.png)]
这里还有一些坑,是我自己在学习的时候踩的:
原本当我测试这段代码的时候,我是没有另写一个const_print函数去调用的,也就是这样开头:
void test2()
{
const list<int> lt;
}
但当我试图这样做的时候,很快发现这是没有意义的,虽然语法上是允许的。但是我们在定义的时候就将list以
const修饰,也就是在list一个结点都没有的时候,将它限死不能改变,那既然一个数据都无法保存,创建list的意义在哪?这显然是不合理的。
于是我开始这样写:
void test2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::const_iterator it = lt.begin();
}
但我发现它仍然不行,因为 lt 没有被const修饰,所以进去调的时候实际上是这样的:
并且返回值也匹配不上,事实上编译器也给了报错:
这都是对类的认识不足导致的,实际上我们只能确实只能这样使用:
template<class T>
void const_print(const list<T>& lt)
{
auto it = lt.begin();
while (it != lt.end())
{
*it += 2; //错误
cout << *it << " ";
++it;
}
}
void test2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
const_print(lt);
}
本质上是因为我们无法控制this指针的传递(无法修饰this),因为这根本不是我们在做的事,是编译器干的。
所以我们只能调用函数的时候传递const list,这时候list中也已经有节点了,满足对数据的存储。
然后又因为是 const list,编译器在调用函数的时候发现这是一个 const list,就会给this指针加上const。
迭代器重载operator-> + 迭代器的第三个模板参数
首先要明白什么时候我们会使用 ->
int x = 10;
int *p = &x;
cout << *p;
struct Date
{
Date(int year,int month,int day):_year(year),_month(month),_day(day){}
int _year;
int _month;
int _day;
}
Date *d = new Date();
d->_year = 2000;
d->_month = 10;
(*d)._day = 5; //也可以这样访问,但相比起来,->会更简洁明了
当我们要访问的数据在一个类(或结构体)中,如果我们拿到了指向这个类的指针p,
那么访问该类的数据,就可以使用 ->
同理,如果list中保存的是内置类型数据(int、double…),自然用不到 ->
但是如果这样保存数据:
list<Date> lt;
lt.push_back(Date(2022,10,1));
我们拿到的节点数据是一个结构体,我们要访问的是结构体里的数据,这时候->就有用到的地方了。
因此我们要进一步完善迭代器,同样的,在重载operator->时
-
对于普通迭代器,我们允许它修改数据,
-
对于const迭代器,我们不允许它修改数据。
能否修改数据,取决于返回值是否以const修饰,所以,返回值是不确定的、模糊的,因此又需要用到模板。
template<class T, class Ref, class Ptr> //第三个模板参数
struct __list_iterator
{
typedef list_node<T> Node;
__list_iterator<T, Ref, Ptr>(Node* node) :_node(node) {}
typedef __list_iterator<T, Ref, Ptr> iterator;
Ptr operator->()
{
return &(_node->val);
}
private:
Node* _node;
};
注意:**operator->的返回值一定必须设置成一个地址。**这样当我们调用的时候就会变成这样
list<Date> lt;
auto it = lt.begin();
it->->_year; //两个箭头
(*it)._year; //也可以这样写,但不够简洁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsQ4ltFr-1679755761152)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918122801142.png)]
第一个箭头拿到节点数据的地址——一个类或结构体的地址,第二个箭头拿到这个类里的数据。
但两个箭头看着很奇怪,所以编译器对这里做了优化,只要我们只需要给一个箭头就能表示获取数据:
it->_year;
那可能就有人要这样问了:取节点value的地址返回,然后在外面使用相当于 (&node->val)->
不就相当于默认了节点保存的是自定义类型(结构体)?是的,就是默认了
那如果val是一个int类型的数据,不就变成了 (&int) -> 可是int不像结构体有多个数据,只有一个,不会出错吗?
是的会出错,本来也就不允许这样使用,因为也没地方指,指向谁?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vx5jt9yE-1679755761153)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918123209909.png)]
所以对于内置类型,没有人会这么用 -> 箭头
我们敢operator->返回地址的底气也就在于:
-> 使用的前提就是类(结构体),节点值保存的一定是个自定义类型(结构体),
否则,对于内置类型根本不会用到 ->,从语法层面就是错的。
template<class T, class Ref>
struct __list_iterator
{
typedef list_node<T> Node;
__list_iterator<T, Ref, Ptr>(Node* node) :_node(node) {}
typedef __list_iterator<T, Ref, Ptr> iterator;
private:
Node* _node;
};