《C++ list详解》

目录

节点篇

迭代器篇

链表篇

构造函数

拷贝构造函数

赋值=重载

迭代器

析构函数

插入和删除

补充篇

迭代器失效

节点中的数据自定义类型的情况


节点篇

链表中的节点包含储存的数据前一个节点的指针后一个节点的指针。在实现节点时还要将其初始化。那么这里我们不知道模板参数T具体是什么类型,就给了一个缺省参数,使用T的构造函数初始化生成一个匿名对象作为val的默认值,使用const T&延长了匿名对象的生命周期。

特别注意一点,初始化列表的写法,第一行的:别写成了;。

//T是节点中存储的数据类型
	template <class T>
	//节点结构
	struct List_Node
	{
		 
		//节点中包含的元素
		List_Node* _next;
		List_Node* _prev;
		T _data;
		//构造函数初始化节点
		//初始化列表:别写成;了!!!!!!!!!!!!!!!
		List_Node(const T& val = T())
			: _next(nullptr)
			, _prev(nullptr)
			, _data(val)
		{}
	};

迭代器篇

链表中的迭代器与vector中的有所不同。

后者的迭代器只是一个单纯的指向数组内容的指针,由于数组的空间是连续的,所以可以满足迭代器的++,--,*等需求,但是这里链表的节点空间不是连续的,无法满足迭代器的基本需求,所以不能将迭代器定义为单纯的指向节点内容的指针。

于是聪明的人们用一个结构体模板来封装迭代器,并在结构体里面重载运算符,来实现迭代器的基本操作。

首先我们需要一个成员来表示此时指向的节点的指针,并用这个节点的指针来初始化该成员,这个成员的类型肯定也是节点的指针。

接下来我们就要完成一些符号的重载,来满足迭代器的操作需求。

还有一个问题就是不仅仅有普通迭代器iterator还要有const_iterator,为了简化代码,在实现时不实现两个模板结构体,就多使用了两个模板参数Ref和Ptr

比如说在重载*时,普通情况下我们拿到的是T&就是节点储存的数据的引用(为什么不直接返回T?原因是我们要修改内容,而返回T拿到的只是一个临时副本,无法修改),但是有时我们不希望他被修改就需要返回const T&,所以为了方便就使用Ref来替代,使用Ptr来替代也是同理。

template <class T,class Ref,class Ptr>
	struct list_iterator
	{
		typedef List_Node<T> node;
		typedef list_iterator<T, Ref, Ptr> Self;
		node* _node;
		list_iterator(node* Node)
			:_node(Node)
		{}
		 
		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 tmp;
		}

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

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

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

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

	};

链表篇

链表的成员变量主要包含头节点的指针节点个数

构造函数

构造函数的主要目的就是创建一个头节点出来,为了简化实现不同构造函数时的代码,将构造头节点的主要过程封装在函数empty_init()中。

默认构造函数里面会new一个node大小的空间,并会调用node的默认构造函数把这个节点初始化,同时返回这个空间的地址给_head,然后再改变_next和_prev的指向,并将_size置为0。

