自己动手实现简易STL

之前学习C++看侯老师的书的时候实现了一下STL的基本组件,包括了6个组件,allocator, iterator, container, trait, functor, algorithm的组件,也是终于搞清楚了6个组件之间的相互关联.分享给大家。

STL六个组件

比较难理解的就是萃取器和迭代器还有算法之间的交互。其实就是一个算法库根据你迭代器属于不同类型进行的一个选择,选择相对最优的方法。下面展示一下核心部分。对应于STL的容器,分别是单向,双向,随机访问迭代器。

class input_iterator {};
class forward_iterator :public input_iterator {};
class bidirectional_iterator :public forward_iterator {};
class random_acess_iterator final: public bidirectional_iterator {};

用继承关系来表达这样一个迭代器的从属关系的原因是由于C++的一个机制,是利用重载的机制。我们首先考虑public继承关系是一个 is a 的关系,那么再这个层次的继承关系中,random_acess_iterator 是最为特殊的一个。
所以从特殊性的角度来考虑 (从特殊性来比较): random_acess_iterator > bidirectional_iterator > forward_iterator ,他们都是input_iterator。
C++的重载是有一个机制,如果有多个同时可以匹配的选择最特殊的那个,这个机制仔细想想也OK,既然我可以匹配更特殊的,何必匹配更加泛化的呢。
下面举一个最简单的算法例子就懂了。
计算两个迭代器之间的距离。

迭代器 & 算法

下面这个是调用函数,可以看到他还调用了一个_distance函数来作为辅助函数。

template<class InputIterator>
unsigned int distance(InputIterator first, InputIterator last)
{
	return _distance(first, last, Traits<InputIterator>::iterator_type());
}

辅助函数一:

template<class InputIterator>
unsigned int _distance(InputIterator first, InputIterator last, random_acess_iterator iter){
	return abs::abs(last - first);
}

第一行是为了debug的,后面一行可以看到只需要做一个减法就可以计算first到last的长度了。也就是O(1)的时间。为什么可以用减号,因为所有的随机访问迭代器都重载了 operator -
可以看到第三个参数就是做了这样一个选择。

辅助函数二:

template<class InputIterator>
unsigned int _distance(InputIterator first, InputIterator last, forward_iterator iter){
	unsigned int dis = 0;
	for (; first != last; ++first)
		++dis;
	return dis;
}

对于其他类型来说,我不能和随机访问迭代器一样,因此只能一个一个地加了,所以是O(last - first)的时间复杂度。

这里就体现了为什么需要迭代器,就是为了性能!

Q:为什么我不直接用这种重载的方式,非要加一个间接的调用层啊。
A: 因为容器有很多种,每个容器对应的迭代器实际是对应容器的特化的迭代器。但是这种重载机制需要迭代器的类型。而这个需要的类型是特化的那几种迭代器。
举个例子就是 deque和vector 都是random_acess_iterator.但是相应的算法具体是针对某个iterator。所以加入直接使用而不加这个distance的间接,是没有办法找到。或者你需要向里面一样复杂的写法,而又希望接口简单,隐藏细节,因此采用这种写法。

容器,迭代器部分

对于每个可以整迭代器的容器,都会有一个与之相当于的迭代器的,并且他们的关系是耦合的,也就是你实现一个容器,需要实现一个对应的迭代器才能加入STL的大家庭。当然也不是必须这么做,因为有的数据结构没法设计迭代器。能这么设计的好处就是你可以用标准库的算法。
下面简单列一下大概的结构。

template<class T>
class iterator{
	//用同意的来表示其数据或者容器的特性。
	using iterator_type = ...;
	...
	// 需要的重载,不多不少
	operator ++(){}
	operator ++(int){}
	operator != (const iterator&) {}
	...
};

template<class T, class Alloc = alloc>
class container{
public:
	// 表示其类型,方便萃取器trait来萃取类型。
	using value_type = T;
	using iterator = ...;
	...
	//函数实现 字段等等
	container();
	...
};

这样一说,就只有allocator没有说了,这里推荐看侯捷老师的内存管理,里面讲到了这种简单的实现,以及SGI的二层分配,也讲了一个loki的能够消除内部碎片的分配器。
这里为了简化,只是用了简单的new delete。
注意这里是不能使用 malloc 或者free的 除非你显示地调用对象析构函数。

class easy_allocator{
public:
	template<class T>
	static T* alloc(){
		return new T();
	}
	template<class T>
	static void dealloc(T* p){
		assert(p != nullptr);
		delete p;
	}
};

适配器

这个没有太多可以说的,你可以认为就是接口之间的转换。比如queue stack。举个例子就是queue 我需要先进先出 也就是push_back() & pop_front()两种操作, 因此我可以选择list deque。
那么对于stack来说 我需要push_back() & pop_back() 那么vector list deque都可以作为我的底层实现。你在使用标准库的时候也会看到你可以再模板自己定义一个类型。

stack<int, vector<int>> st;
queue<int, deque<int>> que;

仿函数

functor 就是为了让标准库的容器, 算法使用更加灵活。C++11之后很多接口可以直接用lambda 非常好用。

额外的工作

当时觉得这么写代码(CTRL + C + V)有点没有意思,于是当时想能不能稍微扩充个整个小玩具。于是封装win平台的窗口作为一个类, 实现了一个相当于一个 容器的 一个可视化。(并没有很好的完善,后面完善一下 贴出源代码。) 封装window窗口创建作为一个类 网络上可以查一下,有点点技巧。
使用方式类似与迭代器,因为是直接和容器相关的。类似于这样是直接耦合再一起

using window = window_vector<T>;

能够简单的可视化一些数据结构,也还是稍微有点意思吧。。。还增加的个按钮 可以看到你最近的操作是怎么变化的。

在这里插入图片描述
这个就是一个二叉树的啦。可以看到插入一个元素对他带来的变化。
红黑树 具体的变化:
红黑树就是不像AVL树对于高度那样严格,通过红黑节点的定义(有证明说明红黑树是和2-3-4树本质是一样的)
并且红黑树旋转的一个很重要性质就是: O(1)的旋转进行插入或者删除,对比AVL树是O(lgn)的旋转数。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这个是list的window…
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

而且用这个窗口可以很好的debug,比如树可以直观的看里面的东西对不对,print出来不太直观。
这些实现要注意窗口resize的消息,需要重新绘制一下。没有加入滚动条的功能,有时间再加。

小结

非常推荐去实现一个vector 包括所有的组件,其实也不是那么简单。不需要实现那么多算法 容器等,知道6个组件具体是怎么工作的就够了。
这里简洁一下6个组件如何交互,下篇文章会用最简单的vector举例子 示范一个具体的实现。

了解标准库,可以更好地扩展他。比如怎么使用它底层的分配器,容器等等。根据自己的需求进行选择,甚至可以做必要的扩充。

初学者,欢迎各位指出问题。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值