《链表的优雅封装:C++ list 模拟实现与迭代器之美》

发布于:2025-09-11 ⋅ 阅读:(13) ⋅ 点赞:(0)

list的优缺点

list的底层是双向链表,相比于之前底层是数组的string,vector,list的缺点是无法通过位置来直接访问元素,因为内存分配不连续;以及底层的迭代器需要封装指针才能实现++,*等逻辑的正确性。优点是 当已知插入/删除位置的迭代器,进行插入,删除元素时仅需改变前后指针的指向;以及list迭代器稳定,插入新节点时不必移动已有节点,迭代器不会失效。

对list双向链表进行拆分成三类,

第一类list_node

,链表中的节点list_node,包括本身存储的数据,以及指向下一个,上一个 节点的指针

本身存储的数据类型是T,创建一个模板,

template<class T>
struct list_node//链表中的每个节点
{
	T _data;
	list_node<T>* _prev;
	list_node<T>* _next;
};

第二类list

,链表本身,包含多个节点list_node,以及链表中节点的个数_size

此时节点仅仅维护一个哨兵位_head,剩下的节点利用push_back等方法进行插入

template<class T>
class list
{
private:
	list_node<T>* _head;
	size_t _size;
};

第三类一会再说,先进行节点插入到链表的操作,实现push_back,

namespace sxm
{
	template<class T>
	struct list_node//链表中的每个节点
	{
		T _data;
		list_node<T>* _prev;
		list_node<T>* _next;

		list_node(const T& data)//节点的构造函数
			:_data(data)
			,_prev(nullptr)
			,_next(nullptr)
		{ }
	};
	
	template<class T>
	class list
	{
	public:
		void push_back(const T& x)
		{
			list_node<T>* newnode = new list_node<T>(x);//先创建一个节点,并将存储的元素x传入,这时候
			//就要对节点弄构造函数(第一个类),用struct方便访问
			//接下来对节点进行链接,tail newnode _head 
			list_node<T>* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;

			++_size;
		}

		list()//链表的构造函数
		{
			_head = new list_node<T>(T()); //创建一个_head的哨兵节点,注意!

			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

	private:
		list_node<T>* _head;
		size_t _size;
	};
	void test()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);

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

注意,list链表的构造函数是先创建一个对象list_node,不能写成_head = new list_node(),而是要写成_head = new list_node(T()); 需要调用list_node的构造函数,而list_node中如果没有无参的构造函数,因为list_node一般要初始化_data,有参,会编译失败。这里不推荐list_node的无参构造,会导致_data的未定义行为

而T () 会创建一个T类型的临时对象,并调用T的默认构造函数,然后这个临时对象会作为list_node构造函数的参数,传递给 list_node(const T& val),用于初始化节点的_data

_head = new list_node<int>(int()); 
// 等价于:new list_node<int>(0),因为 int() 会初始化为 0

_head = new list_node<string>(string()); 
// 调用 string 的默认构造函数,初始化 data 为空字符串

那这时如何用迭代器来遍历链表呢?

第三类,迭代器类(非const类)

,模拟指针行为,封装节点指针

为什么要有这一类?

拿刚刚的迭代器遍历来说,对节点指针进行解引用得到的是节点本身,拿不到节点里面存储的数据;对节点指针进行++,得不到下一个节点,节点在内存中存储是不连续的。

迭代器的本质是对遍历行为的抽象,让用户可以忽略底层的差异,用相同的方法遍历数组,链表等结构。

因此进行封装指针,重载++,*等运算符,提供begin(),end()等方法,供外部使用

先实现一个迭代器模板

template<class T>
struct list_iterator
{
	typedef list_node<T> Node;
	Node* _node;
};

迭代器中的成员变量是一个当前节点指针,存储当前迭代器指向的节点,可以进行向前移动(_node>_prev),向后移动(_node->_next),访问当前元素(_node->_data),通过这一个链表节点对运算符重载,来达到我们想要的目的

	template<class T>
	struct list_iterator
	{
	    typedef list_node<T> Node;
        Node* _node;

		Node* operator++()
		{
			_node = _node->_next;
			return _node;
		}
	};

可以这样写吗?返回一个list_node *_node?

不可以,因为这样会返回的是 节点指针,如果再次对这个指针进行++操作,不会是下一个指针,即无法再对迭代器进行后续操作。

将返回类型改成迭代器类型,返回值应返回迭代器本身,而不是结点指针

	template<class T>
	struct list_iterator
	{
	    typedef list_node<T> Node;
	    Node* _node;

		list_iterator<T>& operator++()
		{
			_node = _node->_next;//迭代器的内部状态被修改,指向下一个节点
			return *this;//为外部提供接口
		}

