序列式容器详解-stl源码剖析笔记

什么是序列式容器

研究数据的特定排列方式,以利于搜寻排序或其他目的,这一门专门的学科我们称之为数据结构。几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL容器即是将运用最广的几种数据结构实现出来。

常用的数据结构不外乎array(数组)、list(链表)、tree(树)、stack(堆栈)、queue(队列)、hash table(散列表)、set(集合)、map(映射表)等等。根据“数据在容器中的排列”特性,这些数据结构分为序列式容器和关联式容器。

所谓序列式容器,其中元素都是可序的,但未必有序。C++语言本身提供了序列式容器array,STL另外再提供vector,list,deque,stack,queue,priority-queue等序列式容器。其中stack和queue只是由deque改头换面而得,技术上被归类为配接器(adapter)。

vector

vector的数据安排及操作方式,与array极为相似。两者唯一的差别在于空间运用的灵活性,array是静态空间,一旦配置不能改变;要换个大的,一切琐碎细节都有客户端负责:首先配置一块新空间,将元素从旧地址一一搬到新地址,再把原来的空间释还给系统。vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。

vector的实现技术关键在于,其对大小的控制及重新配置时元素的移动效率。一旦空间满载,每增加一个元素扩充一个元素的空间并不明智,应该加入未雨绸缪的考虑。

vector定义摘要

以下是vector定义的源代码摘录:

template <class T, class Alloc = alloc> 
class vector {
publictypedef T	value_type;
	typedef value_type* pointer;
	typedef value_type* iterator;
	typedef value_type& reference;
	typedef size_t	size_type;
	typedef ptrdiff_t difference_type;
protectedtypedef simple_alloc<value_type, Alloc> data_allocator;
	iterator start;	//表示目前使用空间的头
	iterator finish;	//表示目前使用空间的尾
	iterator end_of_storage;	//表示目前可用空间的尾

	void insert_aux(iterator position, const T& x);
	void deallocate() {
		if (start) data_allocator::deallocate(start, end_of_storage - start);
	}
	void fill_initialize(size_type n, const T& value) {
		start = allocate_and_fill(n, value);
		finish = start + n;
		end_of_storage = finish;
	}
public:
	iterator begin() { return start; }
	iterator end() { return finish; }
	size_type size() const { return size_type(end() - begin()) };
	size_type capacity() const {return size_type(end_of_storage - begin())};
	bool empty() const { return begin() == end(); }
	reference operator[] (size_type n) { return *(begin() + n); }

