List的模拟实现

List的模拟实现

1、节点和链表的定义

由于C++实现的List是一个带头双向循环链表,所以我们这样设计节点:

cpp 复制代码
template<class T>
struct List_node
{
	T _data;
	struct List_node* next;
	struct List_node* prev;

	// 构造
	List_node(const T& val = T())// 匿名对象做参数
		:_data(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}
};

这样初步设计链表:

cpp 复制代码
template<class T>
class list
{
public:
	using node = List_node<T>;// 重命名,更方便,防止漏类型参数T

	// 构造
	list()
		:_head(new node)
		,_size(0)
	{
		_head->_prev = _head;
		_head->_next = _head;
	}
	
private:
	node* _head;// 头节点(哨兵位)
	size_t _size;// 记录节点个数
};

2、push_back()

由于链表被创建时就有一个哨兵位,所以我们就不需要对链表是否为空的情况进行讨论,直接push_back:

cpp 复制代码
// 尾插
void push_back(const T& val)
{
	node* newnode = new node(val);
	node* tail = _head->_prev;
	newnode->_next = _head;
	newnode->_prev = tail;
	tail->_next = newnode;
	_head->_prev = newnode;

	++_size;
}

3、迭代器

我们实现List的迭代器,还能直接使用指针吗?

之前的vector,使用的是连续的int类型空间,我们将指针重命名成迭代器,就可以很好地完成自增、自减、访问数据。

那List呢?也是直接用指针吗?

List使用的是一个个节点。如果我们使用节点指针访问,得到的是整个节点而不是其中的数据,不符合逻辑;节点与节点之间也不是连续的,不能通过节点指针的+、-来定位某一个节点。

我们只能将List的节点封装成类,在类中通过运算符重载来重定义行为。

迭代器的设计是一种封装。封装的目的是隐藏底层的结构差异,以提供一个统一的方法访问容器。

类中可以通过运算符重载,重定义行为。

3.1、迭代器的设计

迭代器的本质是对节点指针的封装。

cpp 复制代码
template<class T>
struct List_iterator// 实现为公有
{
	using node = List_node<T>;// 重命名

	node* _nodeptr;// 底层为节点指针

	List_iterator(node* nodeptr = node*())
		:_nodeptr(nodeptr)// 构造
	{}
};

迭代器不需要析构。迭代器的拷贝只需浅拷贝。

我们加上几个重要的方法:重载*、重载前置++、重载!=。

cpp 复制代码
template<class T>
struct List_iterator
{
	using node = List_node<T>;

	node* _nodeptr;

	List_iterator(node* nodeptr)
		:_nodeptr(nodeptr)
	{}

	T& operator*() { return _nodeptr->_data; }

	List_iterator<T>& operator++()
	{
		_nodeptr = _nodeptr->_next;
		return *this;
	}

	bool operator!=(const List_iterator<T> it)
	{// *this与it一定不一样,this和it的_nodeptr可能一样
		return _nodeptr != it._nodeptr;
	}// 如果直接this != &it,会导致死循环
};

我们在List中简单实现begin(), end():

  • begin():本质是指向哨兵位的下一个节点指针
  • end():本质是指向哨兵位的节点指针

然后,我们使用范围for,结果是可以正常使用:

实现了迭代器,我们就要实现const_迭代器。

我们很容易想到:根据迭代器的实现,再实现一份const迭代器。

cpp 复制代码
template<class T>
struct List_const_iterator
{
	using node = List_node<T>;

	node* _nodeptr;

	List_const_iterator(node* nodeptr)
		:_nodeptr(nodeptr)
	{}

	const T& operator*() { return _nodeptr->_data; }// 只有*重载不一样

	List_const_iterator<T>& operator++()
	{
		_nodeptr = _nodeptr->_next;
		return *this;
	}

	bool operator!=(const List_const_iterator<T> it)
	{
		return _nodeptr != it._nodeptr;
	}
};

照抄出一份const迭代器,List_iterator都要改为List_const_iterator,太麻烦了。

我们可以做一步优化:迭代器的类名重命名成Self。

cpp 复制代码
// 迭代器
template<class T>
struct List_iterator
{
	using node = List_node<T>;
	using Self = List_iterator<T>;

	node* _nodeptr;

	List_iterator(node* nodeptr)
		:_nodeptr(nodeptr)
	{}

	T& operator*() { return _nodeptr->_data; }

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

	bool operator!=(const Self it) const { return _nodeptr != it._nodeptr; }
};

// const迭代器
template<class T>
struct List_const_iterator
{
	using node = List_node<T>;
	using Self = List_const_iterator<T>;

