list底层实现

目录

引言

结构形式

整体框架

插入删除接口实现

析构函数、拷贝构造、赋值重载

const迭代器实现

取得链表长度的方式

迭代器前置\后置operator++ -- 以及operator->


引言

list是非常常用的一个容器,数据结构是链表,数据空间是以链式结构存储的,物理空间上不一定是连续的,不像vector物理空间是连续的,可通过下标访问,在list中有可能排在后面的数据物理地址比在排在前面的数据小,所以它不方便通过下标访问,但它也具有它才有的特性,让我们来学习一下list的底层实现吧。

结构形式

库里实现的是带头双向循环链表,下面我们实现底层的时候也是实现该种链表。

每个节点有2个指针,一个next指向下一个节点,一个prev指向前一个节点,还有一个位置data存储对应数据。

整体框架

list 类中只需要一个成员变量,就是节点node的头指针_head,有_head就能指向其他节点。

而节点node需要将它作为一个自定义结构,里面的成员变量就是上面说的3个,_next,_prev和data

先将大框架构架出来:

namespace bc
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			,_data(x)
		{ }
	};

	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		list()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		void push_back(const T& x)
		{
			node* newnode = new node(x);
			node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	private:
		node* _head;
	};

为防止和库里的list混,将我们实现的封在namespace bc中。

list创建对象时需要实例化,list<int> , list<double> .......因此在类前给上模板参数T,

构建节点自定义类型,并在里面完成节点的默认构造初始化。

节点包装完成后是class list类的实现,成员变量是node* _head,实现无参构造函数,初始的时候就是节点的头尾指针都指向自己形成循环:

 

插入删除接口实现

 然后再实现一下push_back函数,插入数据也很简单,改变一下指针指向即可:

		void push_back(const T& x)
		{
			node* newnode = new node(x);
			node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

 

测试一下,因为没法用下标访问就想到通用的迭代器,先把测试用例写一下:

	void Test1() {
		list<int> st;
		st.push_back(1);
		st.push_back(1);
		st.push_back(1);
		st.push_back(1);
		list<int>::iterator it = st.begin();
		while (it != st.end()) {
			cout << (*it) << " ";
			++it;
		}
		cout << endl;
	}
}

然后实现迭代器,有了string和vector的经验,迭代器底层可用指针实现,那么list这里是不是也可以直接用指针指代迭代器呢?

————如果可以的话来看一下:typedef node* iterator;

用node* 指代迭代器是绝对不行的,之前说了链表的物理空间不是连续的,迭代器++ -- 的功能就是失效了,对此我们可以借鉴日期类(想比较两个日期或者++ -- 计算日期不可以,封装成类对操作符++ -- 重载就可以实现),这里也是一个道理,将迭代器封装层一个类__list_iterator就解决了

namespace bc
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			,_data(x)
		{ }
	};

	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

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

		__list_iterator<T>& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

		bool operator!=(const __list_iterator<T>& it) {
			return _pnode != it._pnode;
		}
	};


	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		typedef __list_iterator<T> iterator;

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

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

		list()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		void push_back(const T& x)
		{
			node* newnode = new node(x);
			node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	private:
		node* _head;
	};

push_back还可以通过复用insert来实现,下面来实现一下insert

insert是在指定位置之前插入,位置pos还是迭代器类型。insert会导致迭代器失效吗?显然这里插入迭代器不会失效,因为空间没变还在那里,只是在pos位置链接上其他节点(vector那块会失效是因为挪动数据使得空间位置改变了,并且还有扩容的影响)

但库里的insert给了返回类型,我们保持一致也给一个iterator。

insert代码实现也很简单,

            iterator insert(iterator pos, const T& x) {
			node* newnode = new node(x);
			node* cur = pos._pnode;
			newnode->_prev = cur->_prev;
			newnode->_next = cur;
			cur->_prev->_next = newnode;
			cur->_prev = newnode;
            return iterator(newnode);
		}

