第四讲:分配器及容器

一、分配器allocators(STL六大部件之一,幕后英雄)

先谈谈:

  1. operator new()
  2. malloc() :memory allocation,分配内存
    可能大家只听说过new,没听过operator new,但是到最后都会归纳到这。
    小结:不同编译器的标准库都是用malloc和free来实现分配器,vc、bc、Gc都一样。
    malloc会带来额外开销,
    alloc实现为有一个16个的空间,每个空间从左到右为8byte成倍数的单向链表,在给容器分配内存的时候,alloc会根据容器需求的内存选择一个空间中的链表给他,例如:vector需要50byte,那么就会分配一个56byte的空间给他,如果container需求的内存太大,alloc中没有适合的,那么就会去用malloc来分配内存,但这样会带来额外的开销,所以alloc的作用是尽量减少额外的开销。

二、容器
前面介绍了分配器,分配器就是为容器服务的。
1、深度探索list
最简单的是array和vector,list是最具代表性的。
Q1:一个list需要什么东西去控制双向和单向?
A1:一个指针4byte,
看源码的时候,有许多type重命名(typedef),麻烦诺。
结点本身:

template <typename T>
struct _list_node{
	typedef _list_node<T>* pointer;
	pointer prev;
	pointer next;
	T data;
};

因为链表是非连续的内存空间,所以iterator不是指针,当iterator++时,迭代器需要够聪明去看下个指针地址。所以iterator是class。
注:C++不允许后++加两次
iterator的++就是将一个类其中有指针指向当先的节点,通过++使得这个类中指针访问等于结点中的next,也就是写一个结点,从而达到目的。
list.begin()是第一个节点的指针
list.end()是最后一个结点的指针
Q2:为什么list的内存大小是8byte(注意这里只包含指针,不包含保存的数据)
A2:原因是两个指针,prev和next指针,32位计算机的地址大小为4byte,所以list所占内存大小为8byte。

补充:Iterator需要遵循的原则
traits表示特征,萃取出iterator的特性。
有的iterator只能++也能–,有的只能++,这些就叫做分类。
算法提出问题,问iterator,iterator需要回答问题。
算法问:

template<typename T>
inline void
algorithm(I first,I last)
{
	I::iterator_category;
	T::pointer
	vI::....
	};

iterator回答:

template<classT,class Ref,class Ptr>
struct	_list_iterator
{
	typedef bidirectional_iterator_tag
	typedef T value_type;
	typedef .....;
};

注:如果iterator不是class时,例如iterator是指针呢,其实指针也是一种类,只不过是一种泛化的类,那么算法问问题iterator就回答不了,那么就要设计一个traits
所以iterator traits用以分离class iterators 和non-class iterators
解决计算机问题的尚方宝剑:加一个中介层
小结:算法问traits,然后trait然后转问iterator,所以trait作为中间层。typename iterator_traits"" value_type v1;
trait就是算法和iterator沟通的媒介。萃取机
各种traits:type traits、iterator traits、char traits、allocator traits、pointer traits、array traits

二、vector(前闭后开区间,有点像向量的方向)
vector在实现上有点像一个数组,但是在向量数组空间用完之后,如果要扩充需要到内存去找,然后再进行数据搬迁,不能在原来基础上扩充,这是就引入vector这个向量。
vector扩充方式,capacity->reallocation->两倍成长的capacity
用 start、finish、end_of_storage这三个指针进行实现,所以容量为12byte.
补充:注意到,所有的容器,只要是带有连续空间的都有[],进行访问
中括号重载

reference operator[](size_type n)
{
	reutrn *(begin() + n);
}

三、array
比起vector更简单
Q1:为什么要包装成容器来用?
A1:需要进行标准库包装,可以享受到算法以及内部结构的交互。
注意:struct与class是等同低位的,处理一些小的差别。
array是不能扩充的。
注意:只要是连续空间,迭代器就可以用简单的指针来表示,不需要再去设计类。

四、deque、queue、stack深度探索
这些容器复杂一些,这些容器是非常好用,非常有用的。
deque:
Q1:deque怎么扩充呢,双端?
A1:分段buffer(缓冲区),分段连续,并不是真正的连续,所以需要有人把它串接起来,扩充只要放一份buffer即可,用指针指向新的缓冲区。
迭代器(iterator):cur、first、last、node,所以这个iterator是一个类,包含四个指针的类,不是简单的指针
边界靠first和last,如果到达边界,转到下一个缓冲区(buffer)则通过node,注意,在buffer中,只有cur在动。
补充:几乎所有的容器都提供两个迭代器(即begin和end函数)
虽然双向队列本身是分段的buffer,但是为了制造出是连续的假象,需要用iterator来实现。
insert()方法插入元素,为了插入的更快,会往更短的队列的一端推。
Q2:deque如何模拟连续空间?
A2:全都是deque iterators的功劳
通过++与–的重载,让用户感受到deque的内存是连续的。
deque的大小:其父类中有_Tp*、size_t、iterator(start)、iterator(finish)
两个指针8byte,两个迭代器(这是个类,包含四个指针first、cur、last、node一共16byte),所以一个queue一共8+16*2 = 40byte.
小结:queue中的iterator是一个类,这个类中重载了很对操作符,从而能够更加方便的使用queue,从而更加方便的能用到算法中去,这里是一个实例,说明iterator不仅仅是一个简单的指针,也会是一个重载了各种操作符的类。
queue:就是完全调用deque,只不过省掉了一些操作,所以这个数据类型一般叫做适配器。
stack:也是通过的确实现的,封住了deque的一端,并且修改一下操作,

