list常识
- list迭代器不支持【】,所以不支持随机访问。也不支持>、<,没有意义,因为iterator是地址,地址并不连续。
重要的说三遍:
list不支持随机访问,因为没有重写[]。
list不支持随机访问。
list不支持随机访问。 - list底层是双向循环链表,头插尾插都方便,但是vector只有尾插方便(不扩容时)。
list模拟实现
首先,list的底层是每个节点,所以需要:Node类,后才是list类。因为涉及利用迭代器去构造类,所以还需要迭代器类。
0.准备工作:需要的三个类解析
Node、ListIterator、list
- Node:节点类,list是循环链表,需要双指针,所以给需要前后指针,而存值类型需要是泛型,所以类外需要模板。
- 缺省构造函数:省写全缺省型。
这里初始化值为默认值,且两个指针指向空。
多用初始化参数列表,参数列表这只初始化,不赋值。
初始化参数列表好处:
比如有const类型数据和引用类型数据,const类型成员变量,只能初始化不能赋值。
引用类型只能初始化,不能改变,且不能在默认构造函数里,需要专门的构造函数去做初始化,但是也只能在初始化列表中初始化。
效率高
struct ListNode
{
ListNode(const T& val = T())
:_val(val)
, _prev(nullptr)
, _next(nullptr)
{}
ListNode<T>* _pPre;
ListNode<T>* _pNext;
T _val;
};
- Iterator:迭代器类,它的本质是指向Node的地址,所以私有成员是Node节点指针,后续解引用或++、–都需要通过该指针指向成员的prev或next来获取地址。此外,迭代器需要实现基本功能++、–、!=、=、等等。list不能随机访问,所以用迭代器方便访问且非常必要。让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。
- 普通迭代器构造函数:
接收一个节点指针,缺省型构造函数。 - const类型迭代器构造函数:
这也是为什么类外需要三个模板参数,因为只是返回值类型不同,因为比如Operator*,operator++、operator–这些没有参数,迭代器部分重载函数没有参数,C++重载需要参数类型不同,而这里只有返回值类型不同。所以为了减少代码冗余,我们把cosnt类型和普通类型的类、指针、引用都用模板,这样就可以重载了,不必为了const类型重写相似度极大的迭代器代码了,在list上,调用时候,会决定这些的类型。
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T>* PNode;
typedef ListIterator<T, Ref, Ptr> Self;
public:
ListIterator(PNode pNode = nullptr);
ListIterator(const Self& l);
T& operator*();
T* operator->();
Self& operator++();
Self operator++(int);
Self& operator--();
Self& operator--(int);
bool operator!=(const Self& l);
bool operator==(const Self& l);
private:
PNode _pNode;
};
ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。
1. 迭代器的构造函数和拷贝构造函数
迭代器的构造函数有两种。
- 普通构造函数:成员变量是指向节点的指针,所以我们利用节点的指针,可以初始化一个迭代器对象。
直接利用参数的节点指针,初始化自己的成员变量节点的指针值。
- 拷贝构造函数,我们利用迭代器对象,也可以拷贝构造迭代器。
ListIterator(PNode pNode = nullptr)
:_pNode(pNode)
{}
ListIterator(const Self& l)
: _pNode(l._pNode)
{}
2. 迭代器类的重载函数
和T或T&相关的返回值类型的函数,建议先看完list的const迭代器,这里的写法没有考虑const迭代器类型。
0. 回忆:几个遗忘概念:
- this的值是对象的地址,*this得到迭代器这个类的对象。
- ++、–我们要的是迭代器对象本身,所以返回*this或后置时返回迭代器对象temp即可。
- **typedef ListIterator<T, Ref, Ptr> Self; **这样写是说list将来内部会用到Ref、Ptr,而我们的ListIterator中存的值可能是这三种,这里改为Self,就理解成是一个迭代器类型即可。
- return & 和 return 普通,return &不是返回引用类型,而返回引用类型只能从函数类型上做声明。
- 重载解引用:operator*
对迭代器解引用,我们一般的用法像:*it = 2,所以需要能改变节点的值,所以用引用接收,而返回的是this->_pNode->val。
函数类型是引用,使得直接能修改。
函数类型是引用,使得直接能修改。
函数类型是引用,使得直接能修改。
T& operator*()
{
return _pNode->_val;
}
- 重载->:
当节点T中存的是对象,当使用it->,即:迭代器->,需要能拿到节点中存的对象,其实就是val。所以需要:先解引用,得到节点,节点是个对象,我们访问对象的成员,需要用对象的地址,所以再取地址&。
T* operator->()
{
return &*this;
}
- 重载前置++、后置++,区别是括号中有没有int,后置有int。
不论是前置还是后置,都需要返回迭代器,所以前置是*this,迭代器对象本身。Self表示这个迭代器类。此外,用&类型接收效率高。
前置++:
Self& operator++(int)
{
Self tmp(*this); // 这里的前置++,返回了一个*this解引用,是个迭代器,
_pNode = _pNode->_pNext;
return tmp;
}
后置++:利用拷贝构造,以迭代器对象*this拷贝tmp即可。
后置++我们返回的是tmp,不能用&,因为tmp生命周期只在函数体内,而使用普通类型Self则使得函数再返回一个临时拷贝,对return中的tmp再拷贝一次,简言之,return tmp搭配引用,是错误的。
Self operator++(int)
{
Self tmp(*this); // 这里的前置++,返回了一个*this解引用,是个迭代器,
_pNode = _pNode->_pNext;
return tmp;
}
- 重载–:道理如同++,且注意后置–不可以用引用接收。
Self& operator--(int)
{
Self tmp(*this);
_pNode = _pNode->_pPre;
return tmp;
}
Self& operator--()
{
_pNode = _pNode->_pNext;
return *this;
}
- 重载!=和==:
迭代器相不相等,不看迭代器本身,而是内部包含的节点是否相同,而看内部节点相同否,需要看:它的成员变量,节点指针指向的地址是不是相同,相同说明记录的是同一个节点。
bool operator!=(const Self& l)
{
return _pNode != l._pNode;
}
bool operator==(const Self& l)
{
return _pNode == l._pNode;
}
list类部分:
1. 构造函数:
- 普通构造函数:造个头节点即可。
list()
{
_head = new node; //申请一个头结点
_head->_next = _head; //头结点的后继指针指向自己
_head->_prev = _head; //头结点的前驱指针指向自己
}
- 拷贝构造函数:
list(const list<T>& l)
{
_pHead = new Node;
_pHead->_pPre = _pHead;
_pHead->_pNext = _pHead;
list<T> tmp(l.begin(), l.end());
swap(tmp);
}
用迭代器拷贝构造函数拷贝即可。现代写法做交换。
3. 迭代器的拷贝构造函数
先要一个头节点,再从头到尾,插值。
template <class Iterator>
list(Iterator first, Iterator last)
{
_pHead = new Node;
_pHead->_pPre = _pHead;
_pHead->_pNext = _pHead;
while (first != last)
{
push_back(*first);
++first;
}
}
4. Swap:
void swap(list<T>& l)
{
PNode tmp = _pHead;
_pHead = l._pHead;
l._pHead = tmp;
}
这个函数非常重要,在现代写法中灵活利用,省写很多,通过参数(不要用引用类型),参数会调用拷贝构造函数,此swap在list中要交换两个节点,而这里面因为参数list类型的L是局部变量,最终会调用L的析构函数,而不会调用*this的析构函数,所以这两个指向的节点只要交换就行。
CRUD:
注意CRUD时的所有插入操作,都不需要考虑扩容,因为list的底层是链表节点。
- erase():通过迭代器拿节点,然后删除链表节点。
- insert():利用insert()可以实现各种插入操作。list和vector的insert都是对迭代器操作。
iterator insert(iterator pos, const T& val)
{
//assert(pos._pNode); //检测pos的合法性 不能在头位置之前插入
PNode pNewNode = new Node(val);
PNode pCur = pos._pNode;
// 先将新节点插入
pNewNode->_pPre = pCur->_pPre;
pNewNode->_pNext = pCur;
pNewNode->_pPre->_pNext = pNewNode;
pCur->_pPre = pNewNode;
return iterator(pNewNode);
}
5. 普通迭代器
关于迭代器类函数begin和end的返回值,也再次提醒规律,只有全局对象,或者说是成员变量,才配用引用返回(函数类型是引用,确实我们也看到了在迭代器类中才用了返回,且是*this)。
返回迭代器即可,迭代器初始化用节点地址。因为return的值是临时变量,所以我们不能用引用类型。
iterator begin()
{
return iterator(_pHead->_pNext);
}
// list的end()其实是头节点,这里不存值。
// 不用引用,因为返回的是临时变量。不是类成员变量,只有类成员变量,才有资格。如:*this等
iterator end()
{
return iterator(_pHead);
}
6. const迭代器相关
list中通过实例化来使用迭代器类,迭代器类根据不同实例化给的值去区分现在内部的一些模板参数值是啥。所以迭代器类使用三个模板参数,后面两个是引用Ref和指针Ptr。使得const迭代器不能修改,只能读。list中用const_iterator,则iterator中的参数Ref和Ptr会实例化为const T&和const T*。注意:我们迭代器中定了参数,函数返回值类型就用参数即可。
自从,我们改了iterator的模板参数,增加了两个,就也得修改原来的一些返回值类型:
T& 变成Ref,T*变为Ptr。这样就使得list那边调用过来时候自动实例化使得能区分。
此外,别迷糊:内部重命名的Self是迭代器类本身,只有++、–时才使用,做这些返回,而模板参数只涉及内部存储值,和解引用、->相关。
此外,回忆operator*的返回值类型是T&,因为在iterator内部,还要对存储值本身要做修改,所以我们换T&为Ref后,Ref有两种情况,当是普通引用类型时候可读可写,而为const 引用类型时只可读。
7. 遇到的错误:
==ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。
====ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。
==ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。
完整代码
list2.h
#include<iostream>
#include<assert.h>
using namespace std;
namespace lz
{
// List的节点类
template<class T>
struct ListNode
{
ListNode(const T& val = T())
:_val(val)
, _pPre(nullptr)
, _pNext(nullptr)
{
}
ListNode<T>* _pPre;
ListNode<T>* _pNext;
T _val;
};
// 迭代器类
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T>* PNode;
typedef ListIterator<T, Ref, Ptr> Self; // 这里代表的是:迭代器类型,且内部可以存这三种,看看视频吧
public:
// 构造函数 :以节点的指针拷贝,即以节点地址拷贝。
ListIterator(PNode pNode = nullptr)
:_pNode(pNode)
{}
// 以迭代器拷贝:加const为了保护,这个用的更多
ListIterator(const Self& l)
: _pNode(l._pNode)
{
//cout << "以迭代器类型拷贝,后置++中试试" << endl;
}
// 而下面的*this,是*this是个迭代器,走的是第二个
// *返回值类型T& 节点的地址,不用Ref,因为const类型迭代器不可以解引用 所以
// 这里我忘了:我们要修改一个值,就传这个变量本身即可,而我们用&接收,就能修改这个值。
// 这里*,要的是迭代器的节点的成员,不用写this,直接能拿到。
//T& operator
Ref operator*()
{
return _pNode->_val;
}
// *this才是迭代器。
// ->:迭代器的->是为了访问list成员类型是类类型时,我们需要拿到对象的地址,利用对象地址去拿值
// 所以*this是内部存的对象,而&取地址,得到对象地址,有对象地址就能直接访问对象成员了,然而编译器会把两个->合并为一个
//T* operator->()
Ptr operator->()
{
return &*this;
}
// 前置++ 迭代器++后,我们需要的权限是访问和改变list中节点,
// 迭代器本质是存着节点的地址,所以对它解引用,得到节点本身。
// 且返回引用效率高,且我们要操作的是对象本身 通过迭代器可以改变值,所以需要返回引用。
Self& operator++()
{
_pNode = _pNode->_pNext;
return *this;
}
// 后置++
// 拷贝当前迭代器,返回tmp ,相当于是旧的*this,仍然 *this后是个节点,可以改对象
// 上面把迭代器类型起别名为Self,self本身是个迭代器类型,self&接收效率高
Self operator++(int)
{
Self tmp(*this); // 这里的前置++,返回了一个*this解引用,是个迭代器,
_pNode = _pNode->_pNext;
return tmp;
}
// 前置-- :要迭代器中的本身,返回this即可
Self& operator--()
{
_pNode = _pNode->_pNext;
return *this;
}
// 后置--:需要返回迭代器,拷贝一个tmp,而迭代器的值可以改变,用引用接收。内部值也需要改变,所以用引用很合适。
// 不用引用,也可以,只是拷贝了迭代器中的节点,也没啥。
Self operator--(int)
{
Self tmp(*this);
_pNode = _pNode->_pPre;
return tmp;
}
// != 迭代器不等于,想比的是节点等不到,而看节点等不等,比看看是不是指向同一个节点,
// 看看成员变量存的地址相等不。
bool operator!=(const Self& l)
{
return _pNode != l._pNode;
}
// 比存着的地址即可。
bool operator==(const Self& l)
{
return _pNode == l._pNode;
}
PNode _pNode;
};
//list类
template<class T>
class list
{
typedef ListNode<T> Node;
typedef Node* PNode;
public:
// 两个的作用
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T&> const_iterator;
public:
///
// List的构造
// 无参普通构造
list()
{
_pHead = new Node;
_pHead->_pPre = _pHead;
_pHead->_pNext = _pHead;
}
// 带参普通构造: list存n个同样的值 参数带const为了保护 引用为了效率高
list(int n, const T& value = T())
{
_pHead = new Node;
_pHead->_pPre = _pHead;
_pHead->_pNext = _pHead;
for (int i = 0; i < n; ++i)
push_back(value);
}
// 迭代器构造函数:外部传值用 begin()、end()即可
template <class Iterator>
list(Iterator first, Iterator last)
{
_pHead = new Node;
_pHead->_pPre = _pHead;
_pHead->_pNext = _pHead;
while (first != last)
{
push_back(*first);
++first;
}
}
// 拷贝构造:现代写法,通过迭代器拷贝函数构造tmp 再做交换
list(const list<T>& l)
{
_pHead = new Node;
_pHead->_pPre = _pHead;
_pHead->_pNext = _pHead;
list<T> tmp(l.begin(), l.end());
swap(tmp);
}
// 赋值需要让内部值相等。所以先清空。也可以用现代写法。直接拷贝构造,再交换。
/*list<T>& operator=(const list<T> l)
{
if (this != l)
{
clear();
for (const auto& e : l)
{
push_back(e);
}
}
return *this;
}*/
// 重载= : 现代写法 :利用参数做交换,必然不能用cosnt
list<T>& operator=(list<T> l)
{
swap(l);
return *this;
}
// 先清空再删头的空间,置不置null其实都能
~list()
{
clear();
delete _pHead;
_pHead = nullptr;
}
///
// List Iterator
iterator begin()
{
return iterator(_pHead->_pNext);
}
// list的end()其实是头节点,这里不存值。
// 不用引用,因为返回的是临时变量。不是类成员变量,只有类成员变量,才有资格。如:*this等
iterator end()
{
return iterator(_pHead);
}
const_iterator begin()const
{
return const_iterator(_pHead->_pNext);
}
const_iterator end()const
{
return const_iterator(_pHead->_pNext);
}
///
// List Capacity:挨个统计吧
size_t size()const
{
size_t sz = 0;
const_iterator it = begin(); // 安全
while (it != end())
{
sz++;
it++;
}
return sz;
}
// empty():
bool empty()const
{
return begin()==end();
}
// List Access
// front是要第一个节点中存的值
// front 返回了对begin()的解引用,迭代器解引用返回节点所存内容,且迭代器类中重载*返回类型是引用,所以这里也可以用引用类型。
T& front()
{
return *begin();
}
const T& front()const // 返回类型上,自己会限定的
{
return *begin();
}
T& back()
{
return *(--end());
}
const T& back()const
{
return *(--end());
}
// List Modify
void push_back(const T& val) { insert(begin(), val); }
void pop_back() { erase(--end()); }
void push_front(const T& val) { insert(begin(), val); }
void pop_front() { erase(begin()); }
// insert,只能用迭代器插,vector和list都一样
// node的指针别名:PNode 有了迭代器的好处是,插入方便,直接能拿到这个点的前后。
// 返回的是临时变量 不能用引用 如果是该类的成员变量,用&很好。
// list插入不必考虑扩容
iterator insert(iterator pos, const T& val)
{
//assert(pos._pNode); //检测pos的合法性 不能在头位置之前插入
PNode pNewNode = new Node(val);
PNode pCur = pos._pNode;
// 先将新节点插入
pNewNode->_pPre = pCur->_pPre;
pNewNode->_pNext = pCur;
pNewNode->_pPre->_pNext = pNewNode;
pCur->_pPre = pNewNode;
return iterator(pNewNode);
}
// 删除pos位置的节点,返回该节点的下一个位置
// 返回下一个迭代器较好
iterator erase(iterator pos)
{
assert(pos._pNode); // 头结点不能删,且本来就没值
assert(pos != end()); // end位置在尾节点的下一个
// 找到待删除的节点
PNode pDel = pos._pNode; // 通过迭代器定位这个节点地址
PNode pRet = pDel->_pNext; // 待删的下一个
// 将该节点从链表中拆下来并删除
pDel->_pPre->_pNext = pDel->_pNext;
pDel->_pNext->_pPre = pDel->_pPre;
delete pDel;
return iterator(pRet);
}
// 删除:
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
// swap要交换两个链表的头,
// 节点指针,备份当前头节点。
// l出去会调用析构函数,所以l的部分删了,而p的部分没有删除
void swap(list<T>& l)
{
PNode tmp = _pHead;
_pHead = l._pHead;
l._pHead = tmp;
}
private:
PNode _pHead;
};
};
测试文件
list.cpp
#include"l2list.h"
void testl1()
{
lz::list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lz::list<int>::iterator it = lt.begin();
cout << "构造函数测试、迭代器测试、解引用修改值、push测试" << endl;
while (it != lt.end())
{
cout << *it << "改之后 : ";
*it *= 2;
cout << *it << endl;
++it;
}
}
int main()
{
testl1();
return 0;
}