	vecotr() : start(0), finish(0), end_of_storage(0) {}
	vector(size_type n, const T& value) { fill_initialize(n, value); }
	vector(long n, const T& value) { fill_initialize(n, value); }
	vector(int n, const T& value) { fill_initialize(n, value; }
	explicit vector(size_type n) { fill_initialize(n, T()); }
	~vector() {
		destroy(start, finish);	//全局函数
		deallocate();	//vector的成员函数
	}
	reference front() { return *begin(); }	//第一个元素
	reference back() { return *(end() - 1); }	//最后一个元素
	void push_back(const T& x) {	//将元素插入至尾端
		if (finish != end_of_storage) {
			construct(finish, x);
			++finish;
		}
		else {
			insert_aux(end(), x);	//vector成员函数
		}
	}
	void pop_back() {	//将尾端元素取出
		--finish;
		destroy(finish);
	}
	iterator erase(iterator position) {
		if (position + 1 != end()) copy(position+1, finish, position);	//后续元素往前移
		--finish;
		destroy(finish);	//全局函数
		return position;
	}
	void resize(size_type new_size, const T& x) {
		if (new_size < size()) erase(begin() + new_size, end());
		else insert(end(), new_size - size(), x);
	}
	void resize(size_type new_size) { resize(new_size, T()); }
	void clear() { erase(begin(), end()); }
protected:
	iterator allocate_and_fill(size_type n, const T& x) {
		iterator result = data_allocator::allocate(n);
		uninitialized_fill_n(result, n ,x);	//全局函数
		return result;
	}
};

vector迭代器

vector是连续线性空间,所以不论元素指针为何,普通指针都可以作为其迭代器而满足所有必要条件。vector迭代器所需要的操作行为,如operator*,operator->,operator++,operator–,operator+,operator-,operator+=,operator-=,普通指针都具备。vector迭代器支持随机存取,普通指针也具备。所以vector提供的是random access iterator。

vector的数据结构

vector以两个迭代器start,finish分别指向配置的连续空间的已使用部分的头和尾,并已end_of_storage指向整个连续空间(包括备用部分)的尾端。

vector的构造和内存管理

vector缺省使用alloc作为空间配置器,并另外定义了data_allocator,为的是方便以元素大小为配置单位。

template <class T, class Alloc = alloc>
class vector {
protected:
	typedef simple_alloc<value_type, Alloc> data_allocator;
}

于是data_allocator::allocate(n)表示配置n个元素的空间。

vector提供很多构造函数,其中一个允许指定配置空间大小及初值。

vector(size_type n, const T& value) {fill_initialize(n, value);}
void fill_initialize(size_type n, const T& value) {
	start = allocate_and_fill(n, value);
	finish = start + n;
	end_of_storage = finish;
}
iterator allocate_and_fill(size_type n, const T& value) {
	iterator result = data_allocator(n);
	uninitialized_fill_n(result, n, value);
	return result;
}

uninitialized_fill_n()会根据第一个参数的型别特性,决定使用fill_n()或者反复调用construct()来完成任务。

当我们调用push_back将新元素加入尾端时,先判断还有没有备用空间,如果有就在备用空间构造元素,并调整迭代器finish,如果没有则扩充空间(重新配置,移动元素,释放原空间)。
在这里插入图片描述
所谓动态增加大小,不是在原来的连续空间后增加,而是重新配置了一块空间,进行元素移动并释放原空间。故重新分配空间后,原来的迭代器全部失效,需要调整迭代器指向新的vector。

vector元素操作

这里我们介绍pop_back,erase,clear,insert。
在这里插入图片描述
在这里插入图片描述

list

list每次插入或删除一个元素,就配置或释放一个元素,对空间运用绝对精准,绝不浪费。list在任何位置的插入和删除都是常数时间。

list和vector是最常被使用的容器,什么时机下选择哪儿个容器,视元素的多寡,元素的构造复杂度,元素的存取行为特性决定。

list节点

每一个设计过list的人都知道,list和list节点是不同的结构,需要分开设计。

template<class T>
struct __list_node {
	typedef void* void_pointer;
	void_pointer prev;
	void_pointer next;
	T data;
}

list迭代器

list不能向vector那样使用普通指针作为迭代器,因为list节点不保证在空间中连续存在。list迭代器必须有能力指向list节点,并完成正确的递增,递减,取值,成员存取等操作。

STL list是双向链表,提供bidirectioinal iterators。list的插入和结合都不会引起迭代器失效,删除操作也只是引起指向删除元素的迭代器失效。
在这里插入图片描述

list数据结构

STL list不仅是一个双向链表,而且是一个环状双向链表。所以它只需要一个指针,就可以完整表示整个list。

template <class T, class Alloc = alloc>
class list {
protected:
	typedef __list_node<T> list_node;
	typedef list_node* link_type;
protected:
	link_type node;	//只需要一个指针,就可以表示整个环状双向链表
}

如果让node刻意指向尾端的一个空白节点,node便能符合STL的前开后闭区间的要求,成为last迭代器。这么一来,以下几个函数就可以轻易完成。
在这里插入图片描述

list的构造和内存管理

list缺省使用alloc空间配置器,并另外定义了list_node_allocator,为的是方便以节点为大小配置空间。list_node_allocator(n)表示配置n个节点。以下四个节点分别用来配置,释放,构造,销毁一个节点。
在这里插入图片描述
list提供很多构造函数,其中一个默认构造函数,允许我们不指定任何参数做出一个空list。
在这里插入图片描述
当我们以push_back()将新元素插入尾端时,此函数内部调用insert():
void push_back(const T& x) { insert(end(), x); }

insert()属于重载函数,有很多形式,其中一个如下满足上述要求。
在这里插入图片描述

list元素操作

在这里插入图片描述
list内部提供了一个迁移操作:将某连续范围的元素迁移至某特定位置之前。技术很简单,节点间的指针移动而已。这为其他复杂的操作比如splice,sort,merge提供了良好的基础。
在这里插入图片描述
transfer也可以接受[first, last)区间在同一个list中。该函数为非公开接口,list提供公开的结合操作:将某个连续范围的元素从一个list移动到另一个(或同一个)list的某个定点。
在这里插入图片描述
为了提供各种接口弹性,list::splice提供了很多版本。
在这里插入图片描述
以下是reverse(),merge(),sort()的源码,有了transfer这些都不难。
在这里插入图片描述

deque

vector是单向开口的连续空间,deque是双向开口的连续空间。所谓双向,是可以在头尾两端分别做插入和删除操作。从技术观点,vector也可以在头尾两端操作,但效率奇差。

deque和vector的最大差异,一个是deque允许常数时间在头部进行插入删除,一个是在于deque没有容量观念,因为它是以动态的分段连续组合而成。换句话说,向vector那种空间不足而重新分配一个大的空间,再移动元素释放原空间的操作在deque是不会发生的。也因此,deque没有空间保留功能。

虽然deque也提供random access iterator,但它的迭代器不是普通指针,其复杂度不可与vector不可以道里计,这当然影响了各个层面。因此,除非必要,我们尽量选择vector而不是deque。对deque的排序,为了高效率,可以将deque先完整复制到一个vector,将vector排序后,再复制回deque。

deque中控器

deque是由一段一段定量连续空间构成,一旦有必要在deque前端或尾端增加空间,便配置一段定量连续空间,串接在deque前端或尾端。deque的最大任务,是维持其整体连续的假象,并提供随机存取的接口。避开了“重新配置,移动,释放”的轮回,deque的代价就是复杂的迭代器。

deque即为分段连续,就必须有中央控制,而为了维持整体连续假象,数据结构的设计和迭代器的前进后退等操作都颇为繁琐,deque的代码实现分量比vector和list都多的多。

deque采用一块map作为主控,这里的map是一小块连续空间,其中每个元素(也就是每块node)都是指针,指向另一块较大的连续空间,称为缓冲区。缓冲区才是deque的存储空间主体,SGI STL允许我们指定缓冲区大小,默认值0表示使用512bytes缓冲区。

deque迭代器

deque是分段连续空间,维持其整体连续假象的任务落在了operator++,operator–两个运算子身上。

首先我们思考一下,deque迭代器应该具备什么结构。首先它必须能够指出缓冲区在哪儿,其次必须能够判断自己是否已经处于其所在缓冲区边缘,如果是,一旦前进或后退就必须跳至前一个或下一个缓冲区。为了能够正确跳跃,deque必须随时掌握管控中心map。

中控器,缓冲器,迭代器的相互关系:
在这里插入图片描述
假设我们现在产生一个deque,并令其缓冲区大小为32,那么每个缓冲区可容纳32/sizeof(int)=8个元素。经过某些操作之后,deque拥有20个元素,那么其begin(),end()传回来的迭代器如下图所示。这两个迭代器事实上一直保持在deque中,名为start(),finish()。

20个元素需要20/8=3个缓冲区,所以map运用了三个节点。迭代器start内的cur当然指向缓冲区中的第一个元素,迭代器finish内的cur当然指向缓冲中的最后元素(下一个位置)。
在这里插入图片描述
deque迭代器的加、减、前进和后退操作,在行进过程中可能会需要调用set_node跳一个缓冲区。
在这里插入图片描述

deque的数据结构

deque除了维护一个先前说的指向map的指针,还维护start和finish两个迭代器,分别指向第一个缓冲区的第一个元素,和最后一个缓冲区的最后一个元素(下一个位置)。此外它必须记住当前map的大小,因为一旦map提供的节点不足,需要重新配置一块更大的map。

template<class T, class Alloc=alloc, size_t BufSiz = 0>
class deque{
public:
	typedef T value_type;
	typedef value_type* pointer;
	typedef size_t size_type;
public:
	typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
protected:
	typedef pointer* map_pointer;
protected:
	iterator start;
	iterator finish;
	map_pointer map;
	size_type map_size;
...
}

有了上述结构,以下几个机能就可顺利完成。
在这里插入图片描述

deque的构造和内存管理

deque的构造函数调用fill_initialize(),fille_initialize负责产生并安排好deque的结构create_map_and_nodes,并将初值设置妥当。create_map_and_nodes函数先确认好所需分配的map节点数,最少8个,最多所需+2个。然后调用map_allocator::allocate(map_size)进行分配空间。并令start和finish指向所拥有全部节点的中央部分,保持在最中央,可使头尾两端的可扩充空间一样大。然后为每个map节点配置缓冲区,所有缓冲区加起来就是deque的可用空间。最后设置迭代器start和end的正确内容。

push_back和push_front函数为先判断最后一个缓冲区或第一个缓冲区有无备用空间,有则构造元素并修改迭代器finish或start的状态。如果没有备用空间,则调用reserve_map_at_back/front,判断是否需要重新构造map,需要则重新构造,不需要则保持不变。然后分配新的缓冲区,调整finish或start的状态并设置相应值。以上函数均为不成功则全部退回的策略,即回退状态,释放分配的缓冲区的操作。

reserve_map_at_back或front的核心代码:
在这里插入图片描述

deque的元素操作

deque的元素操作除了上述提到的push_back,push_front,还有pop_back,pop_front,erase,clear,insert。pop_back和pop_front都需要考虑在某种情况将缓冲区释放掉,如果最后一个缓冲区或者第一个缓冲区有一个或者多个元素,则调整指针,析构元素,如果最后一个或者第一个缓冲区为空,则需要释放掉,然后调整finish或者start的状态,析构元素。

clear清空整个deque到初始状态,deque的初始状态为保有一个缓冲区。
在这里插入图片描述
erase清楚某个点元素:
在这里插入图片描述
erase区间元素:
在这里插入图片描述
insert在某点之前插入元素,如果插入点是最前端交给push_front,如果插入点为最后端,交给push_back,否则直接进行插入:判断插入前后元素的多少,如果插入点之前的元素较少,则最前端插入一个元素与第一个元素值同,然后将第二个元素开始到pos位置的元素都往前进行移动拷贝,最后修改插入点元素的值,并返回迭代器pos同理,如果插入点之后的元素较少,则最尾端加入一个与最后一个元素相同,将插入点到尾端的元素第二个元素进行后移,最后修改插入点的值并返回迭代器。

stack

stack是一种先进后出的数据结构,只有一个出口,允许在最顶端进行新增,移除和获取值,对于其他元素没有任何方法进行存取,换句话说,stack不允许遍历。

以某种既有容器作为底部结构,改变其接口,使其符合“前进后出”的特性,形成stack是很容器的。deque是一个双向开口的结构,若以deque为底部容器封闭其头端接口,就可以形成一个stack。因此,SGI STL便以deque作为缺省情况的stack底部结构,stack的实现因而非常简单。

因为stack是以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌“的性质,所以也称为配接器。因此STL stack往往不称为容器,而称为配接器。

stack定义完整列表

template <class T, class Sequence = deque<T> >
class stack {
	friend bool operator== __STL_NULL_TMPL_ARGS(const stack&, const stack&);
	friend bool operator< __STL_NULL_TMPL_ARGS(const stack&, const stack&);
public:
	typedef  typename Sequence::value_type value_type;
	typedef typename Sequence::size_type size_type;
	typedef typename Sequence::reference reference;
	typedef typename Sequencd::const_reference const_reference;
protected:
	Sequence c;	//底层容器
public:
	//以下完全利用Sequence c的操作,完成stack的操作
	boll empty() const { return c.empty(); }
	size_type size() const { return c.size(); }
	reference top() { return c.back(); }
	const_reference top() const { return c.back(); }
	void push(const value_type& x) { c.push_back(x); }
	void pop() { c.pop_back(); }
};
template < class T, class Sequence>
bool operator==(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {
	return x.c == y.c;
}

template <class T, class Sequence>
bool operator<(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {
	return x.c < y.c;
}

stack没有迭代器

stack所有元素的进出都必须符合“先进后出”的原则,只有stack顶端的元素才能够被外界存取,stack不提供走访功能,也不提供迭代器。

以list作为stack的底层容器

除了deque以外,list也是双向开口的数据结构。上述源码中的stack的各种函数list也都具备。若以list为底部容器并封装头部接口,一样可以得到stack。

queue

queue是一种先进先出的数据结构,有两个出口。queue允许新增移除,从低端加入,从顶端取元素。但除了低端加入,顶端取出外,没有其他任何方法可以存取queue其他元素。换言之,queue不接受遍历。

以某种既有容器作为底部结构,将其接口改变,使之符合先进先出的特性,形成一个queue,很容易做到。deque是双向开口的数据结构,若以deque为底部结构,封闭其低端出口和前端入口,就可以得到一个queue。因此,SGI STL便以deque作为其缺省底部结构。

由于queue是以底部容器完成其所有工作,而具有“修改某物接口,使之符合另一种风貌”的特性者,称为配接器。因此STL queue往往不被称为容器,而被称为容器配接器。

queue没有迭代器

queue所有元素都必须符合先进先出条件。只有queue顶端元素才能被外界获取。queue不提供遍历功能,也不提供迭代器。

以list作为queue的底层容器

除deque外,list也是双向开口容器。queue使用的底层容器的函数list也都具备。因此,以list为底部结构封装其头端开口,一样可以得到queue。

heap(隐式表述)

heap并不属于STL容器,它是个幕后英雄,扮演priority queue的角色。priority queue允许用户以任何次序推入元素,但取出时必须从优先级最高也就是数值最高的数取出。binary max heap正具有这样的特性,适合作为priority queue的底部机制。

若果使用list作为priority queue的底层机制,元素插入可享常数时间,取极值需要进行线性扫描。我们也可以在插入前先进行排序,使元素值总是有序的,但这样一来,取极值和删除操作达到最高效率,元素的插入只有线性。

我们也可以以二叉搜索树作为底部实现,这么一来,插入极值都是O(logn),但是一来二叉搜索树需要有足够的随机性,而来二叉搜索树并不容易实现。priority queue的效率最好在二叉搜索树和queue之间,binary heap便是合适的选择。

binary heap就是一种完全二叉树,除底层叶子节点外,都是满的,而最底层的叶子节点从左至右又不能有空隙。完全二叉树没有节点漏洞
的一个极大的好处在于,我们可以用array来存储节点。假设动用一个小技巧,将array的0元素设置为极大或极小值,那么当完全二叉树某个节点位于array的i处,其左子节点必然在2i处,右子节点必然在2i+1处,父节点必位于i/2处。通过这个简单的位置规则,我们可以用array轻易实现完全二叉树,这种以array表示tree的方法,我们称之为隐式表述法。

这样我们用一个array和一组heap算法就可以实现priority queue,array的缺点是无法动态改变大小,heap需要这个功能,所以我们用vector来替代。

heap分为max-heap和min-heap,前者的每个节点的键值大于等于其子节点的键值,后者每个节点的键值小于等于子节点的键值。max-heap最大值在根节点,min-heap最小值在根节点,都位于array或者vector的起头处。STL提供max-heap,今天讨论的也都是max-heap。

heap算法

push_heap算法
为了满足完全二叉树特性,新加入的元素一定放在最底层做叶子节点,并填补从左至右第一个空格,也就是把新元素插入底层vector的end()处。
在这里插入图片描述
新元素是否适合当前位置呢?我们执行一个上溯程序,将新节点与父节点比较,比父节点大,就交换,一直上溯,直到不需要交换或者到根节点为止。

以下为push_heap的实现细节。函数接受两个迭代器作为vector的头尾,并且新元素已经插入到容器的最尾端,如果不符合这两个条件,执行结果未可知。
在这里插入图片描述
pop_heap算法
pop操作取走max-heap的根节点(其实是放在vector的最后一个元素),为了满足完全二叉树的特性,必须将最下一层最右边的叶子节点拿掉,我们的任务是为这个叶子节点找一个合适的位子。

为了满足max-heap特性,我们进行下溯,将根节点拿掉(形成一个洞)填入上述失去生存空间的叶子节点值,在将它拿来和其两个子节点比较键值,并与较大的那个对调位置,如此一直下放,直到洞的值大于左右子节点或者到叶子节点为止。
在这里插入图片描述
pop_heap具体实现。该函数接受两个迭代器作为底层容器的头尾,如果不满足这个条件,结果不可预期。
在这里插入图片描述
pop_heap之后,最大值元素只是被放在了容器的最尾端,并未被移走。如果要取其值,可用back(),如果要移走,可用pop_back()。

sort_heap算法

既然每次pop_heap可获得一个键值最大的值,那么持续pop_heap,每次向前缩减一个元素,程序执行完毕,我们就有一个递增的序列。

下面是sort_heap的实现细节,函数接受两个迭代器作为底层容器的头尾两端,如果不满足这个条件,结果未可知。sort_heap之后的heap不再是一个合法的heap了。

template <class RandomAccessIterator>
void sort_heap(RandomAccessIteraor first, RandomAccessIterator last) {
	while (last - first > 1)
		pop_heap(first, last--);
}

make_heap算法
这个算法就是将一段数据转化为一个heap。
在这里插入图片描述

heap没有迭代器

heap所有元素都必须遵循完全二叉树排列规则,所以heap不支持遍历,也不提供迭代器。

priority queue

priority queue是一个拥有权值观念的queue,它允许加入新元素,移除旧元素,审视元素值等操作。由于这是一个queue,只允许低端插入,顶端获取,除此之外无其他办法存取元素。priority queue并非按照元素推入次序排列,而是按照权值排列,权值最高排在最前面。缺省情况下priority queue利用一个max-heap完成,max-heap可以满足priority queue的依权值高低自动递增排序的特性。

priority queue定义完整列表

priority queue缺省以vector为底部容器,再加上heap处理规则。queue为配接器,priority queue也称为配接器。

template <class T, class Sequence = vector<T>, class Compare = less<typename Sequence::value_type> >
class priority_queue {
public:
	typedef  typename Sequence::value_type value_type;
	typedef typename Sequence::size_type size_type;
	typedef typename Sequence::reference reference;
	typedef typename Sequencd::const_reference const_reference;
protected:
	Sequence c;	//底层容器
	Compare comp;	//元素大小比较标准
public:
	priority_queue() :c() {}
	explicit priority_queue(const Compare& x) :c(), comp(x) {}

	template <class InputIterator>
	priority_queue(InputIterator first, InputIterator last, const Compare& x) : c(first, last), comp(x) { make_heap(c.begin(), c.end(), comp); }
	template <class InputIterator>
	priority_queue(InputIterator first, InputIterator last) : c(first, last) { make_heap(c.begin(), c.end(), comp); }

	//以下完全利用Sequence c的操作,完成stack的操作
	boll empty() const { return c.empty(); }
	size_type size() const { return c.size(); }
	const_reference top() const { return c.front(); }
	void push(const value_type& x) {
		__STL_TRY{
			c.push_back(x);
			push_heap(c.begin(), c.end(), comp);
		}
		__STL_UNWIND(c.clear());
	}
	void pop() {
		__STL_TRY{
			pop_heap(c.begin(), c.end(), comp);
			c.pop_back();
		}
		__STL_UNWIND(c.clear()); 
	}
};

priority queue没有迭代器

priority queue进出都有一定的规则,只有顶端元素才有机会被外界取用,priority queue不提供遍历功能,也不支持迭代器。

slist

STL list是一个双向链表,slist是一个单向链表。slist不在标准规格之内。slist是forward iterator,list是bidirectional iterator,因此slist会受到很多限制,但是单向链表耗用空间更小,操作更快,不失为另一种选择。

slist和list一样,插入、移除,接合操作都不会使原有迭代器失效。根据STL的习惯,插入操作都是将元素插入在指定点之前。作为单向链表,slist没有任何办法可以回头定出前一个位置,因此它必须从头找起。换句话说,slist除了在开头附近,其他位置上采用insert和erase操作都不明智。这就是slist相较list的最大缺点,为此,slist提供insert_after()和erase_after()供选择。

基于同样的效率考虑,slist不提供push_back,只提供push_front。

slist节点

slist节点和迭代器设计,在结构上比list复杂的多,运用继承关系,因此在型别转换上有复杂的表现。

//单向链表的节点基本结构
struct __slist_node_base {
	__slist_node_base* next;
};
//单向链表的节点结构
template <class T>
struct slist_node:public __slist_node_base {
	T data;
};
//全局函数:已经某一节点,插入节点于其后
inline __slist_node_base* __slist_make_link(__slist_node_base* prev_node, __slist_node_base* new_node) {
//令new_node的下一个节点为prev的下一个节点
new_node->next = prev_node->next;
prev_node->next = new_node;	//令prev的下一个节点为new_node
return new_node;
}
//全局函数:单向链表的大小
inline size_t __slist_size(__slist_node_base* node) {
	size_t result = 0;
	for (;node!=0;node=node->next)
		++result;
	return result;
} 

slist迭代器

//单向链表的迭代器基本结构
struct __slist_iterator_base {
	typedef size_t size_type;
	typedef ptrdiff_t difference_type;
	typedef forward_iterator_tag iterator_category; //注意,单向
	__slist_node_base* node;	//指向节点基本结构
	__slist_iterator_base(__slist_node_base* x):node(x) {}
	void incr() {node=node->next;}	//前进一个节点
	bool operator==(const __slist_iterator_base& x) const {
		return node==x.node;
	}
	bool operator!=(const __slist_iterator_base& x) const {
		return node!=x.node;
	}
};
//单向链表的迭代器结构
template <class T, class Ref, class Ptr>
struct __slist_iterator : public __slist_iterator_base {
	typedef __slist_iterator<T, T&, T*>	iterator;
	typedef __slist_iterator<T, const T&, const T*>	const_iterator;
	typedef __slist_iterator<T, Ref, Ptr> self;
	typedef T value_type;
	typedef Ptr pointer;
	typedef Ref reference;
	typedef __slist_node<T> list_node;
	__slist_iterator(list_node* x) : __slist_iterator_base(x) {}
	//调用slist<T>::end()时会造成__slist_iterator(0),于是调用上述函数
	__slist_iterator():__slist_iterator_base(0) {}
	__slist_iterator(const iterator& x):__slist_iterator_base(x.node) {}
	reference operator*() const {return ((list_node*)node)->data;}
	pointer operator->() const {return &(operator*());}
	self operator++() {
		incr();
		return *this;
	}
	self operator++(int) {
		self tmp = *this;
		incr();
		return tmp;
	}
}

注意,比较两个slist迭代器是否相等,由于我们没有重载operator==,所以会调用__slist_iterator_base::operator==,根据其定义,我们知道是由__slist_node_base* node是否相等而定的。

slist的数据结构

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值