【C++】STL——list的模拟实现

list的模拟实现

在这里插入图片描述

一、list三个基本类的模拟实现总览

前面list的学习中我们得知,list其实就是一个带头双向循环链表:

image-20221220162602095

现在要模拟实现list,要实现下列三个类:

  1. 模拟实现节点类
  2. 模拟实现迭代器的类
  3. 模拟list主要功能的类

这三个类的实现层层递进,我们的目标是为了实现list类,然而list的模拟实现其基本功能要建立在迭代器类和节点类均已实现好的情况下才得以完成。

namespace list_realize
{
     
     
	//模拟实现list当中的结点类
	template<class T>
	struct List_node
	{
     
     
		//成员函数
		List_node(const T& val = T()); //构造函数

		//成员变量
		T _data;                 //数据域
		List_node<T>* _next;   //后继指针
		List_node<T>* _prev;   //前驱指针
	};

	//模拟实现list正向迭代器
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
     
     
		typedef List_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> Self;

		__list_iterator(node* pnode);  //构造函数

		//各种运算符重载函数
		Self& operator++();
		Self& operator--();
		Self operator++(int);
		Self operator--(int);
		bool operator==(const Self& it) const;
		bool operator!=(const Self& it) const;
		Ref operator*();
		Ptr operator->();

		//成员变量
		node* _pnode; //一个指向结点的指针
	};

    //模拟实现list反向迭代器
	template<class Iterator, class Ref, class Ptr>
	class reverse__list_iterator
	{
     
     
		typedef reverse__list_iterator<Iterator, Ref, Ptr> Self;

		reverse__list_iterator(Iterator* it);  //构造函数

		//各种运算符重载函数
		Self& operator++();
		Self& operator--();
		Self& operator++(int);
		Self& operator--(int);
		bool operator==(const Self& rit) const;
		bool operator!=(const Self& rit) const;
		Ref operator*();
		Ptr operator->();

		//成员变量
    private:
		Iterator _it;//正向迭代器
	};

	//模拟实现list
	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;
        typedef reverse__list_iterator<iterator, T&, T*> reverse_iterator;
		typedef reverse__list_iterator<const_iterator, const T&, const T*> const_reverse_iterator;

		//默认成员函数
		list(); // 无参构造
        template <class InputIterator> 
		list(InputIterator first, InputIterator last); // 迭代器构造
        list(size_t n, const T& val = T()) // 带参构造
		list(const list<T>& lt); // 拷贝构造
		list<T>& operator=(const list<T>& lt); // 赋值运算符重载
		~list(); // 析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;

		//访问容器相关函数
		T& front();
		T& back();
		const T& front() const;
		const T& back() const;

		//插入、删除函数
		void insert(iterator pos, const T& val = T());
		iterator erase(iterator pos);
		void push_back(const T& x);
		void pop_back();
		void push_front(const T& x);
		void pop_front();

		//其他函数
        void empty_initialize();
		size_t size() const;
		void resize(size_t n, const T& val = T());
		void clear();
		bool empty() const;
		void swap(list<T>& lt);

	private:
		node* _head; //指向链表头节点的指针
        size_t _size; // 链表节点个数
	};
}


二、节点类接口实现

因为list的本质为带头双向循环链表,所以其每个节点都要确保有下列成员:

  1. 前驱指针
  2. 后继指针
  3. data值存放数据

而节点类的内部只需要实现一个构造函数即可。

模拟实现

template<class T>
struct List_node //因为成员函数都是公有,就不设计成class了,直接struct
{
     
     
	List_node<T>* _next; // 前驱指针
   	List_node<T>* _prev; // 后继指针
    	T _data; // 记录存放数据

    List_node(const T& val = T())
        :_next(nullptr)
        , _prev(nullptr)
        , _data(val)
{
     
     }
};

构造函数

构造函数这里是用来对其成员变量的一个初始化。

List_node(const T& val = T())
   :_next(nullptr)
   , _prev(nullptr)
   , _data(val)
{
     
     }

三、迭代器类接口实现