	node* _nodeptr;

	List_const_iterator(node* nodeptr)
		:_nodeptr(nodeptr)
	{}

	const T& operator*() { return _nodeptr->_data; }

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

	bool operator!=(const Self it) const { return _nodeptr != it._nodeptr; }
};

这样一来,我们照抄的时候,就只需要改变一两个地方。这也体现了复用的思想。

值得注意的是,构造函数的函数名,不能用Self代替。

但是,我们是否可以再做一步优化?

乍一看,迭代器和const迭代器非常相似,只在解引用的返回值有区别,因为const迭代器指的是修饰const对象的迭代器,而const对象是不能被修改的。

我们能否合二为一,只在解引用的返回值的返回做区别?答案是:可以,在模板参数列表部分再加一个参数Ref

cpp 复制代码
// 迭代器
template<class T, class Ref>
struct List_iterator
{
	using node = List_node<T>;
	using Self = List_iterator;

	node* _nodeptr;

	List_iterator(node* nodeptr)
		:_nodeptr(nodeptr)
	{}

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

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

	bool operator!=(const Self it) const
	{
		return _nodeptr != it._nodeptr;
	}
};

然后,我们在List中,重命名出迭代器和const迭代器:

cpp 复制代码
using iterator = List_iterator<T, T&>;
using const_iterator = List_iterator<T, const T&>;

我们就可以非常巧妙地利用一个模板,实例化出两个迭代器类。

3.2、迭代器其它方法的完善

重载前置++、--,重载后置++、--

cpp 复制代码
// 前置++
Self& operator++()
{
	_nodeptr = _nodeptr->_next;
	return *this;
}
// 后置++
Self operator++(int)
{
	Self tmp = *this;
	_nodeptr = _nodeptr->_next;
	return tmp;
}
// 前置--
Self& operator--()
{
	_nodeptr = _nodeptr->_prev;
	return *this;
}
// 后置--
Self operator--(int)
{
	Self tmp = *this;
	_nodeptr = _nodeptr->_prev;
	return tmp;
}

重载==、!=

cpp 复制代码
// 重载==
bool operator==(const Self& it) const { return _nodeptr == it._nodeptr; }
// 重载!=
bool operator!=(const Self it) const { return _nodeptr != it._nodeptr; }

List中的begin()、end()

cpp 复制代码
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }

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

4、插入、删除操作

插入


cpp 复制代码
// insert
iterator insert(iterator pos, const T& val)// 返回类型为iterator,防止迭代器失效
{
	node* newnode = new node(val);
	node* cur = pos._nodeptr;// pos是迭代器类,不能使用->,因为没有实现重载->
	node* prev = cur->_prev;

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

	++_size;
	return iterator(newnode);
}

这样一来,我们的头插、尾插就可以复用:

cpp 复制代码
// 尾插
void push_back(const T& val) { insert(_head, val); }
// 头插
void push_front(const T& val) { insert(_head->_next, val); }

删除


cpp 复制代码
// pop_back
void pop_back() { erase(_head->_prev); }
// pop_front()
void pop_front() { erase(_head->_next); }

5、链表的析构

const函数,const修饰的是*this。

实现析构,我们可以先实现clear():只保留链表的头节点。

我们可以使用迭代器实现:

cpp 复制代码
// clear
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

析构函数在clear()的基础上,只需再销毁哨兵位:

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

6、拷贝构造与赋值重载

6.1、几个常见的构造方法

我们先来实现initializer_list构造

cpp 复制代码
// initializer_list
list(const initializer_list<T>& il)
{
	for (auto& e : il)
	{
		push_back(e);
	}
}

测试时,程序崩溃了。

原因是此处的构造,没有构造出哨兵位。所以之后的行为都是未定义的。

我们不妨另外定义一个函数,专门生成哨兵位,以后每个构造函数,都要调用这个函数:

cpp 复制代码
void empty_init()
{
	_head = new node(T());
	_head->_prev = _head;
	_head->_next = _head;
}
cpp 复制代码
// initializer_list
list(const initializer_list<T>& il)
{
	empty_init();
	for (auto& e : il)
	{
		push_back(e);
	}
}

我们在实现Print()打印时,如果直接这样写会导致编译错误:

我们可以简单理解出错的原因:编译器无法理解此处的it,到底是const_iterator对象,还是一个静态成员变量。

所以我们在list<T>::const_iterator前加上typename,用来告诉编译器,const_iterator是一个类型。

然后来实现迭代器构造

