STL容器list的模拟实现

大纲:

1. list结构的介绍

cpp 复制代码
namespace xiaoli
{
	// 定义节点的结构
	template <class T>
	struct list_node
	{
		T data;
		list_node<T>* next;
		list_node<T>* prev;
		list_node(const T& x = T())
			:next(nullptr)
			,prev(nullptr)
			,data(x)
		{
		}
	};

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		
	private:
		Node* _head;
	};
}

结构体成员介绍:data -- 存储数据;next -- 指向下一节点的指针;prev -- 指向上一节点的指针。

问题:为什么list中结点要单独定义?

1)list是双向链表,存储的数据不单单是数据,还有指针,封装为结构体便于更好的管理(即结点的核心任务是存储数据 + 维护前后指针)。

2)list容器的职责是管理结点集合,提供插入、删除、遍历等对外接口,修改结点结构不会影响容器的业务逻辑,反之亦然,这符合面向对象的设计原则,同时也跟C++库里面的设计保持一致!

3)设计为模板,是为了适配任何数据类型,更加方便操作。

注意:list含有哨兵位(即头结点)!!!

问题1:这个哨兵位是需要我们手动创建和释放的吗?

答案是:构造时必须手动创建哨兵位,析构时必须手动销毁哨兵位。标准库替你管了哨兵位,自己写的话必须亲手管,且要严格遵循 -- 先建哨兵位、后用链表,先清有效节点、后毁哨兵位 的顺序。

问题2:哨兵位是什么时候创建的?

答案是:刚开始的结构体我们对节点进行了初始化,然后再list的类定义了_head(后初始化)这个就是我们说的哨兵位。

2. 默认成员函数

2.1 构造函数

(1)无参构造

cpp 复制代码
list()
	:_head(new Node)
	,_head->_next(_head)
	,_head->_prev(_head)
{
}

这样写形式不是很好看,可以封装函数,当然这样也🆗。

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

list()
{
	empty_init();
}

(2)构造n个val值

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

(3)初始化列表构造

cpp 复制代码
list(initializer_list<T> lt)
{
	empty_init();

	for (auto& e : lt)
	{
		push_back(e);
	}
}

2.2 拷贝构造

拷贝构造:需要深拷贝,申请新的结点空间,通过更改结点的指向完成链接关系!!

这里的实现逻辑和push_back类似,直接复用即可!!

cpp 复制代码
// lt2(lt1)
list(const list<T>& lt)
{
	empty_init();
	for (auto& e : lt)
	{
		push_back(e);
	}
}

2.3 赋值运算符

这里直接采用现代写法,先构造一份临时对象,在和临时对象交换资源。(反正临时对象要销毁)

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

2.4 析构函数

析构函数:遍历链表,销毁每个节点中的 元素,**释放链表所有节点的内存,**这里无论是头删还是尾删都是一样的效率,最后清理哨兵位。

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

3. 容器内容修改相关函数

3.1 push_front()

cpp 复制代码
// 旧版
void push_front(const T& val)
{
	// 提前保存好上一个节点
	Node* tail = _head->_prev;

	Node* newnode = new Node(val);
	Node* next = _head->_next;

	_head->_next = newnode;
	newnode->_prev = _head;
	newnode->_next = next;
	next->_prev = newnode;
			
}

现代写法:直接套用insert,指定位置即可。

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

3.2 pop_front()

cpp 复制代码
void pop_front()
{
	assert(_head != _head->_prev);
	Node* tailNode = _head->_prev;
	Node* cur = _head->_next;
	Node* nextNode = cur->_next;

	_head->_next = nextNode;
	nextNode->_prev = _head;

	delete cur;
}

现代写法:直接套用erase,指定位置即可。

cpp 复制代码
void pop_front()
{
	erase(begin());
}

3.3 push_back()

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

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

现代写法:直接套用insert,指定位置即可。

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

3.4 pop_back()

cpp 复制代码
void pop_back()
{
	assert(_head != _head->_next);
	Node* tailNode = _head->_prev;
	Node* newtail = tailNode->_prev;

	newtail->_next = nullptr;
	newtail->_prev = _head;
	_head->_prev = newtail;

	delete tailNode;
}

现代写法:

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

3.5 insert()

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

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

3.6 erase()

