【C++修炼之路 第七章】模拟实现 list 类模板

0、前言:

类中的 list 是双向带头循环链表

我们这里模拟实现的 list 也是 双向带头循环链表


1、list 的核心框架实现(list 类模板)

1.1 节点类模板 和 链表类模板

1.1.1 节点类

双向链表节点(含有 prev 指针 和 next 指针)

cpp 复制代码
template<class T>
struct ListNode
{
	typedef ListNode<T> Node;

	Node* _next;
	Node* _prev;
	T _data;

	// 构造函数
	ListNode(const T& data = T())
		:_data(data)
		, _next(nullptr)
		, _prev(nullptr)
	{}
};
【思考】为什么 节点类 用的是 struct ,而不是 class?

先说明:struct 和 class 的区别

因为 struct 在 C++语法中,和 class 都可以表示类

struct 是C++兼容 C语言的 语法

在 C++中 struct 的类,没有访问限定符的限制

所有成员一律 视为公有
再说明:为什么节点类要用 struct

依据C语言学过的链表,在一个链表节点中,指向前后节点的指针和存储该节点数据的变量 都需要频繁的被外界访问(_next、_prev、_data),与其在 class 中使用 public 访问限定符,不如干脆直接使用 struct ,使该类中的所有成员都可被外界直接,提高便捷性


1.1.2 链表类

带头链表:含有一个 哨兵位节点,成员变量 _head 指向该哨兵位节点

cpp 复制代码
template<class T>
class list
{
	typedef ListNode<T> Node; // 将节点类型重命名


private:
	Node* _head; 
};

1.2 构造函数

作用:初始化一个 头节点(即哨兵位)

cpp 复制代码
// 构造函数:创建头节点

list() {
	empty_list();
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;

	// 注意:_next 和 _prev 都是指向 _head,不是 nullptr !!!,否则 push_back 时会 有访问空指针的问题

}

2. 迭代器类模板(重点:理解迭代器思想)

2.1 迭代器思想与封装思想

你一个内置类型,无法完成的操作与行为,可以将该类型封装成一个类,在类里面重载各种运算,使这个内置类型间接拥有之前没有 "功能"

例如:

在链表中,有指向一个节点的指针 Node* _node;

这个指针是不能直接通过 ++ 操作,跳转到下一个节点的(链表空间不连续,不能像 vector 一样通过 原生指针 T* 的 ++操作 到达下一个元素位置)

此时可以将该指针封装成一个类 ,在类里面重载++操作,间接的使指针 _ node 有了 [ 指针++ ] 就跳转到下个节点的 功能(因为重载函数中的操作:_node = _ node-> _next; 使 _node 指向下一个节点位置)

复制代码
 
cpp 复制代码
template<class T>
 struct NewPtr
 {
     typedef ListNode<T> Node;
     typedef NewPtr<T> Self;  // 自己这种类型
 ​


     
     Node* _node;   // 核心还是 一个链表节点指针
 ​  


 ​
     // 构造函数
     // 这里的 p 不用给 const ,否则权限放大
     NewPtr(Node* p = nullptr)
         :_node(p)
     {}
 ​
 ​
     Self& operator++() {
         _node = _node->_next;
         return *this;
     }
 };

小结:

内置类型不能直接使用的行为,我们可以将这个内置类型封装成一个类型,在类型中通过重载运算符控制它的行为

因此:将 list 的节点指针 封装成一个迭代器类,使该指针拥有某些之前无法单独实现的 功能

迭代器的表面上是一个类型的壳

在内心实际上是一个节点的指针


2.2 链表的迭代器 ListIterator

按上面的逻辑:设计链表的迭代器 ListIterator

cpp 复制代码
// 迭代器就是将指向一个节点的指针 node 封装成一个类

template<class T>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T> Self;  // 自己这种类型



	Node* _node;



	// 构造函数
	// 这里的 p 不用给 const ,否则权限放大
	ListIterator(Node* p = nullptr)
		:_node(p)
	{}



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

2.3 补充迭代器函数:后置++ 和 前后置--

cpp 复制代码
Self operator++(int) {
	Self tmp(this->_node);
	_node = _node->_next;
	return tmp;
}

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

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

2.4 迭代器 begin() / end()

注意:返回的也是迭代器,这里使用了 匿名对象(调用构造函数)

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

iterator end() {
	return iterator(_head);
}

2.5 范围for循环 需要的重载函数:不等于:!= 与 解引用:*

除了 begin() 和 end() 使范围for循环 生效,还需要题目中的这两个重载函数

等于:== 与 箭头操作符:-> 是为了匹配,作为相应的补充

cpp 复制代码
// ==  !=
// 范围for 需要使用 != 重载函数
bool operator!=(const Self& it) {
	return (this->_node != it._node);
}


