文章目录
前言
承接上文,我们现在来考虑一下list类的整体实现
正文开始!
一、默认生成函数
构造函数
list是一个带头双向循环链表,在构造一个list对象时,直接申请一个头结点,并让其前驱指针和后继指针都指向自己即可
// List的构造
list()
{
CreateHead();
}
private:
void CreateHead()
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
}
如果是想直接插入n个T元素的话:
list(int n, const T& value = T())
{
CreateHead();
for (int i = 0; i < n; ++i)
push_back(value);
}
如果是传迭代器来构造的话:
template <class Iterator>
list(Iterator first, Iterator last)
{
CreateHead();
while (first != last)
{
push_back(*first);
++first;
}
}
如果是拷贝构造的话:
list(const list<T>& l)
{
CreateHead();
// 用l中的元素构造临时的temp,然后与当前对象交换
list<T> temp(l.begin(), l.end());
this->swap(temp);
}
如果是赋值构造的话:
list<T>& operator=(list<T> l)
{
this->swap(l); // 现代写法,先拷贝构造一下,然后直接交换,刚好销毁
return *this;
}
析构函数
//其中迭代器是作为指向作用,申请资源不属于迭代器
//而属于链表,不需要考虑析构的问题,迭代器就是玩数据
// 所以list在这里必须写析构
~list()
{
// clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可
clear();
delete _head;
_head = nullptr;
}
void clear()
{
Node* cur = _head->_next;
// 采用头删除删除
while (cur != _head)
{
_head->_next = cur->_next;
delete cur;
cur = _head->_next;
}
_head->_next = _head->_prev = _head;
}
二、迭代器相关函数
// 前面的的类就发挥作用了
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T&> const_iterator;
begin & end
begin函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器
当然,还需要重载一对用于const对象的begin函数和end函数
对于list这个带头双向循环链表来说,其第一个有效数据的迭代器就是使用头结点后一个结点的地址构造出来的迭代器,而其最后一个有效数据的下一个位置的迭代器就是使用头结点的地址构造出来的迭代器。(最后一个结点的下一个结点就是头结点)
//返回值没有传引用返回,由于该接口功能是返回开头和节点迭代器
//如果传引用返回,会影响下一次调用该节点
//而我们不需要拿到这个位置的迭代器,只需要拿到这个位置的信息
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
// const iterator可不可以呢?
// 当然不行,我们不是想要iterator不可修改
// 而是希望iterator指向的内容不可修改
//iterator是一个类,如果在前面加const,它表示的意思是这个类对象本身不能被修改,而不是指向内容不能被修改
//会导致++或–运算符之类的运算符重载会失效
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
三、访问容器相关函数
front & back
front和back函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可
当然,这也需要重载一对用于const对象的front函数和back函数,因为const对象调用front和back函数后所得到的数据不能被修改
因为是双向链表,所以其实这是很方便的,但是要注意list是没有operator[ ]的
// 直接使用list成员变量版本
T& front()
{
return _head->_next->_val;
}
const T& front()const
{
return _head->_next->_val;
}
T& back()
{
return _head->_prev->_val;
}
const T& back()const
{
return _head->_prev->_val;
}
// 使用迭代器来间接获取数据版本
T& front()
{
return *begin();
}
T& back()
{
return *(--end());
}
const T& front() const
{
return *begin();
}
const T& back() const
{
return *(--end());
}
四、插入、删除函数
别紧张,这对于双向链表来说是舒适区
insert
先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,接着根据所给数据x构造一个待插入结点,之后再建立新结点与cur之间的双向关系,最后建立新结点与prev之间的双向关系即可
// 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T& val)
{
Node* pNewNode = new Node(val);
Node* pCur = pos._node;
// 先将新节点插入
pNewNode->_prev = pCur->_prev;
pNewNode->_next = pCur;
pNewNode->_prev->_next = pNewNode;
pCur->_prev = pNewNode;
return iterator(pNewNode);
}
erase
erase函数可以删除所给迭代器位置的结点
先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prev和next之间的双向关系即可
// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{
// 找到待删除的节点
Node* pDel = pos._node;
Node* pRet = pDel->_next;
// 将该节点从链表中拆下来并删除
pDel->_prev->_next = pDel->_next;
pDel->_next->_prev = pDel->_prev;
delete pDel;
return iterator(pRet);
}
push_back & pop_back
push_back和pop_back函数分别用于list的尾插和尾删,在已经实现了insert和erase函数的情况下,我们可以通过复用函数来实现push_back和pop_back函数
void push_back(const T& val)
{
insert(end(), val);
}
void pop_back()
{
erase(--end());
}
push_front & pop_front
当然,用于头插和头删的push_front和pop_front函数也可以复用insert和erase函数来实现,push_front函数就是在第一个有效结点前插入结点,而pop_front就是删除第一个有效结点
void push_front(const T& val)
{
insert(begin(), val);
}
void pop_front()
{
erase(begin());
}
在vector实现push_back和pop_back时,通过begin和end得到迭代器指向的位置。返回变量为具有常性的临时变量,不能通过++或–对其修改(但是可以begin() + 3)
在List中迭代器可以进行++或–操作,由于不是对临时对象本身进行修改,而是在运算符重载中改变了运算符的行为,修改是临时对象指向的内容。在vector中修改是对象本身当然是不行的
五、其余函数
size
size函数用于获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数
size_t size() const
{
Node* cur = _head->_next;
size_t count = 0;
while (cur != _head)
{
count++;
cur = cur->_next;
}
return count;
}
resize
- 若当前容器的size小于所给n,则尾插结点,直到size等于n为止
- 若当前容器的size大于所给n,则只保留前n个有效数据
因为size()要遍历一遍链表,这其实很不好,所以我们单独来个oldsize变量保存一下
// 再次体现了复用思想
void resize(size_t newsize, const T& data = T())
{
size_t oldsize = size();
if (newsize <= oldsize)
{
// 有效元素个数减少到newsize
while (newsize < oldsize)
{
pop_back();
oldsize--;
}
}
else
{
while (oldsize < newsize)
{
push_back(data);
oldsize++;
}
}
}
empty
empty函数用于判断容器是否为空,我们直接判断该容器的begin函数和end函数所返回的迭代器,是否是同一个位置的迭代器即可。(此时说明容器当中只有一个头结点)
// 迭代器版本
bool empty() const
{
return begin() == end();
}
// 直接使用list成员变量版本
bool empty() const
{
return _head->_next == _head;
}
swap
swap函数用于交换两个容器,list容器当中存储的实际上就只有链表的头指针,我们将这两个容器当中的头指针交换即可
void swap(list<T>& l)
{
std::swap(_head, l._head);
}
总结
怎么样,是不是感觉很复杂呢!
你可以自己去实现一下!