这里强调下,因为list的特殊性,其本质是带头双向循环链表,对于链表,我们已然得知其内存空间是不连续的,是通过结点的指针顺次链接,我们不能像先前的string和vector一样直接解引用去访问其数据,结点的指针解引用还是结点,结点指针++还是结点指针,归根结底在于list物理空间不连续。而string和vector的物理空间是连续的,所以这俩不需要实现迭代器类,可以直接使用。

为了能让list像vector一样去解引用,++访问到下一个数据,我们需要单独写一个迭代器类的接口实现,在其内部进行封装补齐相应的功能,而这就要借助运算符重载来完成。

  • 注意:

迭代器又分为正向迭代器和反向迭代器。

1.正向迭代器

  • 注意:

这里我迭代器类的模板参数里面包含了3个参数:

template<class T, class Ref, class Ptr>

而后文list类的模拟实现中,我对迭代器进行了两种typedef:

typedef __list_iterator<T, T&, T*> iterator;//普通迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator;//const迭代器

根据这里的对应关系:Ref对应的是&引用类型,Ptr对应的是*指针类型,这里如果我是普通对象传过来的迭代器,生成对应的普通迭代器,如果是const对象传递过来的迭代器,会生成对应的const迭代器。

这样做的原因在于避免单独写一个支持不能修改迭代器指向结点数据的类而造成的复用性差。

默认成员函数

这里的默认成员函数我们只需要写构造函数。

  • 析构函数 – 结点不属于迭代器,不需要迭代器释放
  • 拷贝构造 – 默认浅拷贝即可
  • 赋值重载 – 默认浅拷贝即可

构造函数

这里我们通过结点的指针即可完成构造。通过结点指针构造一个迭代器。

__list_iterator(node* pnode)//通过结点指针构造一个迭代器
	:_pnode(pnode)
{
     
     }

六种运算符重载 */->/++/–/!=/==

  • *** 运算符重载**

*解引用的目的是为了获取结点里的_ data数据,因此我们直接return返回结点指向的_ data即可。

Ref operator*()//结点出了作用域还在,用引用返回
{
     
     
	return _pnode->_data;//返回结点指向的数据
}
  • -> 运算符重载

假设出现这样的场景,我链表存储的不是内置类型,而是自定义类型,如下:

struct AA
{
     
     
	AA(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{
     
     }
	int _a1;
	int _a2;
};
void test()
{
     
     
	cpp::list<AA> lt;
	lt.push_back(AA(1, 1));
	lt.push_back(AA(2, 2));
	lt.push_back(AA(3, 3));
	lt.push_back(AA(4, 4));
}

对于内置类型和自定义类型成员的指针,其访问方式都是不同的:

int*  *it
AA*   (*it). 或者 it->

而这里我们应该重载一个->运算符。以便于访问自定义类型成员的指针的数据。

//->运算符重载
Ptr operator->()
{
     
     
return &(operator*());//返回结点指针所指结点的数据的地址
//或者return &_pnode->_data;
}

实现了->运算符重载后,我们执行it->_ a1,编译器将其转换成it.operator->(),此时获得的是结点位置的地址即AA*,而理应还有一个箭头->才能获取数据,也就是这样:it.operator->()->_a1

  • **总结:**编译器为了可读性进行优化处理,如果不优化应该是it->->_a1,优化以后,省略了一个箭头->。
  • ++ 运算符重载

++运算符分为前置++和后置++

  • 前置++

迭代器++的返回值还是迭代器,这里的++是为了让结点指向下一个结点的指针,注意前置++是要返回自增后的结点指针。

Self& operator++()//迭代器++的返回值还是迭代器
{
     
     
	_node = _pnode->_next;//直接让自己指向下一个结点即可实现++
	return *this;//返回自增后的结点指针
}
  • 后置++

为了区分前置++,后置++通常要加上一个参数以便区别。此外,后置++是返回自增前的结点指针。

Self operator++(int)//加参数以便于区分前置++
{
     
     
	Self tmp(*this);//拷贝构造tmp
	_node = _pnode->_next;//直接让自己指向下一个结点即可实现++
	return tmp;//注意返回tmp,才是后置++
}
  • – 运算符重载

–运算符也分前置–和后置–