// ==: 补充
bool operator==(const Self& it) {
	return (this->_node == it._node);
}




// *   ->

// 范围for 需要使用 * 重载函数
T& operator*() {
	return _node->_data;
}


// ->: 补充
T* operator->() {
	return &(_node->_data);
}

2.6 ⭐迭代器 iterator 完整源码⭐

cpp 复制代码
// 迭代器就是将指向一个节点的指针 node 封装成一个类
	template<class T>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T> Self;  // 自己这种类型


		Node* _node;



		// 构造函数
		// 这里的 p 不用给 const ,否则权限放大
		ListIterator(Node* p = nullptr)
			:_node(p)
		{}


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

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

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

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



		// ==  !=
		// 范围for 需要使用 != 重载函数
		bool operator!=(const Self& it) {
			return (this->_node != it._node);
		}
		// ==: 补充
		bool operator==(const Self& it) {
			return (this->_node == it._node);
		}



		// *   ->
		// 范围for 需要使用 * 重载函数
		T& operator*() {
			return _node->_data;
		}
		// ->: 补充
		T* operator->() {
			return &(_node->_data);
		}

	};

3、list 类中其他函数

3.1 插入 / 删除:insert / erase

插入:在 迭代器 pos 指向的节点前面插入 新节点

删除:删除 迭代器 pos 指向的节点

(新节点的插入 和 节点的删除 的具体操作这里不做讲解,若不明白,可以先学C语言的链表部分)

cpp 复制代码
void insert(const iterator& pos, const T& x)
{
	assert(pos._node);

	Node* newNode = new Node(x);
	Node* next = pos._node;
	Node* prev = next->_prev;

	// prev  newNode  next
	prev->_next = newNode;
	newNode->_prev = prev;
	newNode->_next = next;
	next->_prev = newNode;
}


void erase(const iterator& pos) {
	assert(pos._node);

	Node* cur = pos._node;
	Node* next = cur->_next;
	Node* prev = cur->_prev;

	// prev  cur  next
	delete cur;
	cur = nullptr;
	next->_prev = prev;
	prev->_next = next;
}
【思考】insert 和 erase 是否存在迭代器失效?

根据之前的文章讲解:

迭代器失效往往是因为空间扩容 或 缩容,导致迭代器指针指向一块已经释放的空间,导致"野指针"的问题

链表的insert 函数 中,不存在因为扩容而另外开辟新空间的操作,迭代器 pos 从始至终都指向 合法的链表节点,因此没有 迭代器失效问题

链表的 erase 函数 中,删除 迭代器 pos 指向的节点后,迭代器 pos 就指向了一块已经释放的空间,造成迭代器失效问题

解决办法:学习库里的做法,返回迭代器

erase 函数 返回 指向 被删除节点的下一个节点的 迭代器

cpp 复制代码
iterator insert(const iterator& pos, const T& x)
{
	// ......

	return iterator(newNode);
}
iterator erase(const iterator& pos) 
{
	// ......

	return iterator(next);
}

3.2 push_back / pop_back

push_back

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

	// tail  newNode  _head
	tail->_next = newNode;
	newNode->_prev = tail;
	newNode->_next = _head;
	_head->_prev = newNode;
}

可以直接复用 insert 函数:即尾插,在 end() 前面插入一个节点

cpp 复制代码
void push_back(const T& x) {
	insert(end(), x);
}

**pop_back:**就直接复用 erase 函数

cpp 复制代码
void pop_back() {
	assert(_head->_next != _head);
	erase(--end());
}

3.3 push_front / pop_front

这个就简单了,和 实现 push_back / pop_back 思想一样

cpp 复制代码
// push_front / pop_front

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

void  pop_front() {
    assert(_head->_next != _head);
	erase(begin());
}

3.4 拷贝构造

根据传过来的参数的类型是否是 const 修饰,这里写两种

cpp 复制代码
// 空链表初始化

void empty_list() {

	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;

	// 注意:_next 和 _prev 都是指向 _head,不是 nullptr !!!,否则 push_back 时会 有访问空指针的问题
}


// 构造函数:创建头节点

list() {
	empty_list();
}





// 拷贝构造

list(list<T>& L) {
	empty_list();
	for (auto& e : L) {
		push_back(e);
	}
}

list(const list<T>& L) {
	empty_list();
	for (auto& e : L) {
		push_back(e);
	}
}

因为拷贝一个 链表 ,你自己本身也必须现有头节点(哨兵位),也就是 构造函数做的工作

因此,我们将 初始化一个哨兵位 的工作直接封装成一个函数 empty_list(),给 构造函数、拷贝构造函数等其他函数复用

【思考】为什么要显式的写拷贝构造函数?

因为默认的拷贝构造函数是浅拷贝

