STL容器 --- 模拟实现 list
1. list 的底层结构
查看 stl_list.h 源码可以查到 list 的结点的底层结构为:
template <class T>
struct __list_node {typedef void* void_pointer;void_pointer next;void_pointer prev;T data;
参考源码我们自己来实现结点的结构,代码如下所示:
template<class T>
struct list_node
{T s_data;list_node<T>* s_prev;list_node<T>* s_next;
};
之前我们定义类时,默认使用的是 class,这里却使用 struct,为什么?
对于结点的结构我们不考虑使用访问限定符限制,默认所有成员都是公有的。因为在使用链表的时,需要经常访问控制这些成员,如果使用 class ,访问这些成员就会很麻烦(需要友元函数)。但是设置成公有之后,可以随意访问,这样不会产生问题吗?链表对外访问时,并不会暴露结点,不会产生问题。
结点的结构实现完毕,接下来实现 list 的结构,list 的原型为:template < class T, class Alloc = allocator<T> class list,先来看看源码中是怎么实现的,查看源码后可以发现 list 的成员变量只有一个 --- 哨兵结点,结点的类型为 list_node<T>,为了之后方便使用,可以将 list_node<T> typedef 为 Node:
template<class T>
class list
{typedef list_node<T> Node;private:Node* m_head;
};
2. 相关函数
接下来涉及插入和删除结点的操作,以及开辟新空间,拷贝旧数据,释放旧空间等在数据结构链表中涉及的操作,在这里都不过多赘述。
1. 无参的构造函数
初始化成员变量,list 是双向的带头的循环链表,与数据结构中所说的双链表的初始化一致,new 一个空间,让前驱指针和后继指针指向自己:
list()
{m_head = new Node;m_head->s_prev = m_head;m_head->s_next = m_head;
}
2. push_back 函数
push_back 函数的函数原型为:void push_back (const value_type& val),功能为:尾插结点。具体步骤不过多赘述:
void push_back(const T& val)
{// 创建新节点Node* newnode = new Node(val);// 找到尾结点Node* tail = m_head->s_prev;// m_head newnode tailm_head->s_prev = newnode;newnode->s_prev = tail;newnode->s_next = m_head;tail->s_next = newnode;
}
3. list_node 中的构造函数
在实现 push_back 函数时,新建结点,将 val 传给 list_node 类中的构造函数,接下来就来实现该函数,参数给全缺省值,那么缺省值是什么?是具体的 0 还是其他,都不是。结点可以存储的数据的类型有多种,就像 vector 一样,这里的缺省值给 T 类型的默认构造函数。代码如下所示:
list_node(const T& val = T()): s_data(val), s_prev(nullptr), s_next(nullptr)
{}
3. 迭代器
怎么实现链表的迭代器?底层为数组结构时,我们用简单的方式实现了迭代器。
之前说过迭代器可以使用原生指针来实现,如果这里我们仍然使用指针来实现迭代器,使用的是结点的指针 --- Node*。
然而原生指针不一定符合迭代器的行为的,只有底层是数组结构才符合,若使用结点的指针来实现 list 的迭代器,不符合迭代器的行为。
解引用之后不是结点的数据,是结点;++不能到达下一个结点,因为 list 的底层物理结构不连续,但是我们可以找到下一个结点,通过指针 s_next。
既然这些原本的运算符不能达到我们想要的效果,我们可以重载这些运算符,用类封装结点的指针,模拟实现普通指针访问数组的行为
最重要的是,源码中也是这样做的(省略一部分):
template<class T, class Ref, class Ptr>
struct __list_iterator {typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;typedef __list_iterator<T, Ref, Ptr> self;
}
迭代器用 Node*,无法达到预期行为;类封装 Node*,重载运算符,控制迭代器的行为。
1. 普通迭代器
知道怎么做了,下面需要考虑如何实现?既然是迭代器,自然就包含指针;其次就是重载那些运算符 --- * ,++ ,--,==,!= 。
先实现好迭代器的框架:
template <class T>
struct _list_iterator
{typedef list_node<T> Node;// 创建一个指针Node* s_node;// 初始化指针_list_iterator(Node* node): s_node(node){}
};
1. 重载 * 运算符
为什么要重载 * 运算符,因为 Node* 指针解引用后不是数据,而是结点;所以实现的函数返回值为 s_node->s_data。至于函数的返回值,当然是结点的数据类型 T&。代码如下所示:
// 重载 * 运算符 --- operator*
T& operator*()
{return s_node->s_data;
}
2. 重载前置++ 和前置-- 运算符
既然要像指针访问数组的行为,那么 ++ 之后,得到的指向下一个结点的指针;-- 之后得到的是指向上一个结点的指针。返回值类型当然是迭代器,返回值为 this 对象。代码如下所示:
// 重载前置++ 运算符 --- 不改变对象的内容
_list_iterator<T>& operator++()
{s_node = s_node->s_next;return *this;
}// 重载前置-- 运算符 --- 不改变对象的内容
_list_iterator<T>& operator--()
{s_node = s_node->s_prev;return *this;
}
3. 重载后置++ 和后置-- 运算符
后置++ 和后置-- 与前置++ 和前置-- 之间的区别在于是否改变了原来对象中的内容,前置运算符没有改变,后置运算符改变了。它们重载函数的区别在于,返回的是临时对象还是原对象,其他的细节与前置运算符重载函数并无二异。代码如下所示:
// 重载后置++ 运算符
_list_iterator<T>& operator++(int)
{_list_iterator tmp(*this);s_node = s_node->s_next;return tmp;
}// 重载后置-- 运算符
_list_iterator<T>& operator--(int)
{_list_iterator tmp(*this);s_node = s_node->s_prev;return tmp;
}
4. 重载== 和 != 运算符
== 和 != 比较的是两个迭代器对象的 s_node 是否相等和不相等,参数为迭代器对象,返回值类型为 bool。代码如下图所示:
// 重载 == 运算符
bool operator==(const _list_iterator<T>& it) const
{return s_node == it.s_node;
}// 重载 != 运算符
bool operator!=(const _list_iterator<T>& it) const
{return s_node != it.s_node;
}
2. const 迭代器
普通迭代器实现完毕后,接下来实现 const 迭代器。在之前我们实现 string 和 vector 容器的迭代器时,普通迭代器是T*,const版本的迭代器是const T*。如这般将 const _list_iterator<T> 重命名为 const_iterator,这样就变成本身不能被修改,反而指向的内容可以被修改,这样迭代器就不能++了,这样如何迭代?所以在实现 list 的 const 版本迭代器时,不能简单的像 string 和 vector 那样,将 const T* 重命名为 const_iterator 就完事了。
const 迭代器的特点是迭代器指向的内容不能被修改,在 list 中如何做到迭代器指向的内容不能被修改?换个角度来看这个问题,指向迭代器的内容是通过哪个函数来修改的?
通过 operator* 函数来修改的,只有将指针解引用了才能修改指针指向的内容。所以在 list 中如何做到迭代器指向的内容不能被修改,只需控制 operator* 函数即可。在原函数的基础上,在函数的前面加上 const 。其他函数都与普通迭代器相同,只是返回值类型由 _list_iterator 变成 _list_const_iterator 。
新建一个类为 _list_const_iterator:
template <class T>
struct _list_const_iterator
{typedef list_node<T> Node;Node* s_node;// 初始化指针_list_const_iterator(Node* node): s_node(node){}
};
operator* 函数:
const T& operator*()
{return s_node->s_data;
}
有人可能会想,为什么要单独再创建一个类呢?直接在普通迭代器类中重载 operator* 函数就可以了呀?
const T& operator*() const
{return s_node->s_data;
}
这样写不能达到目的,重载operator*函数,普通对象调用T& operator 函数,const 对象调用 const T& operator* 函数。const 迭代器能调用operator*函数,但是不能调用其他函数,如此一来 const 迭代器自身不能被修改了。
3. 迭代器的第一次优化
实现了普通迭代器和 const 迭代器之后,对比两个类的成员函数可以发现代码有些冗余,是否可以实现得更简洁一些?当然可以。两个迭代器区别在于 const ,在模板上再新增一个参数用于区分普通迭代器和 const 迭代器。
// 这里的 Ref 是 reference 引用的缩写
template <class T, class Ref>
struct _list_iterator
将之前 _list_iterator<T> 换成 _list_iterator<T, Ref>,为了避免写这么长串,重命名为 Self;而 operator* 函数的返回值类型变成 Ref。优化后的迭代器代码,如下所示:
template <class T, class Ref>
struct _list_iterator
{typedef list_node<T> Node;typedef _list_iterator<T, Ref> Self;// 创建一个指针Node* s_node;// 初始化指针_list_iterator(Node* node): s_node(node){}// 重载 * 运算符 --- operator*Ref& operator*(){return s_node->s_data;}// 重载前置++ 运算符 --- 不改变对象的内容Self& operator++(){s_node = s_node->s_next;return *this;}// 重载前置-- 运算符 --- 不改变对象的内容Self& operator--(){s_node = s_node->s_prev;return *this;}// 重载后置++ 运算符Self& operator++(int){Self tmp(*this);s_node = s_node->s_next;return tmp;}// 重载后置-- 运算符Self& operator--(int){Self tmp(*this);s_node = s_node->s_prev;return tmp;}// 重载 == 运算符bool operator==(const Self& it) const{return s_node == it.s_node;}// 重载 != 运算符bool operator!=(const Self& it) const{return s_node != it.s_node;}
};
普通版本迭代器和 const 版本迭代器是同一个类模板实例化的两个类型。
4. 迭代器相关函数
迭代器实现完毕后,在 list 中实现迭代器相关的函数:普通版本迭代器的 begin 和 end 函数,const 版本迭代器的 begin 和 end 函数。对应的 begin 和 end 函数的返回值类型为对应迭代器,可以将对应的迭代器重命名。
typedef _list_iterator<T, T&> iterator;
typedef _list_iterator<T, const T&> const_iterator;
begin 和 end 函数的作用分别为:返回指向第一个结点的迭代器,和返回指向最后一个结点的下一个接待你的迭代器。在我们实现的 list 中,第一个结点就是 m_head->s_next,最后一个结点的下一个结点就是 m_head。
代码:
iterator begin()
{return iterator(m_head->s_next);
}iterator end()
{return iterator(m_head);
}const_iterator begin() const
{return iterator(m_head->s_next);
}const_iterator end() const
{return iterator(m_head);
}
调用迭代器中的构造函数作为返回值。
5. 迭代器的第二次优化
先看一个场景:
struct XHM
{int s_left;int s_right;XHM(int left = 0, int right = 0): s_left(left), s_right(right){}
};void test()
{AY::list<XHM> lt;lt.push_back({ 1,2 });lt.push_back({ 2,3 });lt.push_back({ 3,4 });lt.push_back({ 4,5 });auto it = lt.begin();while (it != lt.end()){cout << (*it).s_left << ":" << (*it).s_right << endl;++it;}
}
list 的实例化类型为结构体,遍历访问 lt 中的数据时,不仅可以使用 * 也可以使用 ->。如下所示:
auto it = lt.begin();
while (it != lt.end())
{cout << it->s_left << ":" << it->s_right << endl;++it;
}
但是由于迭代器中没有重载 -> 运算符,所以上述代码在运行时会报错。对于自定义类型想要使用这些运算符,需要对这些运算符进行重载操作,operator-> 函数怎么写呢?
T* operator->()
{return &(s_node->s_data);
}
取 data 的地址,返回指针,但是结合场景代码来看,似乎有些不太对劲。It->xxx 调用函数It.operator->() 该函数得到的是 T*。从语法逻辑来看,实际上是两个->,应该是 it->->xxx 第一个 -> 是运算符重载,第二个 -> 是访问结构体指针指向的内容。但是为了可读性,省略了一个 -> 。
什么时候迭代器需要使用到->运算符,当 list 实例化为结构体/类对象。所以为了控制 operator-> 函数,需要新增模板参数。
template <class T, class Ref, class Ptr>
第三个模板参数 Ptr 用于控制 operator-> 函数。新增模板参数之后,将涉及到的地方修改一下
1. typedef _list_iterator<T, Ref> Self 改为 typedef _list_iterator<T, Ref, Ptr> Self
2. typedef _list_iterator<T, T&> iterator 改为 typedef _list_iterator<T, T&, T*> iterator
3. typedef _list_iterator<T, const T&> const_iterator 改为 typedef _list_iterator<T, const T&, const T*> const_iterator
最后再将 operator-> 函数的返回值类型改为 Ptr :
Ptr operator->()
{return &(s_node->s_data);
}
6. 插入和删除函数
1. insert 函数
insert 函数的函数原型为:iterator insert (iterator position, const value_type& val),返回值为:指向第一个新插入元素的迭代器。
代码:
iterator insert(iterator pos, const T& val)
{// 创建新节点Node* newnode = new Node(val);Node* cur = pos.s_node;Node* prev = cur->s_prev;// prev newnode curprev->s_next = newnode;newnode->s_prev = prev;newnode->s_next = cur;cur->s_prev = newnode;// 返回指向第一个插入元素的迭代器 --- nenwodereturn newnode;// return iterator(newnode);
}
2. erase 函数
erase 函数的函数原型为:iterator erase (iterator position),返回值为:指向删删除元素之后的元素的迭代器。
代码为:
iterator erase(iterator pos)
{Node* cur = pos.s_node;Node* prev = cur->s_prev;Node* next = cur->s_next;prev->s_next = next;next->s_prev = prev;delete cur;// 返回删除元素的下一个元素的迭代器 --- nextreturn next;// return iterator(next);
}
return xxx 构造一个匿名对象,涉及隐式类型转换;return iterator() 调用了迭代器类中的构造函数
3. push_front 和 push_back 函数
这两个函数的函数原型分别为:void push_front (const value_type& val) 和 void push_back (const value_type& val)。这两个函数可以复用 insert 函数。
代码为:
// push_front 函数 -- 头插
void push_front(const T& val)
{insert(begin(), val);
}// push_back 函数 --- 尾插
void push_back(const T& val)
{insert(end(), val);
}
4. pop_front 和 pop_back 函数
这两个函数的函数原型分别为:void pop_front() 和 void pop_back()。这两个函数可以复用 erase 函数。
代码为:
// pop_front 函数 --- 头删
void pop_front()
{earse(begin());
}// pop_back 函数 --- 尾删
void pop_back()
{// end 函数的返回值为 m_head,它不能动// 删除的是 m_head->s_prev,也就是 --end()erase(--end());
}
7. 其他函数
1. clear 函数和析构函数
clear 函数的函数原型为:void clear(),功能为:删除所有的结点(哨兵结点除外)。
代码为:
// clear 函数
void clear()
{// 删除所有结点auto it = begin();while (it != end()){// 迭代器失效 --- 更新迭代器it = erase(it);}
}
析构函数的函数原型为:~list(),功能为:清除所有数据。该函数可以复用 clear 函数。
代码为:
~list()
{clear();// 删除哨兵结点delete m_head;m_head = nullptr;
}
2. 拷贝构造函数和花括号初始化函数
拷贝构造函数的函数原型为:list (const list& x),功能为:将一个对象中的数据拷贝到另一个对象中。
代码为:
list(const list<T>& lt)
{//创建新节点m_head = new Node;m_head->s_prev = m_head;m_head->s_next = m_head;// 拷贝数据for (const auto& e : lt){push_back(e);}
}
花括号初始化函数的函数原型为:list (initializer_list<value_type> il, const allocator_type& alloc = allocator_type()),功能为 vector 容器中的一致。
代码为:
list(initializer_list<T> il)
{//创建新节点m_head = new Node;m_head->s_prev = m_head;m_head->s_next = m_head;// 拷贝数据for (const auto& e : il){push_back(e);}
}
3. 赋值重载函数
赋值重载函数的函数原型为:list& operator= (const list& x),功能为:清除调用对象原本的数据,再将等号右边的对象的内容拷贝到调用对象中。与 vector 一致,拥有两种写法,一种普通写法,一种特殊写法。
第一种写法 --- 普通写法
代码:
list<T>& operator=(const list<T>& lt)
{// 避免自己赋值自己if (*this != lt){// 清空 this 对象中的数据clear();// 拷贝数据for (const auto& e : lt){push_back(e);}}return *this;
}
第二种写法 --- 特殊写法(借助 swap 函数)
代码:
void swap(list<T>& lt)
{std::swap(m_head, lt.m_head);
}list<T>& operator=(list<T>& lt)
{swap(lt);
}
4. size 函数
size 函数的函数原型为:size_type size(),功能为:统计有效数据个数。遍历链表即可。
代码:
size_t size()
{size_t count = 0;for (auto& e : *this){++count;}return count;
}
如果觉得这样实现起来函数的时间复杂度过高,可以在 list 类中新增一个成员变量 size,再修改那些插入和删除数据的函数即可,每插入结点,size 就++;每删除结点,size 就--。
8. 模板未实例化问题
下面看一个场景:
template<class T>
void Print(const list<T>& lt)
{list<T>::const_iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
}
类模板未实例化,不能去类模板中找后面代码所对应的东西,编译器分不清 const_iterator 是嵌套类型,还是静态成员变量
为了解决这个问题,可以在改行代码的开头加上 typename,告诉编译器,确认这里是类型
template<class T>
void Print(const list<T>& lt)
{typename list<T>::const_iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
}
当然还有一劳永逸的方法,将 it 的类型写为 auto,让编译器自己去识别 it 是什么类型。
template<class T>
void Print(const list<T>& lt)
{auto it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
}