  • 前置–

前置–是让此结点指向上一个结点,最后返回自减后的结点指针即可。

self& operator--()
{
     
     
	_pnode = _pnode->_prev;//让_node指向上一个结点即可实现--
	return *this;
}
  • 后置–

注意传参以区分前置–,最后返回的是自减前的结点指针即可。

self operator--(int)//记得传缺省值以区分前置--
{
     
     
	self tmp(*this);//拷贝构造tmp
	_pnode = _pnode->_prev;
	return tmp;
}
  • != 运算符重载

这里比较是否不等,是两个迭代器的比较,直接返回两个结点的位置是否不同即可。

//!=运算符重载
bool operator!=(const self& it)
{
     
     
	return _pnode != it._pnode;//返回两个结点指针的位置是否不同即可
}
  • == 运算符重载

这里直接返回俩结点指针是否相同即可。

//==运算符重载
bool operator==(const self& it)
{
     
     
	return _pnode == it._pnode;//返回俩结点指针是否相同
}

2.反向迭代器

反向迭代器是一个适配器模式(后文会将适配器)。相较于正向迭代器,反向迭代器有下面三种主要变化:

  • 反向迭代器的++执行的操作是正向迭代器里的–,
  • 反向迭代器里的–执行的操作是正向迭代器里的++
  • 反向迭代器的*解引用和->操作指向的是前一个数据

总代码如下:

template <class Iterator, class Ref, class Ptr>
class reverse__list_iterator
{
     
     
	typedef reverse__list_iterator<Iterator, Ref, Ptr> Self;
public:
	reverse__list_iterator(Iterator it)
		:_it(it)
	{
     
     }

    Ref operator*()
    {
     
     
        Iterator prev = _it;
        return *--prev;
    }
    Ptr operator->()
    {
     
     
    	return &operator*();
    }

    Self& operator++()
    {
     
     
        --_it;
        return *this;
    }

    Self& operator--()
    {
     
     
        ++_it;
		        return *this;
    }

    Self& operator++(int)
    {
     
     
        _it--;
        return *this;
    }

    Self& operator--(int)
    {
     
     
   		_it++;
        return *this;
    }

    bool operator!=(const Self& rit) const
    {
     
     
        return _it != rit._it;
    }
    
    bool operator==(const Self& rit) const
    {
     
     
        return _it == rit._it;
    }

private:
	Iterator _it;
};

四、list类接口实现

此接口的核心任务是为了模拟实现list类的一些核心功能,好比如插入删除,迭代器等等。

在list类中的唯一成员变量即自定义类型的变量,由先前的结点类构成的头结点。

1.默认成员函数

1.1.构造函数

  • 无参构造:

此目的是为了对哨兵位的头结点_head进行初始化:

list()
{
     
     
	_head = new Node();//申请一个头结点
	_head->_next = _head;//头结点的下一个结点指向自己构成循环
	_head->_prev = _head;//头结点的上一个结点指向自己构成循环
    _size = 0;
}
  • 传迭代器区间构造:

先初始化,再利用循环对迭代器区间的元素挨个尾插即可。

//传迭代器区间构造
template <class InputIterator> 
list(InputIterator first, InputIterator last)
{
     
     
    empty_initialize();
    while (first != last)
    {
     
     
        push_back(*first);
        ++first;
    }
}
  • 带参构造:

初始化n个值为val

list(size_t n, const T& val = T())
{
     
     
	empty_initialize();
	for (size_t i = 0; i < n; i++)
	{
     
     
		push_back(val);
	}
}

1.2.析构函数

这里可以先复用clear函数把除了头结点的所有结点给删除掉,最后delete头结点即可。

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

1.3.拷贝构造函数

假设要用lt1拷贝构造lt2。

  • 传统写法:

首先复用empty_init对头结点进行初始化,接着遍历lt1的元素,在遍历的过程中尾插将lt1的元素尾插到lt2上即可。这里直接利用push_back自动开辟空间完成深拷贝。

list(const list<T>& lt)
//list(const list& lt) // 不建议,不规范
{
     
     
	empty_initialize();
	for (const auto& e : lt)
	{
     
     
		push_back(e);
	}
}
  • 现代写法:

这里先初始化lt2自己,再把lt1引用传参传给lt,传lt的迭代器区间构造tmp,复用swap交换头结点指针即可完成深拷贝的现代写法。

list(const list<T>& lt)
{
     
     
	empty_initialize(); // 空初始化
	list<T> tmp(lt.begin(), lt.end()); // 使用迭代器区间构造tmp
	swap(tmp); // 交换头结点指针
}

1.4.赋值运算符重载函数

假设要把lt1赋值给lt2。