直接浅拷贝会导致 析构函数多次释放同一节点的空间,形成悬挂指针,造成程序崩溃

因此要显式的 写 深拷贝函数

3.5 initializer_list 构造

cpp 复制代码
// initializer_list 构造

list(initializer_list<T> il) {
	empty_list();  // 复用函数:初始化一个哨兵位出来
	for (auto& e : il) {
		push_back(e);
	}
}

3.6 赋值运算符重载

这里的赋值 使用的是 STL 的现代写法:使用 swap 交换大法

cpp 复制代码
// 赋值:由这个可以想到显式写 拷贝构造函数(默认的是值拷贝)
list<T>& operator==(const list<T>& L) {
	list<T> tmp(L._head);
	std::swap(_head, tmp._head);
	return *this;
}

3.7 析构函数

【思考】为什么不能直接使用 默认的析构函数:

默认的析构函数 只会讲 list 类中的 _head 直接析构释放,却不会一个一个的析构链表节点,导致只有头节点(即 _head 指向的 哨兵位)被释放,其他的节点不释放,造成内存泄漏

cpp 复制代码
// 析构

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

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

4. 迭代器类 const_iterator 及相关 const 修饰 函数

4.1 设计迭代器类模板 提高代码复用性

这个和前面的迭代器 iterator 类,没什么区别,const_iterator 就表示 迭代器指向的内容不能被修改,在 list 中 const_iterator 迭代器指向一个节点,即表示节点中的数据 _data 不能被修改

和 迭代器 iterator 类 主要的区别是: 下面这两个函数的返回值问题

  • 第一个函数返回 引用,涉及 _data 的修改,要用 const 修饰起来
  • 第二个函数返回 指针,涉及 _data 的修改,要用 const 修饰起来
cpp 复制代码
T& operator*() {
	return _node->_data;
}


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

即修改成:

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


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

因此,就可以设计一个 const _iterator 的迭代器类:ListConstIterator

cpp 复制代码
// 迭代器就是将指向一个节点的指针 node 封装成一个类
	template<class T>
	struct ListConstIterator
	{
		typedef ListNode<T> Node;
		typedef ListConstIterator<T> Self;  // 自己这种类型


		Node* _node;



		// 构造函数
		// 这里的 p 不用给 const ,否则权限放大
		ListConstIterator(Node* p = nullptr)
			:_node(p)
		{}


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

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

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

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



		// ==  !=
		// 范围for 需要使用 != 重载函数
		bool operator!=(const Self& it) {
			return (this->_node != it._node);
		}
		// ==: 补充
		bool operator==(const Self& it) {
			return (this->_node == it._node);
		}



		// *   ->
		// 范围for 需要使用 * 重载函数
		const T& operator*() {
			return _node->_data;
		}
		// ->: 补充
		const T* operator->() {
			return &(_node->_data);
		}

	};

但是 iterator 的 ListIterator 类 和 const _iterator 的 ListConstIterator 类

就仅仅是 两个函数的返回值不同,其他代码的重复性过高了

如何设计复用?

可以 利用 模板的性质,将这个 迭代器类 设计成可以变换 两个函数的返回值模板

给 迭代器类 增加两个 模板参数:Ref 和 Ptr

cpp 复制代码
template<class T, class Ref, class Ptr>

Ref 是 引用 的缩写

Ptr 是 指针 的缩写

然后将 那 两个函数的返回值 更换成 这两个模板参数

这样之后,就能 根据传过来的 参数,来产生不同的 迭代器类对象

cpp 复制代码
Ref operator*() {
	return _node->_data;
}

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

调用 迭代器类模板演示:

在 list 类中 设置两个 typedef :iterator 与 const_iterator 以区分不同类型

传不同的 Ref 和 Ptr 过去

cpp 复制代码
typedef ListIterator<T, T&, T*>  iterator;
typedef ListIterator<T, const T&, const T*>  const_iterator;

4.2 ⭐最终的 迭代器类模板 完全体!⭐

cpp 复制代码
// 迭代器就是将指向一个节点的指针 node 封装成一个类
template<class T, class Ref, class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T, Ref, Ptr> Self;  // 自己这种类型
	Node* _node;


	// 构造函数
	// 这里的 p 不用给 const ,否则权限放大
	ListIterator(Node* p = nullptr)
		:_node(p)
	{}

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


	// ==  !=
	// 范围for 需要使用 != 重载函数
	bool operator!=(const Self& it) {
		return (this->_node != it._node);
	}
	// ==: 补充
	bool operator==(const Self& it) {
		return (this->_node == it._node);
	}


	// *   ->
	// 范围for 需要使用 * 重载函数
	Ref operator*() {
		return _node->_data;
	}
	// ->: 补充
	Ptr operator->() {
		return &(_node->_data);
	}

};