		list_iterator(Node *node)// // 接受节点指针的构造函数,允许从节点指针构造迭代器
			:_node(node)
		{}
	};

同理operator--

list_iterator<T>& operator--()
{
	_node = _node->_prev;//更新内部迭代器
	return *this;//返回给外部更新后的迭代器
}

对*进行运算符重载

,直接对节点指针进行解引用得到的是节点本身,而非节点内存储的数据

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

以及operator!=/operator==

	bool operator!=(const list_iterator<T>& s)
	{
		return _node != s._node;
	}
	bool operator==(const list_iterator<T>& s)
	{
		return _node == s._node;
	}

接着进行插入在list类中实现迭代器begin(),end()

template<class T>
class list
{
	typedef list_node<T> Node;
public:
	typedef list_iterator<T> iterator;
	iterator begin()
	{
		iterator it = _head->_next;//调用 iterator 的构造函数创建一个临时对象,再将这个临时对象拷贝到 it 中
		return it;
        //return iterator(_head->_next);//直接调用迭代器的构造函数,创建一个临时的匿名迭代器对象
        //return _head->next;
	}
	iterator end()
	{
        iterator it=_head;
		return it;//最后一个元素的下一个位置
	}

	list()
	{
		_head = new list_node; //创建一个_head的哨兵节点
		_head->_next = _head;
		_head->_prev = _head;
		_size = 0;
	}

private:
	list_node<T>* _head;
	size_t _size;
};

此时第三种写法return _head->_next;原本的类型是list_node<T>*,但函数的返回值类型是iterator,编译器会在list_iterator<T> 类中查找可以接收 list_node<T>* 作为参数的构造函数,会自动调用这个构造函数构造一个临时的 list_iterator<T> 对象head->_prev 作为参数,返回一个转换后的iterator

对list进行完善

insert

在list类中实现指定位置之前的插入insert

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

		++_size;
	}

push_back

那这时,push_back可以变为

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

push_front

push_front,在开头增加一个新节点

	void push_front(const T& x)
	{
		insert(begin(), x);//_head->_next之前插入,即_head的下一个节点
	}

erase

erase的实现,从list中删除某个节点信息

	void erase(iterator pos)// pos._node->_prev   pos->_node   pos._node->next
	{
		Node* prev = pos._node->_prev;
		Node* next = pos._node->_next;
		prev->_next = next;
		next->_prev = prev;

		delete pos._node;
		--_size;
	}

pop_back

接着实现pop_back,删除链表中最后一个节点

	void pop_back()
	{
		erase(--end());//_head->_prev
	}

pop_front

pop_front,删除第一个有效节点

void pop_front()
{
	erase(begin());//_head->_next
}

size

以及size

	size_t size()
	{
		return _size;
	}

operator++(int),operator--(int)

后置--/++,返回值而不是迭代器的引用,因为核心是操作“先使用自增前的迭代器,再进行自增”,应该先返回自增前的值

list_iterator<T> operator--(int)
{
	list_iterator<T> temp = *this;//this 是一个指向当前对象的指针list_iterator<T>* ; *this 代表的是当前的 list_iterator<T> 对象、
//浅拷贝就行,让新的迭代器 _node 指向同一个节点
	_node = _node->_prev;
	return temp;
}

list_iterator<T> operator++(int)
{
	list_iterator<T> temp = *this;
	_node = _node->_next;
	return temp;
}

我们看下面一段代码,我们定义了一个AA类类型,如果不重载流运算符<<的话,需要写成(*ita)._aa1才能插入到流中

	struct AA
	{
		int _aa1 = 1;
		int _aa2 = 1;

		AA(int aa1,int aa2)
			:_aa1(aa1)
			,_aa2(aa2)
		{ }
	};

	void test()
	{
		list<AA> lta;
		lta.push_back(AA(1, 1));
		lta.push_back(AA(2, 2));
		lta.push_back(AA(3, 3));
		lta.push_back(AA(4, 4));

		list<AA>::iterator ita = lta.begin();
		while (ita != lta.end())
		{
			cout << (*ita)._aa1 << ":" << (*ita)._aa2;
//ita是一个迭代器,lta->_data=AA,即*ita 返回的是 AA&,这个元素是一个 AA 类型的对象,
//而对象需要通过 . 运算符访问其内部成员,如_a1,_a2
            ++ita;
		}
		cout << endl;
	}

还有一种方法,用箭头运算符(ita->_aa1)访问。不过ita是迭代器类型,为了让迭代器像指针一样用 -> 访问元素成员,必须通过重载 operator-> 来实现

operator->