stack和queue都可以选择list或deque作为底层结构。
注:stack和queue都不允许遍历,所以都没有iterator。

stack可以选择vector做底层结构,但是queue不能,因为pop操作不能通过。
stack和queue都不可选择set或map做底层结构。

五、rb_tree(红黑树)
关联容器:也就是通过key 去找value
红黑树与散列表(hashtable)这些是比较底层的东西,一般应用的时候都是上层的东西,但是实际上实现的都是底层。
Q1:红黑树与一般的二分树有什么样的区别?
A1:红黑树是一种平衡二元搜索树(balanced binary search tree)。在树中,我们最担忧的是有一条树干很长,而有的又很短,在搜寻的时候倘若搜索到了长的那个就会很慢,所以我们当然希望有一种树能自动平衡,所以红黑树就是这样的一种树,有利于将来的查找。
特点:排列规则有利于search和insert,并保持高度平衡-没有任何点过深。
rb_tree提供“遍历”操作及iterator。
这个容器也有begin(),这个数据结构的算法是前序遍历。
rb_tree不应该用iterator来改变元素值(因为会破坏排列规则),但是编程层面并非阻绝此事,rb_tree视为set和map服务的(作为底层支持),而map允许元素的data被改变,只有元素的key才是不可被改变的。
rb_tree提供两种insertion操作:insert_unique(结点的key一定在整个tree中独一无二,否则安插失败)和insert_equal(结点的key可重复)

template <class key,
		class value,//分为key和data合成value
		class KeyIfValue,
		class Compare,//告诉红黑树怎么比大小
		class Alloc = alloc>
class rb_tree{
protected:
	typedef _rb_tree_node<Value> rb_tree_node;
	...
public:
	typedef rb_tree_node* link__type;
	...
protected:
	size_type node_count;//rb_tree的大小(结点数量)
	link_type header;//本身是一个指针
	Compare key_compare;//key大小比较准则,即为function
};

例子:

rb_tree<int,
		int,
		identity<int>,//怎么从value中取出key
		less<int>,
		alloc>
mytree;

注:计算容器大小是容器中保存的指针总共分配的内存,而不是计算用于存储数据分配的内存。
仿函数通过模板类来编写,要重写(),并且在后面要传入参数列表即:const T& operator()(const T& a,const T& b ){…}
set和map中都有一个东西:那就是rb_tree

六:set、multiset
set、multiset以rb_tree为底层结构,因此有元素自动排序特性,依据是key,而且value和key合一
set/multiset提供遍历操作及iterators.
set/multiset无法通过iterator改变元素值,因为value和key是一样的,改变了value,则key就也改变了,这是不允许的,会破坏结构。
set的所有操作,都转到呼叫底层t(这个t是rb_tree)的操作,从这层意义上,set是一个container adapter。

七、map、multimap
map、multimap以rb_tree为底层结构,因此有元素自动排序特性,依据是key,
map/multimap提供遍历操作及iterators(遍历就需要迭代器).
禁止改key,可以改data,
map元素必须独一无二,因此insert()可以用rb_tree的insert_unique()
multimap元素的key可以重复,因此insert()可以用rb_tree的insert_equal()

map<int,string> imap;//用int做key,用string做data

八、hashtable深度探索
散列表,hashtable在计算机领域是很经典的,但比红黑树更简单,
并没有太多的数学在里面,主要是经验值。
如果我有一大堆东西要放在一个容器里面,每个东西映射为一个号码,将每一个东西放到对应编号的位置,但是当位置有限(利用取模的方法去转化为对应的篮子编号),有可能出现不同东西放到同一位置,为了解决这个问题,在每一个位置建立一个链表,当不同东西在同一位置时,就加入到该位置的链表中。但是,链表有可能很长,未解决这个问题,也就是将长的链表打散(也就是为什么称之为散列表),当一个链表长度大于篮子的数量时,就要将篮子增加两倍。篮子增加后就可以重新打散hashtable。(成长都是痛苦的)
hashtable是隐藏在背后的幕后英雄,
篮子是vector,链表是篮子里的东西
C的字符串为char*
C++字符串std::string

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值