STL——list

发布于:2025-07-28 ⋅ 阅读:(16) ⋅ 点赞:(0)

介绍

list 双向链表,擅长中间位置 O(1) 插入/删除,但牺牲了随机访问性能与内存紧凑性;当需要频繁在非尾端增删元素时,list 是首选容器。

模拟实现

构造函数

成员初始化列表只能 初始化成员本身,不能在列表里访问成员的成员

template<class T>
struct myList_node
{
	myList_node(const T& val = T())
		:_val(val)
		,_prev(nullptr)
		,_next(nullptr)
	{}

	T _val;
	myList_node* _prev;
	myList_node* _next;
};

template<class T>
class myList {
	typedef myList_node<T> Node;
public:
	myList()	// 初始化头结点
		:_head(new Node)
	{
		_head->_next = _head;
		_head->_prev = _head;
	}

private:
	Node* _head;
};

拷贝构造

遍历旧链表,逐个节点拷贝到新链表

如果 T 是申请了内存资源的自定义类型,那么这里会触发 T 的拷贝构造,只要 T 的 拷贝构造 实现了深拷贝的逻辑,这里构造新节点时就不需要考虑深拷贝问题

	myList(const myList<T>& ml)	// 拷贝构造
		:_head(new Node)
	{
		_head->_next = _head;
		_head->_prev = _head;

		Node* cur = ml._head->_next;	// 旧
		Node* tail = _head;	// 新
		while (cur != ml._head)
		{
			Node* newNode = new Node(cur->_val);	// 构造新节点
			tail->_next = newNode;	// 链接新节点
			newNode->_prev = tail;

			tail = tail->_next;
			cur = cur->_next;
		}
		tail->_next = _head;	// 最后链接头节点
		_head->_prev = tail;
	}

迭代器

list 将迭代器封装为单独的类,并在类中实现运算符重载

构造

目前使用指针来实现迭代器

template<class T>
struct _myList_iterator
{
	typedef myList_node<T> Node;
	Node* _node;

	_myList_iterator(Node* node)	// 使用节点指针构造迭代器对象
		:_node(node)
	{}
};

运算符重载

重载迭代器可能用到的运算符,使迭代器的运算符的功能符合我们的预期

    _myList_iterator<T>& operator++()	// 前置++
	{
		_node = _node->_next;
		return *this;
	}
	_myList_iterator<T> operator++(int)	// 后置++
	{
		_myList_iterator<T> tmp(_node);	// 构造临时对象
		_node = _node->_next;
		return tmp;
	}
	_myList_iterator<T>& operator--()	// 前置--
	{
		_node = _node->_prev;
		return *this;
	}
	_myList_iterator<T> operator--(int)	// 后置--
	{
		_myList_iterator<T> tmp(_node);	// 构造临时对象
		_node = _node->_prev;
		return tmp;
	}
	T& operator*()
	{
		return _node->_val;
	}
	// 加上 const 让 const 的迭代器也可以调用
	bool operator==(const _myList_iterator<T>& ml_it) const	// 判断迭代器的指向 是否相等
	{
		return _node == ml_it._node;
	}
	bool operator!=(const _myList_iterator<T>& ml_it) const
	{
		return !(_node == ml_it._node);
	}

另外,为迭代器类重载指针解引用运算符 -> 需要注意:

如果选择第一种,那么返回的是 T 的指针类型,随后可以继续接 -> 运算符来访问 T 中的成员;

如果选择第二种,那么返回的是 T 的引用,随后可以接 . 运算符来访问 T 中的成员;

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

标准库实现的是第一种,那么实际上调用时,展开函数调用是这个样子:

_myList_iterator<T> it(_node);
it.operator->()->“T的成员”

这里就连续地使用了两个 -> 解引用运算符,编译器为了优化可读性,直接两个变一个:

_myList_iterator<T> it(_node);
it->“T的成员”

const 迭代器

const 迭代器无非就是不想让你修改 该迭代器 所指向的 对象 的 成员

那么在迭代器类中,涉及到对 迭代器 指向 对象 的操作的 方法,就是两个 解引用运算符的重载

将其 返回值类型 加上 const 属性 修饰即可

但是标准库中的实现:是为迭代器类添加了额外的 2 个模板参数,让迭代器类在实例化的时候,就确定该迭代器 是否为 const 迭代器