4.3 begin() 和 end() 也更新一套 const_iterator 版本的

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

const_iterator end() const {
	return const_iterator(_head);
}

5. 完整 list 类 源码

cpp 复制代码
namespace my
{
	template<class T>
	struct ListNode
	{
		typedef ListNode<T> Node;

		Node* _next;
		Node* _prev;
		T _data;

		// 构造函数
		ListNode(const T& data = T())
			:_data(data)
			,_next(nullptr)
			, _prev(nullptr)
		{}
	};

	// 迭代器就是将指向一个节点的指针 node 封装成一个类
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;  // 自己这种类型
		Node* _node;


		// 构造函数
		// 这里的 p 不用给 const ,否则权限放大
		ListIterator(Node* p = nullptr)
			:_node(p)
		{}

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


		// ==  !=
		// 范围for 需要使用 != 重载函数
		bool operator!=(const Self& it) {
			return (this->_node != it._node);
		}
		// ==: 补充
		bool operator==(const Self& it) {
			return (this->_node == it._node);
		}


		// *   ->
		// 范围for 需要使用 * 重载函数
		Ref operator*() {
			return _node->_data;
		}
		// ->: 补充
		Ptr operator->() {
			return &(_node->_data);
		}

	};


	template<class T>
	class list
	{
		typedef ListNode<T> Node;
		
	public:
		typedef ListIterator<T, T&, T*>  iterator;
		typedef ListIterator<T, const T&, const T*>  const_iterator;

		// 空链表初始化
		void empty_list() {
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
			// 注意:_next 和 _prev 都是指向 _head,不是 nullptr !!!,否则 push_back 时会 有访问空指针的问题
		}


		// 构造函数:创建头节点
		list() {
			empty_list();
		}

		// 拷贝构造
		list(list<T>& L) {
			empty_list();
			for (auto& e : L) {
				push_back(e);
			}
		}
		list(const list<T>& L) {
			empty_list();
			for (auto& e : L) {
				push_back(e);
			}
		}

		// initializer_list 构造
		list(initializer_list<T> il) {
			empty_list();
			for (auto& e : il) {
				push_back(e);
			}
		}



		// 析构
		~list() {
			clear();
			delete _head;
			_head = nullptr;
		}
		void clear() {
			iterator it = begin();
			while (it != end()) {
				it = erase(it); 
			}
		}


		// 迭代器:begin(),end()
		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);
		}


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

			// tail  newNode  _head
			tail->_next = newNode;
			newNode->_prev = tail;
			newNode->_next = _head;
			_head->_prev = newNode;
		}

		void pop_back() {
			assert(_head->_next != _head);
			erase(--end());
		}

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

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


		// insert 和 erase
		// insert 无迭代器失效
		iterator insert(const iterator& pos, const T& x)
		{
			assert(pos._node);

			Node* newNode = new Node(x);
			Node* next = pos._node;
			Node* prev = next->_prev;
			
			// prev  newNode  next
			prev->_next = newNode;
			newNode->_prev = prev;
			newNode->_next = next;
			next->_prev = newNode;

			return iterator(newNode);
		}

		iterator erase(const iterator& pos) {
			assert(pos._node);

			Node* cur = pos._node;
			Node* next = cur->_next;
			Node* prev = cur->_prev;

			// prev  cur  next
			delete cur;
			cur = nullptr;
			next->_prev = prev;
			prev->_next = next;

			return iterator(next);
		}

		// 赋值:由这个可以想到显式写 拷贝构造函数(默认的是值拷贝)
		list<T>& operator==(list<T>& L) {
			list<T> tmp(L._head);
			std::swap(_head, tmp._head);
			return *this;
		}


	private:
		Node* _head;
	};

}
相关推荐
獨枭1 小时前
CMake 构建项目并整理头文件和库文件
c++·github·cmake
小王爱吃月亮糖3 小时前
C++的23种设计模式
开发语言·c++·qt·算法·设计模式·ecmascript
小哈龙5 小时前
c++ 类似与c# 线程 AutoResetEvent 和 ManualResetEvent的实现
c++·c#·多线程
yuanbenshidiaos5 小时前
C++--------------树
java·数据库·c++
海螺姑娘的小魏5 小时前
Effective C++ 条款 15:在资源管理类中提供对原始资源的访问
开发语言·c++
青青丘比特7 小时前
STL.string(下)
开发语言·c++
jjjxxxhhh1237 小时前
C++ 模板是为了解决啥问题
开发语言·c++·算法
c++初学者ABC8 小时前
GESP2级2403 小杨的日字矩阵
c++·算法
alien爱吃蛋挞8 小时前
List详解
java·list
代码小将8 小时前
PTA数据结构编程题7-1最大子列和问题
数据结构·c++·笔记·学习·算法