push_back,push_front 都可以复用insert:

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

erase和insert一样,也是根据迭代器位置删除。它需要返回值,返回删除位置的下一个位置的迭代器。很明显删除操作会使迭代器失效,因为删除会直接删掉节点,空间没了迭代器自然失效。

代码如下:

		iterator erase(iterator pos) {
			assert(pos != end());
			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._pnode;
			return iterator(next);
		}

 同样,pop_back,pop_front 都可以复用 erase:

		void pop_back() {
			erase(--end());
		}
		void pop_front() {
			erase(begin());
		}

析构函数、拷贝构造、赋值重载

然后实现一下拷贝构造和析构函数,先写析构吧简单一点。

在析构之前可以先实现clear,析构函数再复用clear就好了。clear不清头结点,只删后面的节点,而析构函数会清除头结点,所以应该是析构调用clear,别搞反了。

clear()可以创建cur节点遍历删除,也可以复用刚刚实现的erase函数:

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

用迭代器从头开始遍历删除,注意这里迭代器不能++(失效),可以用erase返回值,返回的是删除位置的下一个位置的迭代器,这样相当于迭代器自动往下走了。

析构函数:

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

拷贝构造传统写法:

		list(list<T>& st) {
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;	
			for (auto& e : st) {
				push_back(e);
			}
		}

首先说一下,参数list<T>& st之前我没有加const,按理说可以加上毕竟st不改变,但是加了const会报错,因为范围for底层是迭代器,

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

&st是实参的别名,是迭代器对象,const list<int>& st 作为const对象只能调用const迭代器,而我们此时还没有实现const迭代器,所以先把const去掉保证编译通过。

函数内部原理就是拷贝之前要先创建对象,初始化一下,然后用范围for插入数据,相当于拷贝过去了。

注意auto&,这里不用&消耗很大。因为范围for是将*st 拷贝给e,也就是将容器里的数据拷贝给e,

如果list<T>的T是int等内置类型还好,拷贝消耗不大,如果T是string、map等具有大量数据的容器那么拷贝消耗会很大,所以用&可以减小拷贝带来的消耗。

我看了一下库里的实现,它将list初始化写成了一个函数empty_initialize,这样方便其他函数调用,我们也跟着保持一致吧:

		void empty_initialize() {
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}
		list()
		{
			empty_initialize();
		}

		//st1(st)
		list(list<T>& st) {
			empty_initialize();
			for (auto& e : st) {
				push_back(e);
			}
		}

赋值重载传统写法:

		//st1 = st
		list<T>& operator=(list<T>& st) {
			if (this != &st);
			clear();
			for (auto& e : st) {
				push_back(e);
			}
			return *this;
		}

和拷贝构造异曲同工,const和&两个问题也是一样。

注意不要自己给自己赋值。

拷贝构造和赋值重载除了传统写法还有现代写法,在实现之前先写一个迭代器构造函数接口方便操作。

迭代器构造函数

		template <class InputIterator>
		list(InputIterator first, InputIterator last) {
			empty_initialize();
			while (first != last) {
				push_back(*first);
				++first;
			}
		}

 这种构造函数就是通过迭代器区间来构造list<T>对象。

拷贝构造现代写法:

        void swap(list<T>& st) {
			std::swap(_head, st._head);
		}		

        list(list<T>& st)
		{
			empty_initialize();
			list<T>tmp(st.begin(), st.end());
			swap(tmp);
		}

现代写法就是通过上面写的迭代器构造函数构造临时list<T>对象tmp,再通过swap函数交换tmp和st 对象的头指针,也就是交换指向的空间和数据。

这里要注意两点:

1、swap函数我们要自己实现一下,算法库提供的swap函数要进行两次深拷贝代价太大,我们借用算法库中的swap自己实现一个容器中使用的可大大降低消耗。

