您现在的位置是:首页 >技术教程 >STL-list的模拟实现网站首页技术教程

STL-list的模拟实现

风君子吖 2024-06-14 17:20:26
简介STL-list的模拟实现

目录

基础概念

模拟实现

1.list_node的实现

2. list的基础框架

<1>成员变量

<2>无参构造函数

3.__list_iterator(list的迭代器实现) 本文章最重要内容

<1>__list_iterator的构造函数和拷贝构造函数

<2>*重载

<3>++和--的重载

<4>!= 和 == 的重载

4.继续实现list

<1>push_back接口

<2>insert

<3>list的迭代器接口begin和end   重点内容

<*>empty

<4>erase

<5>套用erase和insert 实现push_back、push_front 和 pop_back、pop_front函数

<6>.迭代器构造函数  

<*>swap

<7>现代写法的拷贝构造函数和赋值拷贝

<*>clear

 <8>析构函数

<*>扩展  重要内容

总结


基础概念

在C++中,list的实现是以链表的方式实现的,链表的特点就是在内存中不是连续的空间存储,而是不断开辟新的随机的内存来存放数据,这种存储方式最大的优点就是插入数据不会像string和vector一样可能需要挪动数据,造成大量的时间成本。

但是它的缺点也很明显

1.大量的不连续空间存储可能会导致中间的内存碎片多却又无法有效利用。

2.无法支持随机访问,像sting和vactor可以通过[num]进行随机访问。

3.每次插入数据都要去申请新的空间,缓存利用率不高。

但是即便如此,链表仍然是STL中的重要组成部分,它的缺点不至于掩盖它的优点。下面我们来简单模拟实现STL中常用的接口函数。

模拟实现

1.list_node的实现

从上面的图我们可以知道,链表是由一个接着一个节点存储的,节点内部有它所存储的数据和指向上一个节点和下一个节点的指针,所以我们可以通过结构体来实现list_node。

template<typename T>
	struct list_node {
		typedef list_node<T> node;

		T _data;
		node* _prev;
		node* _next;

		list_node(const T& val = T()) //缺省值为T的默认构造
			:_data(val)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

对于list_node的构造函数,我们只需要传入一个值就可以了,并且将_prev和_next设为nullptr(空指针)

问题1:为什么使用struct而不是使用class?

class对于封装比较严密,我们如果使用class,不利于list去访问它的节点,所以使用struct,后面你可以清晰的感受到。

问题2:这里需不需要写它的析构函数?

不需要,如果你类和对象学的扎实,这三个成员变量都是内置类型,会调用自己的析构函数。

2. list的基础框架

<1>成员变量

class list { 
public:
	typedef list_node<T> node;
private:
	node* _head;
};

这个没什么好说的,list只指向_head哨兵位节点。

<2>无参构造函数

void ini_empty()
{
	//初始化哨兵位节点
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}
//无参构造函数
list()
{
	ini_empty();
}

我们只需要对哨兵位进行开辟空间,并且当链表没有存储任何数据的时候,_head的_next和_prev都指向自己。

3.__list_iterator(list的迭代器实现) 本文章最重要内容

之前我们学过string和vector话,我们可以清楚的知道他们俩的迭代器都是原生指针,为什么呢?

首先我们要明白,迭代器就是为了像指针一样而存在的对于string和vector而言,因为他们本就是一段连续存储的内存空间,所以他们的迭代器就可以直接采用原生指针

那么对于链表而言,它的迭代器又是如何实现的呢?它的迭代器又怎么能像string和vector的迭代器一样能够很方便的遍历我们的存储空间呢?

template<class T, class Ref, class Ptr>
struct __list_iterator{
	typedef list_node<T> node;

    node* _node;
};

大家或许会有很多疑问,list的迭代器竟然是一个结构体、它的成员变量也是node* ,并且这个结构体的模版参数类型居然有三种。

这些问题我们慢慢来解答

首先为什么list的迭代器是一个结构体?

因为如果他要像原生指针一样的话能够遍历存储的数据,那么直接跟string和vector直接作为原生指针是无法做到的,因为它是不连续的存储空间。我们只有通过一个节点的_next才能访问到下一个节点,并且最重要的原因,因为struct和class一样,支持操作运算符重载! 

<1>__list_iterator的构造函数和拷贝构造函数

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

__list_iterator(const iterator& lt)
	:_node(lt._node){}

相信这些对大家不难理解,这里也没有什么新内容。

<2>*重载

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

重载完*,我们就可以直接通过(*迭代器)直接访问到该节点的数据。

<3>++和--的重载

iterator& operator++()   //前置++
{
	_node = _node->_next;
	return *this;
}
iterator operator++(int)  //后置++
{
	iterator tmp(*this);
	_node = _node->_next;
	return tmp;
}
iterator& operator--()   //前置--
{
	_node = _node->_prev;
	return *this;
}
iterator operator--(int)  //后置--
{
	iterator tmp(*this);
	_node = _node->_prev;
	return tmp;
}

看到这里,是否就明了了,重载完++和--后,我们就能像vector的迭代器一样能够++(--)一样访问到下一个(上一个)位置的数据了。

需要注意的是后置++和--,需要返回原来的迭代器,并且返回值因为tmp是临时变量不能传引用返回。

<4>!= 和 == 的重载

bool operator!=(const iterator& it) const
{
	return it._node != _node;
}

bool operator==(const iterator& it) const
{
	return it._node == _node;
}

4.继续实现list

<1>push_back接口

void push_back(const T& val)      //尾插
{
	node* newnode = new node(val);//创建新节点

	node* tail = _head->_prev;    //提前记录尾部节点
    //节点的指向重新设定
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->_next = _head;
}

很简单的push_back的实现,没有难度

<2>insert

iterator insert(iterator pos, const T& val)  //在迭代器pos位置插入一个节点
{  
	node* newnode = new node(val);    //创建新节点
	node* cur = pos._node;            //提前记录pos所指结点
	node* prev = cur->_prev;  
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	return pos;
}

这里return返回的是pos的迭代器位置。

<3>list的迭代器接口begin和end   重点内容

之前我们在__list_iterator中就有这么一个疑问,为什么我们这里的模版参数有三种?

Ref是指reference(引用),Ptr是指指针

这里需要配合list是否是const的

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;

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

};

