目录
前言
一、接口总览
一、节点类的模拟实现
二、迭代器类的模拟实现
迭代器的目的
list迭代器为何要写成类?
迭代器类模板参数说明
模拟实现
1.构造函数
2.*运算符重载
3.->运算符重载
4.前置++
5.后置++
6.前置--
7.后置--
8.!=
9.==
三、list类的模拟实现
Ⅰ、默认成员函数
编辑
1.构造函数
2.拷贝构造
3.赋值重载
4.析构函数
二、迭代器
begin+end
三、访问数据
front和back
四、增删查改
1.insert()
2.erase()
3.push_back
4.pop_back()
5push_front
6.pop_front
7.clear()
8.swap()
五、容量
size
empty()
前言
对于list的模拟实现重点是其迭代器的实现,同时这又是对C++类与对象的一次更深刻的理解与体会!
一、接口总览
namespace li
{
//节点结构
template<class T>
struct ListNode
{
//成员变量
ListNode<T>* _prev;
ListNode<T>* _next;
T _date;
ListNode(const T& value = T());//构造函数
};
//迭代器类
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node);
Ref operator*();
Ptr operator->();
//++it
Self& operator++();
//it++
Self operator++(int);
//--it
Self& operator--();
//it--
Self operator--(int);
//it1!=it2
bool operator!=(const Self& it);
//it1==it2
bool operator==(const Self& it);
};
//list结构
template<class T>
class list
{
public:
typedef ListNode<T> Node;
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
void empty_init()
// //iterator///
iterator begin();
iterator end();
const_iterator begin()const;
const_iterator end()const;
// /
// ///构造//
list();
list(int n, const T& value = T());
//迭代器区间初始化
template <class Iterator>
list(Iterator first, Iterator last);
//拷贝构造
//it1(it2
list(const list<T>& l);
//赋值构造
//it1=it2
list<T>& operator=(list<T> l);
//析构函数
~list();
//modify
iterator insert(iterator pos, const T& x);
iterator erase(iterator pos);
void push_back(const T& x);
void pop_back();
void push_front(const T& val);
void pop_front();
void clear();
void swap(list<T>& l);
///
// List Capacity
size_t size()const;
bool empty()const;
// List Access
T& front();
const T& front()const;
T& back();
const T& back()const;
private:
Node* _head;
size_t _size;
};
}
同样要再自己的命名空间域里面模拟实现!!!
一、节点类的模拟实现
前面我们也有说到list实际上底层就是一个带头双向循环链表,链表的结构就是由一个又一个的结点组成,但是呢,list每次实例化出来的对象数据类型可能不一样,因此我们首先需要实现一个结点类!每一个结点所包含的信息有:数据、前驱指针、后继指针!同时类里面只需要实现一个构造函数。因为该结点类只需根据数据类型去构造去一个结点即可!
//结点类
template<class T>
struct ListNode
{
ListNode<T>* _prev;//前驱指针
ListNode<T>* _next;//后继指针
T _date;//数据
ListNode(const T& value = T())//全缺省
:_prev(nullptr)
, _next(nullptr)
, _date(value)
{}
};
对于构造函数里面的参数在vector的模拟实现有提及,如果有传值,那就用对应类型的值即可,如果没有,那就使用对应类型的默认构造函数所构造出来的值作为数据!
注意:这个用关键字struct的类,默认的成员变量和成员函数是公开的,类内类外都可以访问!
二、迭代器类的模拟实现
迭代器的目的
在解释原因之前,我们需要了解到一点就是迭代器的意义或者说目的到底是什么?
实际上,迭代器的目的就是:不关注底层的实现细节,能够采用一种类似于指针的方式去访问容器中的内容和数据!说白了就是想要去模拟指针的行为!(++,--,*等操作)
list迭代器为何要写成类?
在前面的string和vector的模拟实现中我们都没有单独的去实现这样一个迭代器类,为啥这里需要呢?实际上就是因为底层空间结构,string和vector是一段连续的空间,他们底层的迭代器就是原生的指针!
而list底层的结构是不连续的,随机的,如果采用直接采用原生的指针结点去作为迭代器,那么对于++这类的操作符,一个结点能进行吗?很显然是不能的,因为不是连续的空间!
所以,对于list的迭代器,虽说本质还是原生的结点指针,但是不能直接采用,因为原生的结点指针不能够满足我们所需要的迭代器的行为,也就是它不能像vector和string的迭代器那样直接进行++,--,*等操作!
所以的所以,内置类型不能满足我们所需要的行为时,我们可以将其封装成自定义类型,也就是将原生的结点指针封装成一个类,这样就变成了自定义类型,对于一个类,我们就可以进行运算符的重载!比如表面上是对迭代器进行++操作,其底层实际上是node=node->next!这不是就符合迭代器存在的目的吗?
总结:list迭代器,由于原生结点不能满足我们所需的行为,因此要将其封装成一个类,也就是变成自定义类型,就能控制它的行为!
迭代器类模板参数说明
template<class T, class Ref, class Ptr>
这个参数的存在就是就是因为迭代器实际有两种,一个是非const,一个为const对象提供的。而这两种迭代器的底层实现起来其实大差不差,为了让代码不冗余,所以去定义了这个重定义类型。可以看list类中的这几个重定义类型
typedef ListIterator<T, T&, T*> iterator; typedef ListIterator<T, const T&, const T*> const_iterator;
可以看到Ref对应的是T引用,Ptr对应的就是T指针,他们会根据传进来的类型自动匹配!如果不设计就很难区分。
为啥要一个引用,一个指针呢?实际和运算符重载有关,接着向下看!
模拟实现
1.构造函数
本质还是结点指针,所以迭代器需要传进来一个结点指针完成构造!
ListIterator(Node* node)
:_node(node)
{}
2.*运算符重载
//*it Ref operator*() { return _node->_date; }
这个操作实际上相当于指针的解引用,*it,访问数据,对于解引用操作,我们不仅可以对当前是数据进行读操作,还能重新赋值,也就是可读可写,所以采用引用返回!所以原本是T&,但由于要区分const T&,就用了Ref这个模板参数,这就是它的由来
3.->运算符重载
这个的存在,在某些场景下面用迭代器是可能会用到->操作!
如下场景:
如我们的容器里面的数据类型不是内置类型,而是自定义类型时,可能会使用到这个运算符。
比如上面这段代码,在*it时会发生错误,因为解引用只是访问到A而已,没有访问到里面的成员变量
改法1:
改法2:
这里就需要使用到运算符->重载,实际上这里完整的写法是:it.operator->()->_a,缩写就是两个->->,第一个->实际上获取的是A*,第二个是对A*指针的解引用!编译器为了代码可读性,省略了一个!
有上面的知识可以得出重载的底层实现了,实际上就是返回当前数据的地址!
//为了像指针那样去操作:it->; Ptr operator->() { return &_node->_date; }
这里原本的返回类型是T*的,但是由于要区分const T*, 所以设计一个模板参数叫做Ptr,它会根据传进来的数据类型进行实例化,这也是它由来的原因!
注意:-->操作要求成员变量是公有的才可以访问!
4.前置++
前置++要求是当前对象先自增完才返回自增后的数据,自增的操作实际上就是让指针向后移动
//++it
Self& operator++()
{
_node = _node->_next;
return *this;
}
注意:Self为当前迭代器的类型
typedef ListIterator<T, Ref, Ptr> Self;
5.后置++
后置++,要先返回加之前的结果,然后再++,所以可以拷贝构造出一个临时变量,然后再自增,最后再返回这个临时变量即可!注意了,这里不能用引用返回,而是使用传值返回,因为临时变量具有常性的原因,可读不可写
//it++
Self operator++(int)
{
Self tmp(*this);//虽然这个类,没有拷贝构造,但是默认生成的拷贝构造就已经够用了,因为迭代器只需要浅拷贝就好了
_node = _node->_next;
return tmp;
}
需要注意后置的写法!!!这个再类和对象二有提及
6.前置--
这个和前置++一样的思路,让指针前移即可!
Self& operator--()
{
_node = _node->_prev;
return *this;
}
7.后置--
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
8.!=
注意这里是比较两个结点的指针,不是里面的数据,如果比较数据,那就扯淡了,因为可能数据会相同!
bool operator!=(const Self& it)
{
return _node != it._node;
}
9.==
也是比较指针!
bool operator==(const Self& it)
{
return _node == it._node;
}
其实可以看出上一章list的介绍与使用中,为什么,list的迭代器只能++,而不能是begin()+5等等之类的了,因为底层压根没有重载,同时它本身的结构也不支持!
容器之间的迭代器底层实现是不一样的,这取决于他们的底层结构,但是表面上使用起来类似,都是去模仿指针的行为!
三、list类的模拟实现
Ⅰ、默认成员函数
成员变量无需多言,就是一个结点指针,外加一个size!以及额外的三个重命名类型!
public:
typedef ListNode<T> Node;
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
//成员变量
private:
Node* _head;
size_t _size;
除此之外,我们新设置了个成员函数初始化空链表的,因为每次构造都需要先构造一个空的链表,然后再进行后续操作!
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
1.构造函数
- 无参数构造
list()
{
empty_init();
}
- 特定值初始化
list(int n, const T& value = T())
{
empty_init();
for (int i = 0; i < n; i++)
{
push_back(value);//尾插数据即可
}
}
- 迭代器区间初始化
template <class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
这里和vector的类似!
2.拷贝构造
先初始化一个头,再尾插即可!
//拷贝构造
//it1(it2)
list(const list<T>& l)
{
empty_init();
for (auto& e : l)
{
push_back(e);
}
}
3.赋值重载
这里我们采用现代写法!使用复用拷贝构造+swap交换
//赋值构造
//it1=it2
list<T>& operator=(list<T> l)//引用返回支持连续赋值
{
swap(l);
return *this;
}
这里也可以采用类似 string类中传统写法,先清理容器,再一个个尾插!
//赋值构造
//it1=it2
list<T>& operator=(const list<T>& l)
{
if (this != &l)//避免自己给自己赋值
{
clear();//清理容器
for (auro& e : l)
{
push_back(e);//尾插到it1
}
}
return *this;
}
4.析构函数
先清理内容,再释放头结点!
//析构函数
~list()
{
clear();
delete _head;
_head = nullptr;
}
二、迭代器
begin+end
需知:begin():返回的是第一个有效数据的迭代器
end():返回的是最后一个有效数据的下一个位置的迭代器
iterator begin() { return iterator(_head->_next);//会去调用迭代器类构造一个迭代器,再返回 //return _head->_next;也可以这样去写,因为单参数的构造函数支持隐式类型的转化 //而iterator的构造函数实际上就是单参数的构造 } iterator end() { return iterator(_head); } //const迭代器 const_iterator begin()const { return iterator(_head->_next); } const_iterator end()const { return iterator(_head); }
对于const迭代器,不能写成const iterator,这样性质就不同了,因为这个表示的是迭代器本身不能修改,而我们期望的是指向的内容不能修改,所以const iterator不是我们需要的迭代器!
一定要注意区分这里的iterator和vector中的iterator,这里的iterator已经被重命名了,它实际上是一个类!
typedef ListIterator<T, T&, T*> iterator; typedef ListIterator<T, const T&, const T*> const_iterator;
三、访问数据
front和back
front:返回list的第一个结点中值的引用,就是取头数据
back:返回list的最后一个结点中值的引用,就是取尾数据
T& front() { return _head->_next->_date; } const T& front()const { return _head->_next->_date; } T& back() { return _head->_prev->_date; } const T& back()const { return _head->_prev->_date; }
四、增删查改
1.insert()
在pos位置前插入操作,并返回pos位置的迭代器!这个pos实际上也是个迭代器!具体可以看上节的原函数list的介绍与使用!这个只模拟实现一个就是插入值的操作!
这里的插入,实际上就是new一个结点出来,在进行插入操作即可!
iterator insert(iterator pos, const T& x) { Node* newnode = new Node(x); Node* cur = pos._node; Node* prev = cur->_prev; prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; ++_size; return pos; }
2.erase()
删除pos位置的值,并返回pos位置的迭代器。所以这里会存在迭代器失效的问题,注意只是当前迭代器失效,之后的其他迭代器并不会受到任何影响!因为每一个迭代器都是由一个个结点构造出来的!
iterator erase(iterator pos) { Node* cur = pos._node; Node* prev = cur->_prev; Node* next = cur->_next; prev->_next = next; next->_prev = prev; delete cur; --_size; return next; }
需要注意删除操作,我们需要先保存当前结点的前一个结点和后一个结点,这样才能将其前后的两个结点连接起来!
3.push_back
尾插操作,实际上就是在end()位置插入,就是头结点的前面插入!直接复用insert
void push_back(const T& x) { insert(end(), x); }
4.pop_back()
尾删操作,直接复用erase即可!
void pop_back() { erase(--end());//这里只能--end,不能end-1因为end是传值返回的 }
5push_front
头插操作,直接复用!
void push_front(const T& val) { insert(begin(), val); }
6.pop_front
头删操作,直接复用!
void pop_front() { erase(begin()); }
7.clear()
清理工作,保留头节点!
void clear() { iterator it = begin(); while (it != end()) { it=erase(it);//注意这里一定要更新迭代器,因为会失效 } }
8.swap()
void swap(list<T>& l) { std::swap(_head, l._head); std::swap(_size, l._size); }
直接复用全局的swap
五、容量
size
这里我们在插入和删除操作,都加上了++size或者--size了。所以直接调用即可!
size_t size()const { return _size; }
empty()
bool empty()const
{
return _size == 0;
}
好了今天的分享就到这里,我认为这是我学C++的第二个难点,重点就是迭代器类的模拟实现,它再一次体现了类和对象的特点以及封装的特性。值得反复去体会和反思!!!