当前位置: 首页 > news >正文

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 一样,这里的缺省值给 类型的默认构造函数。代码如下所示:

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;
}

http://www.dtcms.com/a/393904.html

相关文章:

  • Java LTS版本进化秀:从8到21的欢乐升级之旅
  • yolo转tensorrt nano
  • paimon实时数据湖教程-分桶详解
  • kafka集群部署
  • Windows系统安装OpenSSL库最新版方法
  • 因果推断:关于工具变量的案例分析
  • 字节面试题:激活函数选择对模型梯度传播的影响
  • 5.Spring AI Alibaba
  • 如何优化Java并发编程以提高性能?
  • 【重量上下限报警灯红黄绿】2022-12-13
  • Node.js后端学习笔记:Express+MySQL
  • Ubuntu24.04 安装 禅道
  • StandardScaler,MinMaxScaler 学习
  • vscode+ssh连接server
  • 一文快速入门 HTTP 和 WebSocket 概念
  • Vue.js 项目创建指南
  • 核心策略、高级技巧、细节处理和心理
  • 算法优化的艺术:深入理解 Pow(x, n) 及其背后的思考
  • Projection Approximation Subspace Tracking PAST 算法
  • 容器化简单的 Java 应用程序
  • 【实证分析】上市公司并购数据dofile数据集(2005-2024年)
  • OceanBase备租户创建(三):通过带日志的物理备份恢复
  • OceanBase用户和权限管理
  • VMware Workstation Pro 虚拟机为 Ubuntu 18 配网教程
  • 城市自然资源资产离任审计试点DID
  • 算法日记---新动计划
  • Vue3水波纹指令:2025年Material Design交互新标准
  • Ansible-yum_repository模块
  • Java 单元测试(JUnit)与反射机制深度解析
  • Spring MVC 入门:构建 Web 应用的核心框架