2、一定要在拷贝构造前先构造初始化对象st,否则st对象的_head指针就是指向一段随机空间,swap交换tmp就指向了这段随机空间,在出函数调用析构函数清除临时对象tmp时就会报错,当然也不能给st._head初始化为空,链表为空析构的时候要求至少有哨兵位的头结点,否则也会报错,所以要先构造初始化。

赋值重载现代写法:

		list<T>& operator=(list<T> st) {
			swap(st);
			return *this;
		}

有了上面的基础, operator= 就很简单了。注意传参不能给引用,否则交换的时候会将st1 和 st 空间数据交换,那么外面st 就改变了,st 真是舍己为人了。

 

const迭代器实现

承接上面的问题,现在要实现一个const迭代器。有的人可能会说,const迭代器有什么难的,重载一下函数,在前面加一个const不就搞定了?——没那么简单,这样操作的不是所谓的const迭代器,因为const对象就不一样。

 这是const迭代器吗?————显然不是!这里const修饰的是cit 对象本身,而不是它指向的值。

const int* a1  和 int* const a2是有区别的:前者const修饰的是指针指向的内容,而不是其本身,也就是它指向的值不能改变,但是a1本身可以改变,对于迭代器来说也就是迭代器指向的值不能修改,但迭代器可以++; 后者相反,迭代器本身不能修改不能++,但能改指向的值。

显然前者符合const迭代器的预期,而这里的写法是属于后者,因此不是我们要的const迭代器。

那么就到迭代器类的内部修改它的重载函数*,使得能用const对象调用该函数,看看这种方法是否可行。

 重载一下解引用* 不同对象调用不同的重载函数,这样是解决了解引用访问的问题,但是++又如何呢?难道还能搞出个const ++?

于是想到既然重载函数不行,那么是否可以再构建一个类,一个const迭代器的类。我们不想着再从迭代器类中解决问题,而是换个角度再搞个类出来,一个const_iterator。

	template<class T>
	struct __list_const_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		__list_const_iterator(node* p)
			:_pnode(p)
		{ }
		const T& operator*(){
			return _pnode->_data;
		}
		__list_const_iterator<T>& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

		__list_const_iterator<T>& operator--() {
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const __list_const_iterator<T>& it) {
			return _pnode != it._pnode;
		}
	};

整个类相对于iterator类也就是        const T& operator*()的区别,这样就可以通过不同类访问const迭代器和非const迭代器了。但要注意这是两个不同的类,绝不是相近类的概念,就像list<int>和list<double>是两个完全不同的类型一样。

然后在class list中重定义一下这个const类,增加const begin()和const end()

		typedef __list_const_iterator<T> const_iterator;
		const const_iterator begin()const {
			return const_iterator(_head->_next);
		}

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

测试一下:

void Print(const list<int>& st) {
		list<int>::const_iterator cit = st.begin();
		while (cit != st.end()) {
			cout << *cit << " ";
			++cit;
		}
	}
	void Test2() {
		list<int> st;
		st.push_back(1);
		st.push_back(2);
		Print(st);
	}

const迭代器对象只能读不能写。