  • 传统写法:
list<T>& operator=(const list<T>& lt)
{
     
     
	if (this != &lt)
	{
     
     
		clear();
		for (const auto& e : lt)
		{
     
     
			push_back(e);
		}
	}
	return *this;
}
  • 现代写法:

这里直接给出现代写法。注意这里传值传参把lt1传给lt自定义类型传值传参调用拷贝构造,拷贝构造完成的是深拷贝生成了lt,复用swap函数交换lt1与lt的头结点指针指向即可,最后返回*this。

list<T>& operator=(list<T> lt)//套用传值传参去拷贝构造完成深拷贝
{
     
     
	swap(lt);
	return *this;
}

2.访问相关函数

2.1.front和back

front和back函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可。

T& front()
{
     
     
	return *begin(); //返回第一个有效数据的引用
    // return _head->_next->_data;
}
T& back()
{
     
     
	return *(--end()); //返回最后一个有效数据的引用
    // return _head->_prev->_data;
}

我在这里

const T& front() const
{
     
     
    return *begin();
	//return _head->_next->_data;
}

const T& back() const
{
     
     
	return _head->_prev->_val;
}

3.迭代器相关函数

3.1.begin和end

  • begin的作用是返回第一个位置的结点的迭代器,而第一个结点就是哨兵位头结点的下一个结点,因此,直接return返回_head的_next即可。
  • end的作用就是返回最后一个有效数据的下一个位置的迭代器,而这里对于list指的就是头结点_head的位置。

begin和end均分为普通对象调用和const对象调用,因此要写两个版本。

  • 普通对象调用版
iterator begin()//begin返回的就是第一个有效数据,即头结点的下一个结点
{
     
     
	return iterator(_head->_next);//构造了一个匿名对象,通过调用构造函数利用头结点指向的第一个结点作为参数,来返回头结点
	//return _head->_next;  也可以这样写
}

iterator end()
{
     
     
	return iterator(_head);//end返回的是最后一个结点的下一个结点,就是头结点_head
	//return _head;  也可以这样写
}
  • const对象调用版
const_iterator begin() const
{
     
     
	return const_iterator(_head->_next);
	//return _head->_next; 
}
const_iterator end() const
{
     
     
	return const_iterator(_head);
	//return _head;  也可以这样写
}

3.2.rbegin和rend

rbegin就是正向迭代器里end()的位置,rend就是正向迭代器里begin()的位置。

image-20221222204544241

rbegin和rend同样分为普通对象调用和const对象调用:

  • 普通对象调用:
reverse_iterator rbegin()
{
     
     
	return reverse_iterator(end());
}

reverse_iterator rend()
{
     
     
	return reverse_iterator(begin());
}
  • const对象调用:
const_reverse_iterator rbegin() const
{
     
     
	return const_reverse_iterator(end());
}

const_reverse_iterator rend() const
{
     
     
	return const_reverse_iterator(begin());
}

4.插入删除相关函数

4.1.insert

实现insert首先创建一个新的结点存储插入的值,接着取出插入位置pos的结点为cur,记录cur的上一个结点位置prev,先链接prev和newnode,再链接newnode和cur即可。最后记得要返回新插入元素的迭代器位置。

//insert,插入pos位置之前                                                                               
iterator insert(iterator pos, const T& val)
{
     
     
	node* newnode = new node(val); //创建新的结点
	node* cur = pos._pnode; //迭代器pos处的结点指针
	//链接prev和newnode
	node* prev = cur->_prev;
	prev->_next = newnode;
	newnode->_prev = prev;
   	//链接newnode和cur
	newnode->_next = cur;
	cur->_prev = newnode;
	++_size;
    //返回新插入元素的迭代器位置
	return iterator(newnode);
}                                                                                                
  • **补充:**list的insert不存在野指针和意义改变的迭代器失效问题。

4.2.push_back

