接上篇,我们已经了解了基本的接口用法了,而这一篇呢,我们就来模拟STL标准库中是如何实现的,以对list的理解更深一步。
第一次了解链表的朋友,建议可以先去下面这篇问题了解了解!
vector与list的区别:
我们先来对比一下vector与list两者的区别:
vector | list | |
底 层 结 构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随 机 访 问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插 入 和 删 除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空 间 利 用 率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭 代 器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭 代 器 失 效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使 用 场 景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
ps:
1.理解内存碎片:是指内存中出现了一些不连续的小空闲区域。
举个例子:比如有10GB的内存,被分割成很多个小块,其中最大的空闲区域的内存只有100MB,而现在若有个200MB的连续内存需要存储,那么,这是不是就会受到影响?
2.迭代器的区别:
我们知道vector的底层原理实现是传统的顺序表,它的空间是连续的,具有指针天然的迭代器,并且是随机迭代器,即它可以通过++或者--去 找到元素的位置。
而list的底层实现的是带头双向循环链表(它不是连续的),所以对象的指针就无法承担起迭代器的功能,因为,它在物理内存中++或者--的结果是未知的内存块。因此,我们要想让迭代器自动去指向链表的下一个或者前一个,就需要自己去将结点的指针封装成一个类,然后再进行重载结点指针的++或者--或者!=来完成迭代器的功能。
首先,我们来看一下库里面的框架和成员变量:
那么,由于我们只是为了深入了解它的实现方式,并不是完全造出一个跟它一样或者更好的list,所以我们这里的list实现方式会跟STL中有所差异!这也是对于初学的我们比较好接受的一种方式了!
因此,我们现在正式来模拟实现list!
整体框架:
为了实现良好的封装性,层次性,和代码的维护性,我们要设计3个类:
1. list_node 节点类:
用于表示链表中的每个节点。每个节点包含数据域(val)以及指向前驱节点(prev)和后继节点的指针(next)。它是链表的基本构建单元,将数据和节点的连接关系封装在一起。
2. list_iterator 迭代器类:
ps:链表的迭代器不能直接使用内置类型的结点指针,但是结点指针还是数据的基础,迭代器内部通常会包含一个结点指针来记录当前的位置,本质上,内部所有操作都是通过结点指针来实现的,只是迭代器提供了一种更加安全的遍历方式。
为 list 容器提供了一种遍历元素的方式。迭代器类重载了各种运算符(如 operator* 、 operator++ 、 operator-- 等),使得可以像使用指针一样方便地遍历链表中的元素,同时隐藏了链表节点的具体实现细节。
3. list 类:
作为链表的容器类,它管理着链表的整体结构,包含对链表进行各种操作(如插入、删除、遍历等)的成员函数。 list 类依赖于 list_node 类来构建链表结构,并通过 list_iterator 类来提供对链表元素的访问接口。
接下来,我们就按照上面三个类进行讲解:
节点类的实现:
1.因为我们在的节点类在后续的list类中要使用的,而且是在list类的函数操作中会有直接使用到它的成员变量的,所以我们在使用class还是struct是有影响的。
我们知道,class的成员变量默认是私有的,也就是当你类外面想用它的成员变量时,需要public或者友元才能访问到它的成员变量。struct的成员变量和函数默认是公有的,既然后面都会用到节点类的成员变量,我们不妨直接把它定义成struct。即可满足我们的需求。
2.下面的const T& val = T(),这里不能把缺省值给0/nullptr,为什么呢?
因为:当调用构造函数时如果不传参数,就会使用 T() 来初始化 val 。对于内置类型(像 int 、 double 等), T() 会得到一个合适的初始值(比如 int() 是 0 , double() 是 0.0 );对于自定义类型,它会调用该类型的无参构造函数来创建一个对象,这就是为什么不能给 0 或者 nullptr 做默认值。
template<class T>
struct list_node
{
//成员变量
list_node<T>* _next;
list_node<T>* _prev;
T _val;
//构造函数-初始化
list_node(const T& val = T())
:_next(nullptr)
, _prev(nullptr)
, _val(val)
{}
};
迭代器类:
1.成员变量
struct __list_iterator
{
//链表节点的类
typedef list_node<T> Node;
//定义迭代器的成员变量
Node* _node;
};
2.初始化迭代器
为什么迭代器也要弄一个构造函数?
因为:通过传入节点指针,使得迭代器能够正确指向链表中的节点,为后续的迭代器操作(如 operator* 、 operator++ 等)提供基础。若你不弄一个构造函数的话,一开始迭代器指向那里?它是一个随机值。
并且,我们知道,不写初始化列表,所有成员也会走初始化:内置类型初始化列表不处理,自定义类型,会取调用它的构造函数。
__list_iterator(Node* node)
:_node(node)
//将 _node 初始化为传入构造函数的 node 指针。
{}
3.operator*运算符重载
1.先想一想:实现operator*的本质是什么?
答:取这个指针的指向的那个数据的对象。
因为迭代器模拟的是指向结构体的指针,而它属于自定义类型并不能支持直接的运算,所以只能弄成运算符重载实现这个操作。(C++引用不能改变指向)
//Ref是一个类模板,后面再讲解,现在先理解成T&或者const T&
Ref operator*()
{
return _node->_val;
}
4. operator->运算符重载
1.先想一想:实现operator->的本质是什么?什么时候要用到->呢?
答:结构体指针就要箭头。
操作符.与操作符->的区别:
实现的原因:
-成员访问的便捷性:
当容器中的元素是复杂类型(如包含多个成员的结构体或类)时operator-> 提供了一种方便的方式来访问元素的成员。它的行为类似于 (*it).member ,但语法上更加简洁。
例如,如果链表中的节点存储了一个包含 x 和 y 坐标的 Point 结构体,
通过 operator-> 可以直接写 it->x 或 it->y 来访问坐标值,而不需要写成 (*it).x 和 (*it).y 。
- 符合指针行为的习惯: operator-> 的设计是为了让迭代器在行为上更类似于指针。在C++中,使用 operator-> 来访问复杂对象的成员是一种自然的方式,使得迭代器的操作在语义上更加贴近于直接使用指针操作对象的方式,提高了代码的可读性和易用性
综上,我们要实现一个operator->函数,来通过迭代器直接访问Node类型的成员。
//Ptr是一个模板,后面再讲解,现在暂且可以把它理解为T*或者const T* Ptr operator->() { return &_node->_val; 因为要返回的是指针,_node->_val是一个值,加&是取地址相当于指针,指针本质是地址 }
当我们去使用时,我们会发现这直接是it->_a1,而并不是it->->_a1?
按道理来讲,按照我们上面实现的operator->返回的是指针(地址),那么我要想得到它的值应该再次->才能得到,这是因为编译器经过了特殊的处理,把其中一个->省略了。
operator++运算符重载
这里我们来补充一下:
如何理解:只要是给模板,就会导致不同类:
因为C++中,使用模板时,编译器会根据不同的模板参数类型来实例化,比如int就会实例化出int的类型,double就会实例化出double类型,它们并不是同一个类,模板只是不需要你是写,通过编译器它来实现帮你写了。
如果对前置++和后置++还不了解的小伙伴,我们可以去看看下面的文章:日期类的前置++和后置++的部分。
前置++
//因为我们返回值必须是实例化得到类型
//这里我们后面会讲,暂且把它看作T&或者const T&
self& operator++()
{
更新
_node = _node->_next;
return *this;
}
后置++
后置++
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
operator--运算符重载
跟++原理差不多一样。这里就不多讲解了。
前置--
self& operator--()
{
_node = _node->_prev;
return *this;
}
后置--
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
operator!=运算符重载
******这里需要注意的一点是******
这里的this指针为什么要加const?
下面用到了end()函数,我们先简单看看它实现的方式。我们来看一下在它!=使用时的情况:
iterator end() { return _head; }
while(it !=end()) { }
若没有加const,会报错,为什么呢?
it是迭代器类型对象,去调用operator!=,我们可以看到下面,给了一个&引用,然后,你调用了一个end(),调用的是一个函数,这里是一个传值返回,返回的不是一个_head,而是它的一个临时拷贝,生成临时对象,临时对象具有常性,所以要加const。ps(引用可以平移,缩小,但不能扩大)
bool operator!=(const self& it) const { return _node != it._node; }
operator==运算操作符
bool operator==(const self& it) const
{
return _node == it._node;
}
const迭代器
怎么实现list的迭代器呢?
现在,我们先来看看vector是怎么实现const迭代器的:
接着,我们再来复习一下C语言阶段const的位置不同情况:
1..const如果放在*的左边,保证指针指向的内容(值)不能通过指针来改变。
但是指针变量本⾝的内容(地址)可变
2• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容(地址)不能修改,但是指针指向的内容(值),可以通过指针改变。
那么,我们再来想一想,我们创建const迭代器的目的是什么?
保证const修饰的对象在使用迭代器时不能通过*或者->的访问后对它的值进行修改。
所以我们list的const迭代器也是需要的是不能改变它的值,符合使用const放到左边的情况,但是这里并不能像vector那样实现:
typedef const __list_iterator<T> const_iterator;
如果这样设计意味着把整个节点类型变成常量类型。这样不仅节点内存储的值不能修改,连迭代器移动(比如通过 ++ 、 -- 操作改变指向节点)都可能受限,因为迭代器本身变成了常量对象,无法满足遍历容器等正常操作需求 。
我们知道,迭代器是一种像指针一样的东西,const迭代器既要保证迭代器指向的值不能被改变,同时也要保证迭代器能像普通指针一样能够移动来遍历容器。
因此,我们正确的迭代器设计,通常是在迭代器的内部,即在operator*和operator->等等操作符,
typedef __list_const_iterator<T> const_iterator;
按照我们上面的说法是这样的情况:
template<class T>
struct __list_const_iterator
{
typedef list_node<T> Node;
Node* _node;
__list_const_iterator(Node* node)
:_node(node)
{}
const T& operator*()
{
return _node->_val;
}
__list_const_iterator<T>& operator++()
{
_node = _node->_next;
return *this;
}
__list_const_iterator<T> operator++(int)
{
__list_const_iterator<T> tmp(*this);
_node = _node->_next;
return tmp;
}
bool operator!=(const __list_const_iterator<T>& it)
{
return _node != it._node;
}
bool operator==(const __list_const_iterator<T>& it)
{
return _node == it._node;
}
};
但是,这里是不是就显得非常的冗余,为了解决这样的问题,而库里是这样实现:
借用模板来复用,合成一个
*********************之前******************************** typedef __list_iterator<T, T&, T*> iterator; typedef __list_iterator<T, const T&, const T*> const_iterator; ********************改后********************************** template<class T, class Ref, class Ptr> Ref=reference引用 Ptr=pointer指针
最后,当我们返回它们的类型时
类型就是:__list_iterator<T, Ref, Ptr>,这也比较长了,我们为了后面简便,所以重新typedef一下自定义命名成简单的代替它:
typedef __list_iterator<T, Ref, Ptr> self;
list类
定义成员变量
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
private:
Node* _head;
size_t _size;
};
初始化空链表
创建一个带头的哨兵位结点,并初始化。
void empty_init()
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
//更新size
_size = 0;
}
无参构造函数
list()
{
empty_init();
}
拷贝构造
1.功能:拷贝一个一模一样的链表
2.先初始化成一个新的带头链表
2.再原链表一个一个结点尾插进去新的链表。
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
清理函数
功能:清理链表中的所有结点,但不包括头哨兵,因为begin()的实现已经帮助我们解决了不包括头哨兵了,所有这里看似并没有做什么处理。
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
_size = 0;
}
析构函数
1.调用清理函数。
2.在释放之前单独new出来的变量。并置空。
~list()
{
clear();
delete _head;
_head = nullptr;
}
赋值函数
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;
}
begin()函数
功能:返回第一个结点的迭代器。
因为我们实现的链表是双向带头的链表。所以begin的位置不是_head,而是它的next
1.下面的这两种情况都是可以的:因为这里的迭代器的内部的构造函数是支持Node*类型指针来初始化自身的,所以直接返回Node*,不用强转也是可以的!因为编译器支持隐式类型转化
iterator begin()
{
//return _head->_next;
return iterator(_head->_next);
}
end()函数
功能:返回结点的最后一个结点的迭代器,也就是_head
iterator end()
{
return _head;
//return iterator(_head);
}
只读begin()
const_iterator begin() const
{
//return _head->_next;
return const_iterator(_head->_next);
}
只读end()
const_iterator end() const
{
return _head;
//return const_iterator(_head);
}
插入函数insert
这里的过程,看上面的图+之前我们对C语言双向循环链表。就能理解了,
注意的是:记得更新size
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
++_size;
return newnode;
}
尾插push_back()
直接调用insert函数
void push_back(const T& x) { insert(end(), x); }
头插push_front
void push_front(const T& x)
{
insert(begin(), x);
}
erase删除函数
iterator erase(iterator pos)
{
//检查删的不是头节点(空)
assert(pos != end());
//记录cur的前一个和后一个
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
//链接
prev->_next = next;
next->_prev = prev;
//删除释放
delete cur;
//更新size
--_size;
return next;
}
尾删pop_back
void pop_back()
{
erase(--end());
}
pop_front
void pop_front()
{
erase(begin());
}
size()函数
size_t size()
{
return _size;
}
好了,关于list的模拟实现就到这里了,希望你我共同进步!
附上总代码:
namespace bai
{
template<class T>
struct list_node
{
list_node<T>* next;
list_node<T>* prev;
T _val;
list_node(const T& val=T())
:next(nullptr)
,prev(nullptr)
,_val(val)
{ }
};
template<class T,class Ref,class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
Node* _node;
list_iterator(Node* node)
:_node(node)
{ }
Ref operator*()
{
return _node->_val;
}
//T* operator->()
//{
// return &_node->_val;
//}
Ptr operator->()
{
return &_node->_val;
}
list_iterator<T, Ref, Ptr>& operator++()
{
_node= _node->next;
return *this;
}
list_iterator<T, Ref, Ptr>& operator++(int)
{
list_iterator<T, Ref, Ptr> temp(*this);
_node = _node->next;
return temp;
}
list_iterator<T, Ref, Ptr>& operator--()
{
_node = _node->prev;
return *this;
}
list_iterator<T, Ref, Ptr>& operator--(int)
{
list_iterator<T> temp(*this);
_node = _node->prev;
return temp;
}
bool operator==(const list_iterator<T, Ref, Ptr>& it)
{
return _node == it._node;
}
bool operator!=(const list_iterator<T, Ref, Ptr>& it)
{
return _node != it._node;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef list_iterator<T,T&,T*> iterator;
typedef list_iterator<T,const T&,const T*> const_iterator;
iterator begin()
{
//return _head->next;
return iterator(_head->next);
}
iterator end()
{
return _head;
}
void empty_init()
{
_head = new Node;
_head->next = _head;
_head->prev = _head;
_size = 0;
}
list()
{
empty_init();
}
//
list(const list<T>& lt)
{
empty_init();
for (auto e : lt)
{
push_back(e);
}
}
void push_back(const T& x)
{
Node* tail = _head->prev;
Node* newnode = new Node(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = _head;
_head->prev = newnode;
}
void swap(list<T> lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list<T>& operator=(list<T><)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
it++;
}
_size = 0;
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
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;
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->prev;
Node* next = cur->next;
prev->next = next;
next->prev = prev;
delete cur;
_size--;
return next;
}
size_t size()
{
return _size;
}
private:
Node* _head;
size_t _size;
};
最后,到我们的鸡汤环节:
前进!