以以往我们写string和vector的经验,它们的迭代器跟它们的最重要的接口就是begin和end函数,我们现在写的list也不例外。

如果我们定义一个const list,那么他就应该去构造一个const iterator,而我们如何去识别是不是const的,就可以通过多加Ref和Ptr模版参数来判断,如果是const的,那么就返回一个以<T, const T&, const T*>为参数模版的迭代器,Ref就是const T&,Ptr就是const T*,如果是识别到是以<T, const T&, const T*>为参数模版,它在调用*重载的时候就会返回const T&,反之,就是返回T&

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

--------------------------------------------------------------------------------------------------------------------------------

因为begin和end的范围是左闭右开,所以begin指向_head的_next(也就是第一个有效存储节点)

end指向的是_head(哨兵位自己)。

<*>empty

作用:判断链表是否为空

bool empty()
{
	return begin()._node == _head;  //begin是否是指向哨兵位
}

如果begin指向着是哨兵位,那么说明这个节点没有数据,也就是空链表。

<4>erase

bool empty()
{
	return begin()._node == _head;  //begin是否是指向哨兵位
}

iterator erase(iterator pos)
{
	assert(pos._node != end()._node);    //不能把哨兵位给删了
	assert(!empty());                    //不能是空链表
	node* cur = pos._node;
	node* prev = cur->_prev;
	node* next = cur->_next;
	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return iterator(next);
}

注意要把要删除的节点给delete释放了,返回的是删除节点的下一个节点。

<5>套用erase和insert 实现push_back、push_front 和 pop_back、pop_front函数

void push_back(const T& val)     //尾插
{
	/*node* newnode = new node(val);
	node* tail = _head->_prev;
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->_next = _head;*/

	//套用insert
	insert(end(), val);
}

void push_front(const T& val)    //头插
{
	//套用insert
	insert(begin(), val);
}


void pop_back()                 //尾删
{
	erase(--end());
}

void pop_front()                //头删
{
	erase(begin());
}

<6>.迭代器构造函数  

//迭代器构造
template<typename InputIterator>
list(InputIterator first, InputIterator last)
{
	ini_empty();    //初始化哨兵位
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

为什么它的迭代器构造函数是加了模版参数?    

举个例子,如果你想插入一个vector里面的一连串数据,你就可以通过传vector的迭代器的一个范围来插入数据到list中,是不是很方便!

<*>swap

作用:交换两个list的数据

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

如果我们要交换两个链表的数据,其实并不需要一个一个去挪动,只需要交换他们的哨兵位即可。

<7>现代写法的拷贝构造函数和赋值拷贝

list(const list& lt)      //在类里可以不加模版参数(但是推荐加上)
{
	ini_empty();
	list tmp(lt.begin(), lt.end());
	swap(*this, tmp);
}

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

我们推崇现代写法来写拷贝构造和赋值拷贝,因为它真的很简洁!!!

<*>clear

作用:清理链表里所有的有效存储节点(不包括哨兵位)

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

 <8>析构函数

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

与clear不同,析构函数需要将哨兵位也一同释放掉,否则会出现内存泄漏!

<*>扩展  重要内容

如果有这么一种情况,list里存储的是一个结构体或者类,并且如果我们要访问到这些结构体内的成员变量和函数,我们可以在__list_iterator重载它的->操作符,让这种情况访问数据更加方便。

以下面代码为例子

struct Pos
{
    int _a1;
    int _a2;

    Pos(int a1 = 0, int a2 = 0)
	    :_a1(a1)
	    , _a2(a2)
        {}
};
void list_test3()
{
	list<Pos> lt;
	lt.push_back(Pos(10, 20));
	lt.push_back(Pos(10, 21));

	list<Pos>::iterator it = lt.begin();
	while (it != lt.end())
	{
		//cout << (*it)._a1 << ":" << (*it)._a2 << endl;
		cout << it->_a1 << ":" << it->_a2 << endl;

		++it;
	}
	cout << endl;
}
Ref operator*()
{
	return _node->_data;
}

Ptr operator->()        //注意,这里竟然没有参数!!
{
	return &(operator*());         //这种写法是什么意思呢?
}

仔细看这里->的重载方式,&(operator*());是什么意思呢?

就是取了 _node->_data的地址。

让我们再回到上面的例子,it->_a1  ,因为我们重载的->没有参数,那么就会发现一个很奇怪的现象,是不是少了一个->,难道不应该是it->->_date吗。

其实没有错,本应该是两个->,但是因为编译器为了代码可读性,把这里的->->简化为了->。

并且如果我们在这里写了->->反而会报错!

这里的返回值与*的重载有点类似,如果是const的list ,就会返回const T*。

总结

list与 string和vector最大的区别就是迭代器的区别,所以我们学习list主要是学习它的迭代器实现方式。

STL是具有相似性的,学会了一种,其实在使用的角度,其他的都会使用了,即便他们数据结构完全不一样。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。