使用花括号{}构造的构造函数List(std::initializer_list<T> il),内部同样会使用empty_init(),然后再将花括号{}中的内容尾插过来。

        void empty_init()
		{
			//初始化头节点
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

	    List()
		{
			empty_init();
		}

        List(std::initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}

拷贝构造函数

1.调用empty_init()初始化头节点。

2.尾插链表内容。

        List(List<T>& lt)
		{
			empty_init();
			for (auto& t : lt)
			{
				push_back(t);
			}
		}

赋值=重载

和vector中的赋值重载类似,需要实现一个swap函数来交换链表中的内容。

当lt2给lt1赋值,首先会调用拷贝构造将lt2的内容拷贝给lt,将lt1的内容和lt交换,出了作用域lt就会销毁机lt1之前的空间就被销毁,而此时的lt1指向的就是拷贝构造出来的lt指向的空间。

		void swap(List<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		//lt1 = lt2
		List<T>& operator=(List<T> lt)
		{
			swap(lt);
			return (*this);
		}

迭代器

在链表中需要提供迭代器接口供外部使用。

那么如何在类中使用我们定义的迭代器结构体?如何将迭代器结构体和链表类联系起来呢?

解决方法是使用typedef将模板实例化的复杂类型嵌套定义为类的成员类型在类中有了成员类型就可以使用这个成员类型来定义变量了

		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*> const_iterator;

为了满足使用需求,还需要提供供外部使用的迭代器,分为普通迭代器const迭代器这里返回的是一个调用迭代器类中的构造函数的匿名对象。

本来的过程是,先在函数内构造匿名对象,然后通过拷贝构造函数将这个匿名对象拷贝到返回值位置,最后匿名对象被销毁。

编译器优化后变为,直接在返回值的内存位置构造这个匿名对象,完全跳过了临时对象的创建和拷贝步骤。

		iterator begin()
		{
			//返回的是一个匿名对象
			//调用构造函数
			return iterator(_head->_next);
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}
		iterator end() 
		{
			return iterator(_head);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}

析构函数

链表的各个节点不是存在于一块连续的空间, 空间没法一次性释放,必须将节点逐个释放。所以要实现一个函数clear()来释放链表中各个节点(头节点除外,因为clear()不止用在析构函数中)的空间。将头节点空间释放。

		~List()
		{
			clear();
			delete _head;
			_head = nullptr;
			_size = 0;
		}
		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

插入和删除

插入和删除的函数之间都有很强的复用性。实现节点的插入和删除,只需改变目标节点的_prev和_next即可,插入要创建节点空间,删除要释放节点空间。

		void push_back(const T& val)
		{
			/*node* newnode = new node(val);
			node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_next = _head;
			newnode->_prev = tail;
			_head->_prev = newnode;
			_size++;*/
			insert(end(), val);
			_size++;
		}
		void push_front(const T& val)
		{
			insert(begin(), val);
		}
		void pop_back()
		{
			erase(--end());
		}
		void pop_front()
		{
			erase(begin());
		}
		 
		void insert(iterator pos, const T& val)
		{
			node* pcur = pos._node;
			node* prev = pcur->_prev;
			node* newnode = new node(val);
			newnode->_prev = prev;
			newnode->_next = pcur;
			prev->_next = newnode;
			pcur->_prev = newnode;
			_size++;
		}
		iterator erase(iterator pos)
		{
			assert(pos != end());
			node* pcur = pos._node;
			node* prev = pcur->_prev; 
			node* next = pcur->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pcur;
			--_size;
			//erase删完返回下一个节点的地址
			return next;
		}

补充篇

迭代器失效

list的erase在使用过后会将pos指向的那块空间释放并置为nullptr,那删除后就无法执行迭代器的相关操作,所以要在erase函数中返回下一个节点的地址,在循环中连续使用erase时就要重新给迭代器赋值

		while (it != lt.end())
		{
			//删除偶数
			if (*it % 2 == 0)
			{
				//迭代器失效了,重新赋值
				it = lt.erase(it);
			}
			else
			{
				it++;
			}
		}

节点中的数据自定义类型的情况

	struct AA
	{
		int a1;
		int a2;
		AA(int a = 0, int b = 3)
			:a1(a)
			, a2(b)
		{}
	};
	void test02()
	{
		//当链表节点存储的数据是自定义类型时
		List<AA> lt;
		AA aa = { 1,2 };
		lt.push_back(aa);
		List<AA>::iterator it = lt.begin();
		
		//for (auto& e : lt)
		//{
		//	//e开始拿到的是lt的begin()迭代器,而这个迭代器呢指向的内容是一个自定义类型
		//	//而库中的<<只能打印内置类型,如果要打印就要在AA中重载<<
		//	cout << e << " ";
		//}
		while(it != lt.end())
		{
			//(*it)是一个自定义类型AA,自定义类型访问成员变量用.操作符
			cout << (*it).a1 << " " << (*it).a2 << " ";
			//自定义类型的指针可以通过->来访问成员函数和成员变量
			//it是一个迭代器,底层是一个指针,其初始化成lt的第一个节点的指针
			//那么it的内容就是一个指针,通过->可以访问节点的内容,这个内容就包含了T _data(自定义类型的对象的指针)
			//这个_data是一个指针变量,即AA的对象aa的地址
			//所以要访问aa中的a1,a2就要再次使用->
			//为了易读性,这里省略一个->
			cout << it->a1 << " " << it->a2 << " ";
			//本质是
			cout << it.operator->()->a1 << " " << it.operator->()->a2 << " ";
			it++;
		}
	}

}

"希望这篇内容能帮你少走弯路。保持好奇,持续编码!🚀" 

 

  

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值