步骤1:迭代器的 operator-> 必须返回一个指针(如 AA*

步骤2:编译器会用这个指针再次调用 -> 访问 _aa1 成员。

当写 ita->_aa1 时,编译器会自动展开为 (ita.operator->())->_aa1。但为了可读性,省略了后一个->

所以返回值应该是T*,&表示取地址

T* operator->()
{
	return &(_node->_data);//返回 _node->_data 这个对象的内存地址,结果是 T* 类型的指针
}

这样就可以进行->访问了

cout << ita->_aa1 << ":" << ita->_aa2<<" ";

接下来我们看这样一段代码

//打印
template<class T>
void Print(const list<T>& v)
{
	for (auto it = v.begin(); it != v.end(); ++it)
	{
		cout << *it << " ";
	}
	cout << endl;
}

v是const容器时,表示容器里面的数据不能修改,调用v.begin(),应该返回一个const迭代器,但我们之前只实现了普通迭代器,v.begin()返回普通迭代器iterator,auto it自动推导成普通迭代器类型,编译会报错,v.begin()应该返回一个const_iterator类型的迭代器,保证不能修改。接下来我们进行const迭代器的操作。

刚刚介绍了非const迭代器,成员函数operator*/operator-> 的返回值都可以被修改;而非const迭代器要求不能通过迭代器修改元素

那我们是新建一个const_iterator类型,还是在iterator的基础上+const 呢?const iterator

const iterator:指迭代器本身不能修改

const_iterator:指向内容不能修改

我们想要的非const迭代器是 指向的内容不能修改,而不是迭代器本身不能修改,因为迭代器需要进行++/--等操作进行移动,移动到下一个节点,因此定义一个const_iterator 迭代器

const迭代器只需要将operator*/operator-> 的返回值前面+const,表示返回值的指向内容不能修改

const迭代器

	template<class T>
	struct list_const_iterator
	{
		typedef list_node<T> Node;
		typedef list_const_iterator<T> Self;

		Node* _node;

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

		Self& operator++()
		{
			_node = _node->_next;//更新迭代器内部存储的节点指针
			return *this;
		}

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

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

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

		const T& operator*()//返回你的别名,但返回的是const别名,将权限缩小了之后再返回,修改不了
		{
			return _node->_data;
		}

		const T* operator->()
		{
			return &_node->_data;//返回const指针
		}

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

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

begin,end

以及const迭代器的begin,end

template<class T>
class list
{
	typedef list_node<T> Node;
public:
	typedef list_iterator<T> iterator;

	typedef list_const_iterator<T> const_iterator;

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

	//const迭代器
	const_iterator begin()const
	{
		const_iterator it = _head->_next;
		return it;
	}
	const_iterator end()const
	{
		const_iterator it = _head;
		return it;
	}
};

返回 const_iterator 解决的是 “迭代器能否修改元素” 的问题,而成员函数加 const 解决的是 “const 容器能否调用这个函数” 的问题

但是这两类迭代器const与非const大部分代码都相似,单独写两个类太麻烦了,因此在这两个类的基础上再实现一个类,同一个类模板,增加两个模板参数来控制。

template<class T,class Ref,class Ptr>
struct list_iterator
{
	typedef list_node<T> Node;
//T表示迭代器的元素类型,Ref表示operator*的返回值,T& or const T&,Ptr表示的是operator->的返回值,T* or const T*
	typedef list_iterator<T, Ref, Ptr> Self;

	Node* _node;

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

	Self& operator++()
	{
		_node = _node->_next;//更新迭代器内部存储的节点指针
		return *this;
	}

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

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

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

	Ref operator*()//返回你的别名返回的是const别名,or 非const别名
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;//返回const指针
	}

	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:
//通多指定不同的Ref 和 Ptr,让同一个list_iterator模板实例化出两种迭代器类型
		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;
		}
		iterator end()
		{
			return _head;
		}

		//const迭代器
		const_iterator begin()const
		{
			const_iterator it = _head->_next;
			return it;
		}
		const_iterator end()const
		{
			const_iterator it = _head;
			return it;
		}

接下来看迭代器失效的问题

1.insert

不会引起迭代器失效的问题,每个节点是单独储存的,在pos节点之前插入新节点,但本身迭代器pos指向的节点_node仍然不变。

规范写的话,insert加上返回值,返回新插入节点的迭代器

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

		++_size;
		return newnode;//隐式类型转换
	}

2.erase

void erase(iterator pos)

被删除节点的迭代器pos会失效,pos->_node在函数结束后会被释放,但形参仍保留着pos迭代器的地址,如果进行++等操作,操作对象就是野指针。

比如删除一个偶数

void test()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	Print(lt);

	list<int>::iterator it = lt.begin();

	while (it != lt.end())
	{
		if (*it % 2 == 0)
		{
			lt.erase(it);
		}
		it++;//it是野指针
	}
}

这样我们可以将erase函数设置一个返回值,返回下一个节点的迭代器。

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

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

			delete pos._node;
			--_size;

			return next;
		}
void test()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	Print(lt);
	list<int>::iterator it = lt.begin();

	while (it != lt.end())
	{
		if (*it % 2 == 0)
		{
			it=lt.erase(it);
		}
		else
		{
			it++;
		}
	}
	Print(lt);
}

