🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉前言👈
- 👉节点的创建👈
- 👉list 的构建👈
- list 的框架
- 无参的构造函数
- push_back 和 push_front
- 正向迭代器
- insert 和 erase
- pop_back 和 pop_front
- clear 和 析构函数
- 拷贝构造
- 赋值运算符重载
- 用 n 个 val 来构造对象
- size 和 empty
- 类名和类型
- front 和 back
- 完整代码
- 👉vector 和 list 的对比👈
- 👉总结👈
👉前言👈
上一篇博客介绍了 list 的基本使用,那么本篇博客就带着大家来模拟实现 list。模拟实现 list 之前需要注意几个问题:第一,为了避免和库函数产生命名冲突,我们需要将我们的代码封装在命名空间里。第二,我们模拟实现的 list 是带哨兵位头节点的双向循环链表。
👉节点的创建👈
链表是一个节点连接着一个节点的,所以我们首先要将节点创建出来。
namespace Joy
{
template <class T>
struct list_node
{
list_node* _prev;
list_node* _next;
T _data;
list_node(const T& val = T())
: _prev(nullptr)
, _next(nullptr)
, _data(val)
{}
};
}
注:因为节点存储的数据可以是内置类型,也可以是自定义类型,所以我们要讲节点定义成模板类。还有就是以下的代码都是封装在命名空间里。
👉list 的构建👈
list 的框架
我们已经将节点定义好了,那么我们现在就来搭建 list 的基本框架。因为我们实现的是带哨兵位头节点的双向循环链表,所以 list 的成员变量只需要哨兵位的头节点 _head
就行了。
template <class T>
class list
{
typedef list_node<T> node;
private:
node* _head; // 哨兵位头节点
};
注:为了方便使用list_node<T>
,我们可以将其重命名为node
。以下的函数接口均是 public 修饰。
无参的构造函数
无参的构造函数主要是申请哨兵位的头节点,然后该哨兵位头节点的
_prev
和_next
都指向自己。
// ...
list()
{
_head = new node; // 申请一个哨兵位头节点
_head->_next = _head;
_head->_prev = _head;
}
// ...
push_back 和 push_front
因为
_head->_prev
就是尾结点,所以根据双向循环链表的特性,我们就很容易将尾插函数写出来。
// ...
void push_back(const T& val)
{
node* tail = _head->_prev;
node* newnode = new node(x);
// _head tail newnode
tail->_next = newnode;
newnode->_prev = tail;
_head->_prev = newnode;
newnode->next = _head;
}
// ...
void push_front(const T& x)
{
node* head = _head->_next;
node* newnode = new node(x);
// _head newnode head
_head->_next = newnode;
newnode->_prev = _head;
newnode->_next = head;
head->_prev = newnode;
}
正向迭代器
迭代器是类的内嵌类型,所以我们使用迭代器时需要指定类域。迭代器用起来像是指针,支持解引用,++ 和 - -
其底层的实现不一定是原生指针,而 vector 的正向迭代器的底层就是原生指针。因为 vector 的空间是连续的,++ 和 - - 就能够找到后一个数据和前一个数据 。而链表的空间是不连续的,那么 ++ 和 - - 就不能找到后一个数据和前一个数据了。但是,我们又想支持这样的使用方法,那怎么办呢?我们可以将迭代器封装成一个类,然后利用运算符重载来支持 ++ 和 - - 的用法。
以上的做法也是 stl 源码中的实现方式。
// 像指针一样的对象
template <class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> Self; // Ref是T&,Ptr是T*,Self是迭代器
node* _pnode; // 正向迭代器的成员变量,_pnode是指向节点的指针
__list_iterator(node* pnode = nullptr)
: _pnode(pnode)
{}
// 因为T有可能是自定义类型,所以返回值设置为引用Ref,可以减少拷贝构造
Ref operator*() const
{
return _pnode->_data; // 返回节点的数据
}
// 比较节点的指针是否不相等即可
bool operator!=(const Self& it) const
{
return _pnode != it._pnode;
}
// 比较节点的指针是否相等即可
bool operator==(const Self& it) const
{
return _pnode == it._pnode;
}
// 返回节点数据的地址
Ptr operator->()
{
return &(operator*());
}
// 因为链表是通过指针连接起来的,那么_pnode = _pnode->_next就相当于迭代器++
// ++it,前置++的返回值为++之后的值
Self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
// it++,后置++的返回值为++之前的值
Self operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return *this;
}
// --it
Self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
// it--
Self operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
};
因为链表中的节点是通过指针来建立联系的,所以正向迭代器的成员变量就可以是节点指针
_pnode
了。那么_pnode = _pnode->_next
就相当于迭代器 ++。
为什么正向迭代器有三个模板参数?
正向迭代器有三个模板参数主要是为了避免代码冗余。因为除了实现没有
const
修饰的正向迭代器,我们还需要实现有const
修饰的正向迭代器,所以我们只需要修改模板参数的类型就能同时实现两个迭代器了,从而避免代码的冗余。
const 迭代器的易错点
const T* p1
和T* const p2
,const 迭代器类似 p1 的行为,保护指向的对象不被修改,迭代器本身可以修改。
为什么要有operator->函数重载?
struct Pos
{
int _row;
int _col;
Pos(int row = 0, int col = 0)
: _row(row)
, _col(col)
{}
};
void listTest5()
{
list<Pos> lt;
Pos p1(1, 1);
lt.push_back(p1);
lt.push_back(p1);
lt.push_back(p1);
lt.push_back(p1);
lt.push_back(Pos(2, 2));
lt.push_back(Pos(3, 3));
list<Pos>::iterator it = lt.begin();
while (it != lt.end())
{
cout << (*it)._row << ':' << (*it)._col << endl;
++it;
}
cout << endl;
}
有时候,链表的数据类型有可能是自定义类型。而我们想数据自定义类型的数据,这时候就可以通过流插入。如果不是使用流插入的话,可能会出现像上面(*it)._row
和(*it)._col
的写法。这样的写法并不好。那么为了解决这个问题,就需要借助operator->
重载函数了。这个函数返回的是节点数据的地址。
迭代器是否需要析构函数?
默认生成的析构函数对于自定义类型,会调用该自定义类型的析构函数;而对于内置类型,编译器不做处理。知道了这个,那么迭代器需不需析构函数就显而易见了。迭代器不需要析构函数,如果有析构函数的话,就会把链表的节点给释放掉。这是我们不希望看到的。那如果我们不写析构函数,默认生成的析构函数会不会做出处理呢?也不会,因为迭代器的成员变量是指针(内置类型),它不会轻易地释放这个资源。
迭代器是否需要深拷贝
之前我们说过,需要自己写析构函数的自定义类型,都需要自己写拷贝构造(深拷贝)。那么,很明显迭代器不需要深拷贝。因为迭代器不需要写析构函数,浅拷贝也能够完成任务。
typedef list_node<T> node;
typedef __list_iterator<T, T&, T*> iterator; // 正向迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator; // const正向迭代器
const_iterator begin() const
{
// const_iterator it(head->_next);
// return it
// 匿名对象
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
注:迭代器的
begin
就是_head->_next
,而迭代器的end
就是最后一个数据的下一个位置,也就是哨兵位头节点_head
。
现在正向迭代器就实现完了,那么我们通过下面的测试用例来测试一下迭代器写得对不对。
void listTest1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin(); // 拷贝够,迭代器浅拷贝就可以了
while (it != lt.end())
{
cout << *it << " "; // 调用operator*()函数
++it; // 调用operator++()函数
}
cout << endl;
it = lt.begin();
while (it != lt.end())
{
*it *= 2;
++it;
}
// 傻瓜式替换成迭代器,如果名字对不上就会报错
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
注:比较迭代器最好使用 != 或者 ==,而只有string 和1 vector 能够使用 > 和 < 来比较迭代器,因为这两个容器的空间是连续的。vector 的正向迭代器也不一定是原生指针,sgi 版(g++)的 vector 迭代器是原生指针,而 pj 版(VS)的 vector 迭代器不是原生指针。
迭代器的价值
- 封装底层实现,不暴露底层的实现细节
- 提供统一的访问方式,降低使用成本
insert 和 erase
iterator insert(iterator pos, const T& x)
{
node* cur = pos._pnode; // 迭代器中节点的指针
node* prev = cur->_prev;
node* newnode = new node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode); // 返回值为新插入节点的迭代器
}
iterator erase(iterator pos)
{
assert(pos != end()); // pos不能等于end()
node* cur = pos._pnode;
node* prev = cur->_prev;
node* next = cur->_next;
// prev cur next
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next); // 返回值为删除节点的下一个节点的迭代器
}
有了 insert 和 erase 函数,那么 push_back 和 push_front 函数就可以改成下面的样子了。
void push_back(const T& x)
{
insert(end(), x); // end()是哨兵位头节点
}
void push_front(const T& x)
{
insert(begin(), x); // begin()是第一个数据的迭代器
}
pop_back 和 pop_front
void pop_back()
{
/*node* tail = _head->_prev;
node* prev = tail->_prev;
// _head prev tail
_head->_prev = prev;
prev->_next = _head;
delete tail;*/
erase(--end()); // --end()为尾结点
}
void pop_front()
{
/*node* head = _head->_next;
node* next = head->_next;
// _head head next
_head->_next = next;
next->_prev = _head;
delete head;*/
erase(begin());
}
测试样例
void listTest2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " "; // 1 2 3 4 5
++it;
}
cout << endl;
it = lt.begin();
while (it != lt.end())
{
*it *= 2;
++it;
}
for (auto e : lt)
{
cout << e << " "; // 2 4 6 8 10
}
cout << endl;
lt.push_front(10);
lt.push_front(20);
lt.push_front(30);
lt.push_front(40);
lt.pop_back();
lt.pop_back();
for (auto e : lt)
{
cout << e << " "; // 40 30 20 10 2 4 6
}
cout << endl;
}
clear 和 析构函数
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it); // erase返回下一个位置的迭代器
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
clear 函数依次释放链表中的节点(除了哨兵位头节点),析构函数则需要将所有节点释放点。所有析构函数可以先复用 clear 函数,再释放哨兵位头节点,再将其置为
nullptr
。
拷贝构造
传统写法
void empty_init()
{
// 创建并初始化哨兵位头节点
_head = new node;
_head->_prev = _head;
_head->_next = _head;
}
// 拷贝构造传统写法 lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (const auto& e : lt) // 加引用避免自定义类型的拷贝构造
{
push_back(e);
}
}
现代写法
template <class InputInterator>
list(InputInterator first, InputInterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& x)
{
std::swap(_head, x._head); // 交换哨兵位的头节点
}
// 拷贝构造现代写法 lt2(lt1)
list(const list<T>& lt)
{
// 注意:_head不能为nullptr,因为链表至少要有哨兵位头节点
empty_init(); // 先初始化哨兵位的头节点,防止报错
list<T> tmp(lt.begin(), lt.end()); // 迭代器区间初始化
swap(tmp); // 交换哨兵位头节点
}
赋值运算符重载
传统写法
list<T>& operator=(const list<T>& lt)
{
if (this != <) // 防止自己给自己赋值
{
clear(); // 清理数据
for (const auto& e : lt)
{
push_back(e);
}
}
return *this;
}
现代写法
// l2 = l1
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
用 n 个 val 来构造对象
list(int n, const T& val = T())
{
empty_init();
for (int i = 0; i < n; ++i)
{
push_back(val);
}
}
size 和 empty
为了避免频繁调用 size 函数,降低效率,所以我们可以多加一个成员变量_size
。那么所以跟_size
有关的函数接口都需要修改。不过也可以采用不增加成员变量的方式,自己喜欢吧。
增加成员变量的写法
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
不增加成员变量的写法
size_t size() const
{
iterator it = begin();
size_t Size = 0;
while (it != end())
{
++Size;
++it;
}
return Size;
}
bool empty() const
{
return _head->_next == _head
&& _head->_prev == _head;
}
注:因为有了 size 和 empty 函数接口,所以之前的 erase、pop_back 等函数接口,都需要进行判空检查。
类名和类型
对于普通类而言,类名就等价于类型;对于类模板而言,类名不等于类型。如:
list
模板,类名list
,类型list<T>
。而在类模板里面可以用类名代表类型,但是建议不要那么用。但是在类外,类名不等同于类型。
front 和 back
T& front()
{
assert(!empty());
return *begin();
}
const T& front() const
{
assert(!empty());
return *begin();
}
T& back()
{
assert(!empty());
return *(--end());
}
const T& back() const
{
assert(!empty());
return *(--end());
}
完整代码
namespace Joy
{
template <class T>
struct list_node
{
list_node* _prev;
list_node* _next;
T _data;
list_node(const T& val = T())
: _prev(nullptr)
, _next(nullptr)
, _data(val)
{}
};
// 像指针一样的对象
template <class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> Self; // Ref是T&,Ptr是T*,Self是迭代器
node* _pnode; // 正向迭代器的成员变量,_pnode是指向节点的指针
__list_iterator(node* pnode = nullptr)
: _pnode(pnode)
{}
Ref operator*()
{
return _pnode->_data;
}
bool operator!=(const Self& it) const
{
return _pnode != it._pnode;
}
bool operator==(const Self& it) const
{
return _pnode == it._pnode;
}
Ptr operator->()
{
return &(operator*());
}
// ++it
Self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
// it++
Self operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return *this;
}
// --it
Self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
// it--
Self operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
};
template <class T>
class list
{
public:
typedef list_node<T> node;
typedef __list_iterator<T, T&, T*> iterator; // 正向迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator; // const正向迭代器
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
list()
{
/*_head = new node;
_head->_prev = _head;
_head->_next = _head;*/
empty_init();
}
void empty_init()
{
// 创建并初始化哨兵位头节点
_head = new node;
_head->_prev = _head;
_head->_next = _head;
_size = 0;
}
// 拷贝构造传统写法 lt2(lt1)
/*list(const list<T>& lt)
{
empty_init();
for (const auto& e : lt)
{
push_back(e);
}
}*/
template <class InputInterator>
list(InputInterator first, InputInterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& x)
{
std::swap(_head, x._head); // 交换哨兵位的头节点
std::swap(_size, x._size);
}
// 拷贝构造现代写法 lt2(lt1)
//list(const list& lt)
list(const list<T>& lt)
{
empty_init(); // 先初始化哨兵位的头节点,防止报错
list<T> tmp(lt.begin(), lt.end()); // 迭代器区间初始化
swap(tmp); // 交换哨兵位头节点
}
// 赋值运算符重载传统写法
/*list<T>& operator=(const list<T>& lt)
{
if (this != <) // 防止自己给自己赋值
{
clear(); // 清理数据
for (const auto& e : lt)
{
push_back(e);
}
}
return *this;
}*/
// 赋值运算符现代写法
//list& operator=(list lt)
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
list(int n, const T& val = T())
{
empty_init();
for (int i = 0; i < n; ++i)
{
push_back(val);
}
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
_size = 0;
}
// 析构函数
~list()
{
clear();
delete _head;
_head = nullptr;
}
void push_back(const T& x)
{
/*node* tail = _head->_prev;
node* newnode = new node(x);
// _head _tail newnode
tail->_next = newnode;
newnode->_prev = tail;
_head->_prev = newnode;
newnode->_next = _head;
++_size;*/
insert(end(), x);
}
void push_front(const T& x)
{
/*node* head = _head->_next;
node* newnode = new node(x);
// _head newnode head
_head->_next = newnode;
newnode->_prev = _head;
newnode->_next = head;
head->_prev = newnode;
++_size;*/
insert(begin(), x);
}
iterator insert(iterator pos, const T& x)
{
node* cur = pos._pnode; // 迭代器中节点的指针
node* prev = cur->_prev;
node* newnode = new node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode); // 返回值为新插入节点的迭代器
}
iterator erase(iterator pos)
{
assert(!empty());
assert(pos != end()); // pos不能等于end()
node* cur = pos._pnode;
node* prev = cur->_prev;
node* next = cur->_next;
// prev cur next
prev->_next = next;
next->_prev = prev;
delete cur;
--_size;
return iterator(next); // 返回值为删除节点的下一个节点的迭代器
}
void pop_back()
{
/*assert(!empty());
node* tail = _head->_prev;
node* prev = tail->_prev;
// _head prev tail
_head->_prev = prev;
prev->_next = _head;
delete tail;*/
erase(--end());
}
void pop_front()
{
/*assert(!empty());
node* head = _head->_next;
node* next = head->_next;
// _head head next
_head->_next = next;
next->_prev = _head;
delete head;
--_size;*/
erase(begin());
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
T& front()
{
assert(!empty());
return *begin();
}
const T& front() const
{
assert(!empty());
return *begin();
}
T& back()
{
assert(!empty());
return *(--end());
}
const T& back() const
{
assert(!empty());
return *(--end());
}
private:
node* _head; // 哨兵位头节点
size_t _size;
};
}
👉vector 和 list 的对比👈
注:vector 和 list 的对比是面试中非常喜欢考的知识点,比如:vector 的扩容问题、迭代器失效问题等等。
vector 的扩容问题
为什么 vector 的扩容倍数是二倍?因为二倍比较合适,扩容过多存在空间浪费问题;扩容过少会导致频繁扩容,影响效率。
vector 的 CPU 高速缓存命中率高
CPU 不会直接访问内存拿取内存中的数据,而是先将内存的数据加载到缓存中,然后 CPU 再去缓存获取想要的数据。而将内存的数据加载到缓存中并不是只加载一个数据,而是加载该数据及其后面一段的数据。为什么呢?根据局部性原理,你访问该数据,就有可能访问其周围的数据,所以就把其周围的数据也加载到缓存中,提高效率。因为 vector 的空间是连续的,所以其高速缓存命中率高。而 list 的空间是不连续的,高速缓存命中率不高,还会带来缓存污染的问题。
迭代器失效问题总结
对于 vector,insert 和 erase 函数接口的不正确使用都会带来迭代器失效问题。vector 的 insert 因为扩容问题而带来迭代器失效问题,而 erase 是因为迭代器的意义变了,即相对位置变了。如果再用该迭代器去访问数据就会导致无法意料的结果。而 list 只有 erase 函数接口会失效,因为其节点都被释放掉了。那么 string 会不会有迭代器失效问题呢?其实 string 也会有迭代器失效问题,insert 和 erase 也会导致迭代器失效,原因和 vector 的迭代器失效原因相似。但是因为不经常使用迭代器向 string 对象里插入数据或者删除数据,通常使用下标来插入或删除数据,所以我们不太关心 string 的迭代器失效问题。
👉总结👈
本篇博客主要介绍了 list 的模拟实现,重点的内容是正向迭代器的实现、vector 和 list 的对比和迭代器失效问题总结。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️