cpp 复制代码
iterator erase(iterator pos)
{
	assert(pos != end());

	Node* cur = pos._node;
	Node* nextNode = cur->_next;
	Node* prevNode = cur->_prev;

	prevNode->_next = nextNode;
	nextNode->_prev = prevNode;

	delete cur;

	return iterator(nextNode);
}

3.7 clear()

根据迭代器,获取哨兵位的结点,然后遍历链表进行结点的删除。

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

4. 迭代器

4.1 begin()和end()

首先先来看下面这个代码是否正确?

cpp 复制代码
// 迭代器
typedef Node* iterator;
typedef const Node* const_iterator;
iterator begin()
{
	return _head->_next;
}
iterator end()
{
	return _head;
}
cpp 复制代码
void test_2()
{
	list<int> l;
	l.push_back(1);
	l.push_back(2);
	l.push_back(3);
	l.push_back(4);
	l.push_back(5);

	list<int>::iterator it = l.begin();
	while (it != l.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

如果按照这样去写的话,存在两个问题:

问题1:it是iterator,是指向结点的指针,那么按理说 *it 拿到的数据应该是结点,怎么可能拿到里面的数据data?

问题2:list不是vector,它的底层是双向带头链表,空间是不连续的,你怎么保证it++之后就能到达下一个结点?

问题3: *it 是一个node对象,如果没有为它重载<<运算符,代码会编译失败!

所以如何解决这个问题,内置类型会直接对指针进行解引用,但是这里的结果并不符合预期,那么我可以重定义实现重载啊,就像在之前实现日期类的时候,没有实现对于功能的运算符怎么办? -- 当时的解决办法也是运算符重载!!!!!

由于list里面++,--,*和->都需要实现重载,不妨封装为1个类,便于管理和理解。

所以正确的迭代器应该这样写:

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

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

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

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

	Self& operator--(int)
	{
		// 后置--,先保存后使用
		Self tmp(*this);
		_node = _node->_prev;
		return *this;
	}

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

	Self operator==(const Self& it)
	{
		return _node == it._node;
	}
};
cpp 复制代码
typedef list_iterator<T, T&, T*> iterator;
typedef const list_iterator<T, T&, T*> const_iterator;

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

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

注意:这里引入两个模板参数Ref和Ptr,是因为数据类型不一定是int,所以需要泛型化!!!同时还需要注意const修饰的是指向迭代器的内容,不是修饰迭代器本身!!!

4.2 迭代器失效的问题

迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。上一期vector的迭代器失效原因有2个:1)深拷贝的问题;2)删除元素时,编译器会对其标记,认为erase之后迭代器是失效的

这里list的迭代器失效主要是由于删除结点导致的!!!!!

解决办法:及时的更新it

正确更新如下:

cpp 复制代码
void test_3()
{
	list<int> l;
	l.push_back(1);
	l.push_back(2);
	l.push_back(3);
	l.push_back(4);
	l.push_back(5);
	l.push_front(5);
	xiaoli::list<int>::iterator it = l.begin();
	while (it != l.end())
	{
		it = l.erase(it++);
	}
}

到这里list的模拟实现就讲解完毕,下一期分享stack和queue的接口和模拟实现!!!

相关推荐
StandbyTime13 小时前
《算法笔记》学习记录-第二章 C/C++快速入门
c++·算法笔记
摇滚侠13 小时前
macbook shell 客户端推荐 Electerm macbook 版本下载链接
java·开发语言
我在人间贩卖青春13 小时前
C++之结构体与类
c++··结构体
程序员布吉岛13 小时前
Java 后端定时任务怎么选:@Scheduled、Quartz 还是 XXL-Job?(对比 + 避坑 + 选型)
java·开发语言
rainbow688913 小时前
C++实现JSON Web计算器
c++
C++ 老炮儿的技术栈13 小时前
Qt Creator中不写代如何设置 QLabel的颜色
c语言·开发语言·c++·qt·算法
知无不研13 小时前
lambda表达式的原理和由来
java·开发语言·c++·lambda表达式
艾莉丝努力练剑13 小时前
【Linux:文件】基础IO
linux·运维·c语言·c++·人工智能·io·文件
lili-felicity13 小时前
CANN多模型并发部署与资源隔离
开发语言·人工智能