  • 法一:

首先,创建一个新结点用来存储尾插的值,接着找到尾结点。将尾结点和新结点前后链接构成循环,再将头结点和新结点前后链接构成循环即可。

void push_back(const T& x)
{
     
     
	Node* tail = _head->_prev;//找尾
	Node* newnode = new Node(x);//创建一个新的结点
	//链接tail和newnode
	tail->_next = newnode;
	newnode->_prev = tail;
	//链接newnode和头结点_head
	newnode->_next = _head;
	_head->_prev = newnode;
}
  • 法二:

这里也可以复用insert函数,当insert中的pos位置为哨兵位头结点的位置时,实现的就是尾插,因为insert插入是在pos位置前插入,而pos位哨兵位头结点时,在其前一个位置(尾部)插入就是实现了尾插。

void push_back(const T& x)
{
     
     
	//法二:复用insert
	insert(end(), x);
}

4.3.push_front

直接复用insert函数,当pos位置为begin()时,获得的pos就是第一个有效结点数据,即可满足头插。

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

4.4.erase

erase删除的是pos位置的结点,首先取出pos位置的结点为cur,记录cur上一个结点的位置为prev,再记录cur下一个结点的位置为next,链接prev和next,最后delete释放cur的结点指针即可。最后记得返回删除元素后一个元素的迭代器位置。

iterator erase(iterator pos)
{
     
     
     assert(pos != end())
     node* cur = pos._pnode;
     node* prev = cur->_prev;
     node* next = cur->_next;
     //链接prev和next
     prev->_next = next;
     next->_prev = prev;
     delete cur;
     cur = nullptr;
     _size--;
     return iterator(next);//返回删除元素下一个元素的迭代器位置
 }

erase存在迭代器失效问题,为了避免此问题,我们修改了返回值为iterator。

4.5.pop_back

直接复用erase即可,当pos位置为–end()时,pos就是最后一个结点的位置,实现的就是尾删。

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

4.6.pop_front

直接复用erase即可,当pos位置为begin()时,pos就是第一个有效数据,实现的就是头删。

void pop_front()
{
     
     
	erase(begin());
}

5.其它相关函数

5.1.resize

resize函数的规则:

  • 若当前容器的size小于所给n,则尾插结点,直到size等于n为止。
  • 若当前容器的size大于所给n,则只保留前n个有效数据。

实现resize函数时,不要直接调用size函数获取当前容器的有效数据个数,因为当你调用size函数后就已经遍历了一次容器了,而如果结果是size大于n,那么还需要遍历容器,找到第n个有效结点并释放之后的结点。

这里实现resize的方法是,设置一个变量oldsize,用于记录当前所遍历的数据个数,然后开始变量容器,在遍历过程中:

当oldsize大于或是等于n时遍历结束,此时说明该结点后的结点都应该被释放,将之后的结点释放即可。
当容器遍历完毕时遍历结束,此时说明容器当中的有效数据个数小于n,则需要尾插结点,直到容器当中的有效数据个数为n时停止尾插即可。

void resize(size_t newsize, const T& val = T())
{
     
     
	size_t oldsize = size();
	if (newsize <= oldsize)
	{
     
     
		// 有效元素个数减少到newsize
		while (newsize < oldsize)
		{
     
     
			pop_back();
			oldsize--;
		}
	}
	else
	{
     
     
		while (oldsize < newsize)
		{
     
     
			push_back(val);
			oldsize++;
		}
	}
}

5.2.clear

clear的作用是清除除了头结点外的所有结点,这里可以复用erase并通过遍历的方式逐个删除。

void clear()
{
     
     
	//复用erase
	iterator it = begin();
	while (it != end())
	{
     
     
		it = erase(it);//用it接收删除后的下一个结点的位置
	}    
}

5.3.empty_initialize

由于对头结点初始化使用频繁,我们特定把它封装成一个函数,作用就是把哨兵位的头结点开辟出来。

//空初始化  对头结点进行初始化
void empty_initialize()
{
     
     
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;
    _size = 0;
}

5.4.swap

对于链表的swap,直接交换头结点指针的指向即可完成。直接复用库函数的swap即可。

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

猜你喜欢

转载自blog.csdn.net/m0_64224788/article/details/129281780