【C++】list的实现

文章目录

  • [一、 前言](#一、 前言)
  • [二、 基本框架](#二、 基本框架)
    • [1. 铺垫](#1. 铺垫)
    • [2. list](#2. list)
    • [3. list_node](#3. list_node)
  • 三、模拟实现
    • [1. 构造函数](#1. 构造函数)
    • [2. 析构函数](#2. 析构函数)
    • [3. 拷贝构造函数](#3. 拷贝构造函数)
    • [4. 赋值运算符重载](#4. 赋值运算符重载)
    • [5. 迭代器](#5. 迭代器)
    • 6.begin()和end()
    • [7. push_back、push_front和insert](#7. push_back、push_front和insert)
    • [8. pop_back、pop_front和erase](#8. pop_back、pop_front和erase)
    • [9. size](#9. size)
    • [10. 打印函数(用于测试)](#10. 打印函数(用于测试))
    • [11. 总结](#11. 总结)
  • [四、 源代码](#四、 源代码)
    • [1. list.h](#1. list.h)
    • [2. test.cpp](#2. test.cpp)

一、 前言

上期我们介绍了list的各种接口,那么接下来我们就来自己模拟实现一个基本的list,------>>>点击查看《list的使用》,不过在实现list的之前,还是需要我们对数据结构中的链表有一定的了解的,我们这里的list就是一个双向链表

  1. 圆表示哨兵位
  2. 矩形表示节点

二、 基本框架

1. 铺垫

这里我们的list还是使用模板来实现的,所以我们依然是声明与定义不分离,我们创建了两个文件,分别为list.h和test.cpp,下面我们来介绍一下list需要实现的类

2. list

cpp 复制代码
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;
};
  1. 这里是我们list最基本的结构,我们这里的list是用来存储各个节点的地址的,其中_head就是我们的哨兵位头节点,_size就是list中有效数据的个数
  2. 至于list的各个节点,我们是需要再定义一个类的,我们在C语言的时候学习链表的时候应该是没有这样的思想的,我们可以这样理解,节点是装数据的小盒子,而容器是管理所有盒子的管家,节点只存数据和指针,不参与任何管理逻辑,容器只管理节点,不关心节点内部怎么存数据
  3. 这里我们为了方便书写,我们就把list_node使用typedef对节点的类型进行重命名为Node,另外两个typedef是list的普通对象的迭代器和const对象的迭代器,我们在后续实现会详细讲述

3. list_node

cpp 复制代码
template<class T>
struct list_node
{
	T _data;
	list_node<T>* _next;
	list_node<T>* _prev;

	list_node(const T& data = T())
		:_data(data)
		,_next(nullptr)
		,_prev(nullptr)
	{ }
};
  1. 这就是我们用来存放数据和前后关系的节点类,其中data就是我们要存储的数据,_next和_prev分别是后继节点的指针和前驱节点的指针
  2. 注意这个类我们是定义成一个struct了,为什么要写成struct而不是class呢?因为struct内默认是public成员,我们的list_node是需要被频繁访问节点关系的,如果写成private成员就会很麻烦
  3. 我们这里是设计好了节点类的构造函数的,这里的缺省值我们之前也介绍过,会调用对应类型的默认构造函数来进行初始化,C++对于内置类型也是可以调用默认构造函数进行构造出对象

三、模拟实现

1. 构造函数

cpp 复制代码
void empty_init()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	_size = 0;
}

list()
{
	empty_init();
}
  1. 我们注意到,我们这里是实现了一个空list初始化函数,然后构造函数是通过复用这个函数来实现的
  2. 这里我们实现empty_init函数主要是来初始化出来一个哨兵位,我们这里单独设计出来的用处可以在后面的拷贝构造函数看出用处

2. 析构函数

cpp 复制代码
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}
  1. 这里我们的析构函数主要是通过复用clear函数来实现的,而我们的clear函数是通过复用erase函数来实现的,下面会分点来粗略说一下它们之间的复用逻辑,我们后续会更详细的讲解的
  2. erase就是删除指定位置的节点,我们在后面会模拟实现
  3. clear这里还涉及了迭代器的内容,我们现在只需要知道clear的作用是清空除了哨兵位以外的所有节点就可以了,等我们自己模拟实现出来迭代器后这里自然就可以看懂了
  4. 复用clear之后,我们的list就只剩下哨兵位了,我们单独把哨兵位释放掉就可以了

3. 拷贝构造函数

cpp 复制代码
list(const list<T>& lt)
{
	empty_init();
	for (auto& e : lt)
	{
		push_back(e);
	}
}
  1. 这里就可以看出来为什么我们在构造函数那里要把empty_init单独实现出来了,我们在拷贝构造函数这里也是需要复用的
  2. 为什么拷贝构造函数需要复用这个呢?我们要知道拷贝构造函数是利用一个已经存在的对象来构造一个不存在的对象的,那么我们就要首先给这个不存在的对象来初始化出来一个哨兵位才满足我们实现的list的结构

4. 赋值运算符重载

cpp 复制代码
list<T>& operator=(list<T> lt)
{
	swap(lt);
	return *this;
}

void swap(list<T>& lt)
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}
  1. 这里的赋值运算符重载我们是通过复用swap函数来实现的,这就是我们的现代写法
  2. 这里我们单独设计出来swap来实现哨兵位和节点个数的互换
  3. 这里的赋值运算符重载传参时一定不能写成引用,如果写成引用我们就会改变原始的另外一个对象,这不是我们希望的

5. 迭代器

cpp 复制代码
template<class T, class Ref, class Ptr>
struct list_iterator
{
	typedef list_node<T> Node;
	typedef list_iterator<T, Ref, Ptr> Self;
	Node* _node;

	list_iterator(Node* node)
		:_node(node)
	{ }

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;
		return tmp;
	}

	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	bool operator!=(const Self& s) const
	{
		return _node != s._node;
	}

	bool operator==(const Self& s) const
	{
		return _node == s._node;
	}
};
  1. 这里就是我们list容器最难的部分了,下面一起来详细拆解一下
  2. 首先我们先从模板的设计来看一下,我们这里的模板是传入了三个类型的,分别为数据类型,该类型的引用和该类型的指针,为什么这样设计呢,主要是我们这里可以控制我们这里是普通对象的迭代器还是const对象的迭代器,我们要理解这个内容呢,就需要回头去看一下我们在list类中是如何typedef迭代器的,我们可以看下面的两个typedef,对于普通对象的迭代器,我们传入的是该数据的类型、该数据类型的引用以及该数据类型的指针,而对于const对象呢,我们传入的就是该数据的类型、该数据的const引用以及该数据的const指针
cpp 复制代码
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
  1. 我们这里的迭代器也是设计成struct了,因为我们也是需要频繁的使用迭代器的
  2. 这里我们为了方便书写,依然是把list_node使用typedef定义成了Node,为了方便书写迭代器类型,我们这里把迭代器自己定义成了Self
  3. 我们这里定义了一个节点类型的成员变量,为了后续方便传参和返回
  4. 首先我们第一个方法是重载了operator*,我们是希望我们通过解引用迭代器后就可以找到对应的数据的,所以我们返回的是对应类型的引用,这里可以是普通对象的引用,也可以是const对象的引用
  5. 接下来我们重载了->,这里我们下面会给大家一个例子帮助理解,这里我们就是返回了对应类型的指针,针对下面的例子,我们肯定是希望我们可以通过迭代器来访问到A类型中的成员变量的,所以我们重载->是必须的 ,我们还需要注意我们在例子中的注释,->重载只是返回了对应类型的指针但是还是访问不到对应类型的成员变量,我们应该是调用这个重载方法后继续跟一个->才能访问到对应类型的成员变量,而编译器为了可读性就省略了一个->,看起来就像是我们在结构体那里访问成员变量引用
cpp 复制代码
struct A
{
	int _a1 = 1;
	int _a2 = 2;
};

list<A> lta;
lta.push_back(A());
lta.push_back(A());
lta.push_back(A());
lta.push_back(A());
list<A>::iterator it = lta.begin();
while (it != lta.end())
{
	// 本来应该是两个->,编译器为了可读性省略了一个->
	//cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << " ";
	cout << it->_a1 << ":" << it->_a2 << " ";
	++it;
}
cout << endl;

for (auto e : lta)
{
	cout << e._a1 << ":" << e._a2 << " ";
}
cout << endl;
  1. 在我们前面实现的string以及vector中,它们的数据在底层是连续的,所以想要访问到下一个或者上一个位置的迭代器的话直接++/--就可以了,但是我们的list不一样,它的节点在内存中是不连续的,如果我们想要迭代器能够移动就必须重载++/--,我们是可以通过节点中的_next和_prev找到后继节点和前驱节点的,所以我们的++/--的重载只需要返回对应移动后的this指针就可以了,++就是先让_node移动到下一个位置然后返回对应的this指针,--就是让_node移动到上一个位置然后返回对应的this指针 ,不知道会不会有朋友想着为什么不直接返回_node->_next或者_node->_prev呢?原因就是这压根不是同一种类型,我们可以看到,我们的++/--重载返回的是迭代器自身,而_node->_next或者_node->_prev是Node*类型,编译器无法把 Node* 转换成 Self&
  2. 在上述所说的++/--重载中,我们实现的都是前置,我们这里来说一下如何实现后置,我们传入一个int参数就可以实现后置了,但是我们要注意不要引用返回,我们返回的是tmp这个局部对象,出了作用域就会销毁
  3. 剩下就是重载一下==和!=了,这里只需要比较一下地址就可以了

6.begin()和end()

cpp 复制代码
iterator begin()
{
	/*iterator it(_head->_next);
	return it;*/

	//return iterator(_head->_next);

	return _head->_next;
}

iterator end()
{
	return _head;
}

const_iterator begin() const 
{
	return _head->_next;
}

const_iterator end() const
{
	return _head;
}
  1. 这里分别实现了普通对象的迭代器和const对象的迭代器
  2. 注意我们在begin那里的注释,我们在迭代器详解那里可以看到,我们迭代器是实现了构造函数的,所以我们这里是一个隐式类型转换,剩下的几个方法也是同理的

7. push_back、push_front和insert

cpp 复制代码
void push_back(const T& x)
{
	/*Node* newnode = new Node(x);
	Node* tail = _head->_prev;

	tail->_next = newnode;
	newnode->_prev = tail;
	newnode->_next = _head;
	_head->_prev = newnode;

	++_size;*/
	insert(end(), x);
}

void push_front(const T& x)
{
	insert(begin(), x);
}

iterator insert(iterator pos, const T& x)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(x);

	newnode->_next = cur;
	cur->_prev = newnode;
	newnode->_prev = prev;
	prev->_next = newnode;

	++_size;

	return newnode;
}
  1. 我们注意到,我们这里的push_back和push_front都是对insert的复用,所以我们这里就来细细看一下insert的实现了
  2. 学过数据结构的朋友看这段逻辑应该是很容易理解的,我们只需要新建一个节点插入到指定位置之前就可以了,我们这里设计了返回值,也算是更新一下迭代器了,但是我们的insert是不涉及迭代器失效的,我们的pos是一直指向对应的节点的
  3. 对于push_back,我们是希望插入最尾部的,对于双向链表来说,我们就需要插入到_head的前面,所以我们正好传入end(),对于push_front来说,我们希望它插入最前面,所以我们插入到_head的下一个节点的前面就行,所以我们正好传入begin()

8. pop_back、pop_front和erase

cpp 复制代码
void pop_back()
{
	erase(--end());
}

void pop_front()
{
	erase(begin());
}

iterator erase(iterator pos)
{
	assert(pos != end());
	Node* prev = pos._node->_prev;
	Node* next = pos._node->_next;

	prev->_next = next;
	next->_prev = prev;

	delete pos._node;

	--_size;

	return next;
}
  1. 这里的pop_back和pop_front也是通过复用erase来实现的,所以我们也是重点来看一下erase的实现
  2. 我们的erase只是删除指定位置的节点,但是erase这里是有迭代器失效的,所以我们返回一个迭代器来更新迭代器是必须的

9. size

cpp 复制代码
size_t size() const
{
	return _size;
}
  1. 这里是我们自己设计的list的一个小接口,在基本框架那里我们可以看到我们的list是设计了size的成员变量的,方便我们获取有效节点个数

10. 打印函数(用于测试)

cpp 复制代码
template<class T>
void print_list(const list<T>& lt)
{
	// const iterator ---》迭代器本身不能修改
	// const_iterator ---》指向的内容不能修改
	// 这两者是不同的
	typename list<T>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}
  1. 我们在vector那里也实现了这样一个函数,方便我们测试的时候来打印
  2. 这里我们需要注意的是我们注释那里的内容,所以拿出来单独展示给大家看了

11. 总结

  1. list这里最需要理解的有两个点
  2. 一个是为什么我们要分别设计出来list类和节点类,另外一个就是我们的迭代器的相关内容了,希望大家可以认真思考,有疑问欢迎讨论,有问题欢迎大家指出

四、 源代码

1. list.h

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace William
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& data = T())
			:_data(data)
			,_next(nullptr)
			,_prev(nullptr)
		{ }
	};

	template<class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;
		Node* _node;

		list_iterator(Node* node)
			:_node(node)
		{ }

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._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()
		{
			/*iterator it(_head->_next);
			return it;*/

			//return iterator(_head->_next);

			return _head->_next;
		}

		iterator end()
		{
			return _head;
		}

		const_iterator begin() const 
		{
			return _head->_next;
		}

		const_iterator end() const
		{
			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);
			}
		}

		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		void push_back(const T& x)
		{
			/*Node* newnode = new Node(x);
			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;

			++_size;*/
			insert(end(), x);
		}

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;

			++_size;

			return newnode;
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* prev = pos._node->_prev;
			Node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;

			delete pos._node;

			--_size;

			return next;
		}

		size_t size() const
		{
			return _size;
		}

	private:
		Node* _head;
		size_t _size;
	};

	template<class T>
	void print_list(const list<T>& lt)
	{
		// const iterator ---》迭代器本身不能修改
		// const_iterator ---》指向的内容不能修改
		// 这两者是不同的
		typename list<T>::const_iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void test1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		print_list(lt);
	}

	void test2()
	{
		struct A
		{
			int _a1 = 1;
			int _a2 = 2;
		};

		list<A> lta;
		lta.push_back(A());
		lta.push_back(A());
		lta.push_back(A());
		lta.push_back(A());
		list<A>::iterator it = lta.begin();
		while (it != lta.end())
		{
			// 本来应该是两个->,编译器为了可读性省略了一个->
			//cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << " ";
			cout << it->_a1 << ":" << it->_a2 << " ";
			++it;
		}
		cout << endl;

		for (auto e : lta)
		{
			cout << e._a1 << ":" << e._a2 << " ";
		}
		cout << endl;
	}

	void test3()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		// insert以后迭代器不失效
		list<int>::iterator it = lt.begin();
		lt.insert(it, 10);
		*it += 100;
		print_list(lt);

		// erase以后迭代器失效
		it = lt.begin();
		while (it != lt.end())
		{
			if (*it % 2 == 0)
			{
				it = lt.erase(it);
			}
			else it++;
		}

		print_list(lt);
	}

	void test4()
	{
		list<int> lt1;
		lt1.push_back(1);
		lt1.push_back(2);
		lt1.push_back(3);
		lt1.push_back(4);
		print_list(lt1);

		list<int> lt2(lt1);
		print_list(lt2);

		list<int> lt3;
		lt3.push_back(10);
		lt3.push_back(20);
		lt3.push_back(30);
		lt3.push_back(40);
		lt1 = lt3;
		print_list(lt1);
	}
}

2. test.cpp

cpp 复制代码
#include"list.h"

int main()
{
	William::test4();
	return 0;
}
相关推荐
艾莉丝努力练剑2 小时前
【Linux系统:信号】线程安全不等于可重入:深度拆解变量作用域与原子操作
java·linux·运维·服务器·开发语言·c++·学习
楼田莉子2 小时前
同步/异步日志系统:日志的工程意义及其实现思想
linux·服务器·开发语言·数据结构·c++
kpl_202 小时前
特殊类设计、类型转换和IO流(C++)
c++
牢姐与蒯2 小时前
栈和队列的实现
c++
cccyi72 小时前
【C++ 脚手架】brpc 的介绍与使用
c++·rpc·brpc
小肝一下2 小时前
每日两道力扣,day4
c++·算法·leetcode·职场和发展
paeamecium3 小时前
【PAT甲级真题】- Talent and Virtue (25)
数据结构·c++·算法·pat
Mr_Xuhhh3 小时前
蓝桥杯复习清单真题(C++版本)
c++·算法·蓝桥杯
tankeven3 小时前
HJ163 时津风的资源收集
c++·算法