但是这样写有一个缺陷,就是代码冗余,创建 __list_const_iterator类确实能解决,可只有一行不一样显得太过多余,对此库里的实现是用模板解决冗余问题。

	template<class T,class Ref>
	struct __list_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

		Ref& operator*() {
			return _pnode->_data;
		}
		__list_iterator<T,Ref>& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

		__list_iterator<T, Ref >& operator--() {
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const __list_iterator<T, Ref>& it) {
			return _pnode != it._pnode;
		}
	};



	template<class T>
	class list
	{
	public:
		typedef list_node<T> node;
		typedef __list_iterator<T,T&> iterator;
		typedef __list_iterator<T, const T&> const_iterator;

既然两个类的区别只在operator*函数的返回值,那么就再给iterator类一个模板参数Ref,去标识这个返回值是T&还是const T&。 在class list类中重定义一下,模板参数是<T,T&>就对应非const迭代器,是<T,const T&>就对应const迭代器,这样就解决了代码冗余的问题。

非const迭代器实现好了,拷贝构造和operator = 函数就可以调用非const迭代器了。

PS:因为下面还要加一个模板参数,所以为了书写方便,在这里typedef一下__list_iterator<T,Ref>

	template<class T,class Ref>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref> Self;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

		Ref& operator*() {
			return _pnode->_data;
		}
		Self& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

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

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

取得链表长度的方式

首先想到的是实现一个size接口,遍历链表得到长度,但是这样的接口开销很大,试想每一次得到长度都要遍历一次链表肯定不好。

我们可以采取空间换时间的思想,再加一个成员变量_size。

class list
{
public:
    size_t size(){
        return _size;
    }    
private:
    size_t _size;

那么在构造、增删等函数中都要再修改一下_size,尤其是swap,不然拷贝构造和赋值的对象调用size就不对了。

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

 

迭代器前置\后置operator++ -- 以及operator->

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

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

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

		Self& operator--(int) {
			Self tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}

前置与后置对于内置类型没有什么区别,但对于自定义类型还是有很大差异的,从代码角度就可以看出后置要多两次拷贝,所以还是尽量使用前置。(为区别前置与后置++ --,规定参数部分使用int区分 )

operator->的使用方式很怪,它返回的是节点数据的地址:

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

通过一个场景来理解一下:

	struct Pos
	{
		int _row;
		int _col;
		Pos(int row = 0, int col = 0)
			:_row(row)
			, _col(col)
		{}
	};
	void Test3() {
		list<Pos>st;
		st.push_back(Pos(1, 1));
		st.push_back(Pos(2, 2));
		list<Pos>::iterator it = st.begin();
		while (it != st.end()) {
			cout << it->_row <<":"<< it->_col << endl;
			++it;
		}
	}

 list的实例化模板给了一个自定义类型Pos,通过迭代器访问打印,it->_row调用了重载函数operator->   这里相当于是it.operator->(),返回值是数据的地址T*,也就是Pos*。  不对呀,Pos*怎么能打印出数据呢?————其实这里是调用了两次->  只是没有显示写出来,其实是 it->->_row,因为Pos*->_row就可以打印出数据了,两次->在使用的角度不方便理解(运算符重载的意义就是方便使用理解),所以编译器处理成了一个->的形式。


 再看如下一段代码:

	void Print(const list<Pos>& st) {
		list<Pos>::const_iterator cit = st.begin();
		while (cit != st.end()) {
			cout << cit->_row++;
			cout << cit->_row << ":" << cit->_col << endl;
			++cit;
		}
	}
	void Test3() {
		list<Pos>st;
		st.push_back(Pos(1, 1));
		st.push_back(Pos(2, 2));
		list<Pos>::iterator it = st.begin();
		while (it != st.end()) {
			cout << it->_row <<":"<< it->_col << endl;
			++it;
		}
		Print(st);
	}

在Print函数中,for循环const迭代器指向内容居然能++,cit->_row++并且编译器编译通过了,不是说const迭代器指向内容不能修改吗?

————因为operator-> 返回值都是T*,T*->可以修改指向内容。这里不论是const对象还是非const返回的都是T*,要区分const与非const对象还是用到之前的方法——模板。

	template<class T, class Ref,class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

		Ref& operator*() {
			return _pnode->_data;
		}

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

    template<class T>
	class list
	{
	public:
		typedef list_node<T> node;
		typedef __list_iterator<T, T&,T*> iterator;
		typedef __list_iterator<T, const T&,const T*> const_iterator;

上面说的第3个模板参数就是用在这里,实际上和第2个本质上是一样的。

以上就是list的底层实现,因为本人浅薄的理解可能讲的不到位,仅仅只是对自己学习的一个总结,感谢浏览!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值