cpp 复制代码
// 迭代器构造
template<typename InputIterator>
list(InputIterator first, InputIterator last)
{
	empty_init();
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

再来实现n个值构造

cpp 复制代码
list(size_t n, const T& val = T())
{
	empty_init();
	for (size_t i = 0; i < n; ++i)
	{
		push_back(n);
	}
}

由于我们无法采用更好的方式解决此时的n个值构造迭代器构造匹配混乱的问题,所以我们加上一个重载进行补救:

cpp 复制代码
list(size_t n, const T& val = T())
{
	empty_init();
	for (size_t i = 0; i < n; ++i)
	{
		push_back(n);
	}
}
list(int n, const T& val = T())
{
	empty_init();
	for (int i = 0; i < n; ++i)
	{
		push_back(n);
	}
}

6.2、拷贝构造与赋值重载的一般写法

拷贝构造

  1. 创建哨兵位
  2. 遍历,push_back
cpp 复制代码
// 拷贝构造
list(const list<T>& li)
{
	empty_init();
	for (auto e : li)
	{
		push_back(e);
	}
}

赋值重载

  1. 避免自己给自己赋值
  2. 清理到留下哨兵位
  3. 遍历,push_back
cpp 复制代码
// 赋值重载
list<T>& operator=(const list<T>& li)
{
	if (this != &li)
	{
		clear();
		for (auto e : li)
		{
			push_back(e);
		}
	}

	return *this;
}

6.2、拷贝构造与赋值重载的现代写法

拷贝构造与赋值重载现代写法的核心:抢夺别人的成果

拷贝构造

cpp 复制代码
list(const list<T>& li)
{
	empty_init();// 不能使用tmp(li),这会调用拷贝构造,导致无穷递归
	list<T> tmp(li.begin(), li.end());
	swap(tmp);
}

赋值重载

cpp 复制代码
// 现代写法
list<T>& operator=(list<T> li)
{
	clear();
	swap(li);

	return *this;
}

7、重载->与迭代器的完善

7.1、重载->

我们之前使用List,每一个节点保存的都是比较简单的内置类型。

那如果,每一个节点存入一个自定义类型呢:

cpp 复制代码
struct A
{
	int _a1;
	int _a2;

	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{}
};

我们只需传入多个参数,进行隐式类型转换,就可以完成一些简单的初始化、插入等操作:

cpp 复制代码
l1.push_back({ 1, 1 });
l1.push_back({ 2, 2 });
l1.push_back({ 3, 3 });
l1.push_back({ 4, 4 });

使用迭代器打印看看呢?

显然,直接打印A类型的对象是不行的。

我们只能依次打印出A类型成员:

这样就不太简洁,因为我们会很自然地理解为:(*[指针]).的行为等价于[指针]->

所以这里需要我们实现重载->:

cpp 复制代码
T* operator->() { return &_nodeptr->_data; }

乍一看,我们可能对当前->的行为有点费解:

cpp 复制代码
cout << it->_a1 << ":" << it->_a2 << endl;

其实当前->会被处理,这是处理后的模样:

cpp 复制代码
cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;

迭代器it,通过重载->,访问到当前it指向链表节点存储的A对象(的指针),再通过A对象的指针,访问到A对象下的成员。

7.2、迭代器的完善

有了迭代器的重载->,就需要有const迭代器的重载->。

再写一个重载->吗?不需要。我们借助模板:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct List_iterator
{
	// 重载->
	Ptr operator->() { return &_nodeptr->_data; }
};

template<class T>
class list
{
public:
	using iterator = List_iterator<T, T&, T*>;// 实例化出了两种迭代器类
	using const_iterator = List_iterator<T, const T&, const T*>;
};

代码演示

代码演示

相关推荐
HalvmånEver1 小时前
MySQL的内置函数
linux·数据库·学习·mysql
兜兜工作室1 小时前
兜兜消消单词|04.29 每日单词|glove
学习
zhangrelay1 小时前
三分钟云课实践速通--工程制图基础-3D--FreeCAD
笔记·学习·3d
无敌秋2 小时前
C++ 抽象工厂模式实战指南
开发语言·c++·抽象工厂模式
AI玫瑰助手2 小时前
Python基础:数据类型的转换(int/str/list等互转)
windows·python·list
CoderMeijun2 小时前
C++ 智能指针:auto_ptr
c++·内存管理·智能指针·raii·auto_ptr
勤劳的进取家2 小时前
传输层基础
运维·开发语言·学习·php
wuminyu2 小时前
专家视角看Lambda表达式的原理解析
java·linux·c语言·jvm·c++
Gary Studio2 小时前
Frameworks学习预览
学习