template<class T, class Ref, class Ptr>
struct _myList_iterator
{
	typedef myList_node<T> Node;
	Node* _node;

	......

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

	......

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

该迭代器实例化时传参示例如下:

	_myList_iterator<string, string&, string*>;
	_myList_iterator<string, const string&, const string*>;

在 list 中实例化

实例化迭代器

template<class T>
class myList {
	typedef myList_node<T> Node;
	typedef _myList_iterator<T, T&, T*> iterator;
	typedef _myList_iterator<T, const T&, const T*> const_iterator;

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

析构函数

需要逐个释放节点,使用指针即可

    ~myList() {
		Node* cur = _head->_next;
		while (cur != _head) {
			Node* next = cur->_next; // 先保存下一个
			delete cur;	// 再释放当前
			cur = next;	// 继续
		}
		delete _head;
		_head = nullptr;
	}

operator=

 (copy-and-swap 的小技巧)

赋值运算符的重载,直接复用 拷贝构造 的逻辑:使用传值传参,这时形参是实参的拷贝,然后交换形参与被赋值对象的头节点指针;出了函数作用域, ml 会被自动调用析构函数释放掉

	myList<T>& operator=(myList ml)
	{
		std::swap(_head, ml._head);
		return *this;
	}

insert

在迭代器 position 前,插入一个值为 val 的元素

	iterator insert(iterator position, const T& val)
	{
		Node* cur = position->_node;
		Node* curPrev = cur->_prev;
		Node* newNode = new Node(val);	// new 一个新节点
		// 链接新节点
		curPrev->_next = newNode;
		newNode->_prev = curPrev;
		newNode->_next = cur;
		cur->_prev = newNode;
		return iterator(newNode);
	}

push_back

复用 insert

	void push_back(const T& val)
	{
		insert(end(), val);
	}

erase

释放当前迭代器处的节点,并返回下个新节点的迭代器

但是注意不能删除头节点,破坏双链表结构

	iterator erase(iterator position)
	{
		Node* cur = position._node;
		if (cur == _head)	// 不能删除头节点
		{
			return iterator(_head);
		}
		Node* curPrev = cur->_prev;	// 链接
		Node* curNext = cur->_next;
		curPrev->_next = curNext;
		curNext->_prev = curPrev;

		delete cur;
		return iterator(curNext);
	}

pop_back

复用 erase,也要注意不能删除头节点

	void pop_back()
	{
		if (_head->_prev != _head)
		{
			erase(iterator(_head->_prev));
		}
	}

resize

void resize (size_t n, T val = T());

resize 依然要分段考虑,如果 n <= size( ),将链表从后向前缩减至 size() == n;(复用pop_back)

如果 n > size(),就 push_back n 个值为 val 的节点

那么首先实现 size(),难免要遍历一遍链表;当然,也可以提前在 myList 类中定义成员变量 _size,来记录当前链表的size

	size_t size()
	{
		size_t cnt = 0;
		iterator it = begin();
		while (it != end())
		{
			cnt++;
			it++;
		}
		return cnt;
	}

	void resize(size_t n, T val = T())
	{
		size_t cur_size = size();    // 提前缓存 size()
		while (cur_size > n)
		{
			pop_back();
			cur_size--;
		}
		while (cur_size < n)
		{
			push_back(val);
			cur_size++;
		}
	}

这里为避免每次循环都调用 size() ,提前缓存好即可(进一步提升性能还得是像标准库中一样,维护一个 _size 成员来记录)

小结

有了一些手撕容器的思路,要注意深拷贝问题,从 拷贝构造 到 赋值运算符重载;

元素 的 方法中,要注意构造对象时的操作; 方法中,要注意对资源的深度清理

迭代器的封装:主要是为了实现让迭代器的 运算符重载 支持预期的功能;以及迭代器需要实现两套(const

且在使用过程中,要注意迭代器的失效问题,在 vector 容器中扩容之后尤其注意;因为 list 不涉及扩容,所以迭代器失效问题不明显;但只要迭代器失效就不能再使用,否则行为未定义

注意被封装的迭代器中,对 -> 解引用运算符的重载,其实是为 list<string<T>> 这样的类型准备的;这样迭代器就可以通过 -> 解引用实现对 string<T> 成员的访问,实际上有两层解引用


网站公告

今日签到

点亮在社区的每一天
去签到