但其他迭代器不受影响,因为其他迭代器指向的节点地址都不会被改变。

继续完善list类

~list与clear

		~list()//将节点一个一个的释放
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		void clear()//不清除哨兵位
		{
			auto it = begin();
			while (it != end())
			{
				it=erase(it);//it接收删除后的下一个迭代器
			}
		}

实现深拷贝

	//lt2(lt1),this是list2,lt1是list1
	list(const list<T>& lt)
	{

		for (auto& e : lt)
		{
			push_back(e);//遍历原链表的每个元素,为每个节点创建新空间,
		}
	}

不可以这样写,push_back的前提是得有一个哨兵位,使得_head不为nullptr,否则_head是一个野指针,无法进行插入

因为调用的是拷贝构造list<int>lt1(lt2),构造函数不会再调用其他构造函数,拷贝构造也是构造函数,

因此我们委托构造,让拷贝构造先调用默认构造的逻辑,实现空链表的初始化

void empty_init()
{
	_head = new list_node<T>(T()); //创建一个_head的哨兵节点
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}

//lt2(lt1),this是list2,lt1是list1
list(const list<T>& lt)
{
	empty_init();

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

以及赋值的深拷贝

//lt1=lt3
list<T>& operator=(list<T> lt)//lt是lt3的拷贝(拷贝构造函数创建的深拷贝),lt与lt3是两个完全独立的链表
{
	swap(lt);//交换这个深拷贝lt 和 lt1
	return *this;
}
void swap(list<T>& lt)
{
	std::swap(_head, lt._head);//交换两个变量的值
	std::swap(_size, lt._size);
}

函数结束后,lt3 的副本 lt 也会被销毁。

initializer_list初始化链表

C++11支持initializer_list初始化链表,允许使用花括号 {} 传递一组同类型的值。

    initializer_list<int> lt = {1,2,3};

用initializer_list<T>类型,是为了支持花括号 {} 初始化

	list(initializer_list<T> il)
	{
		empty_init();
		for (auto& e : il)
		{
			push_back(e);
		}
	}
	initializer_list<int> lt1({ 1,2,3 });

当写 list<int> lt = {1,2,3}; 时,编译器会自动将 {1,2,3} 隐式类型转换为initializer_list<int> 类型的临时对象,然后调用参数类型为initializer_list<T>的构造函数,用这个临时的对象初始化lt1。

list的迭代器与vectoe的迭代器的对比

list进行插入,迭代器不会失效。

std::list<int> l = {1, 3, 4};
auto it = ++l.begin();  // it 指向 3(第二个节点)

l.insert(it, 2);  // 在 3 前面插入 2,链表变为 {1,2,3,4}
// it 仍然指向 3(节点地址未变),*it 结果仍为 3(有效)

而vector进行插入,有时会触发扩容,将原有元素全部复制到新内存中,导致指向旧内存的原迭代器失效

std::vector<int> v = {1, 3, 4};
v.reserve(3);  // 固定容量为 3(避免自动扩容,仅演示移动导致的失效)
auto it = ++v.begin();  // it 指向 3(第二个元素)

v.insert(it, 2);  // 插入后变为 {1,2,3,4},原 3、4 向后移动
// it 原本指向旧位置的 3,但插入后该位置变为 2,it 现在指向 2(失效,因为指向的元素变了)

扩容

std::vector<int> v = {1, 2, 3};
auto it = v.begin();  // 指向 1(旧内存地址)

v.push_back(4);  // 若容量不足,触发扩容(分配新内存)
// it 指向旧内存(已释放),彻底失效

进行删除操作时,list中被删除的节点会失效,其他迭代器仍然有效

std::list<int> l = {1, 2, 3, 4};
auto it1 = l.begin();       // 指向 1
auto it2 = ++l.begin();     // 指向 2(准备删除的节点)
auto it3 = ++++l.begin();   // 指向 3

l.erase(it2);  // 删除节点 2,链表变为 {1,3,4}
// it2 失效(指向已释放的节点)
// it1 仍指向 1,it3 仍指向 3(均有效)

而vector进行删除时,删除位置后的元素向前挪动,进行覆盖,被删除元素的迭代器失效,以及被删除之后的元素的迭代器也会失效

std::vector<int> v = {1, 2, 3, 4};
auto it1 = v.begin();       // 指向 1
auto it2 = ++v.begin();     // 指向 2(准备删除的元素)
auto it3 = ++++v.begin();   // 指向 3

v.erase(it2);  // 删除 2 后,3、4 向前移动,变为 {1,3,4}
// it2 失效(指向被删除的位置)
// it3 原本指向 3,但移动后 3 的位置变了,it3 现在指向的是原 4 的位置(值为 4),失效

网站公告

今日签到

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