STL之vector/list
只有知道了各个容器内部是怎么实现的,才能更好的用这个容器。怎么使用这里就不单独介绍了,做题的部分有使用的介绍,这里主要记录下内部实现。
vector
介绍
- 可以动态改变容器本身的大小,分配器会处理内存上的事情。
- 拥有一段连续的内存空间,可以通过下标来随机访问容器中任何元素,时间复杂度是O(1),但是由于是连续的内存空间,所以插入和删除一个元素的时候都要对其他的元素进行移动,并且它没有办法原地扩充,扩充的话只有在内存中重新找到一块能够扩充的地方,这会造成内存块的拷贝而且扩容 ,时间复杂度是O(n),当然在尾部插入或者删除元素还是很方便的。另外还有迭代器失效的问题。也是因为可以通过下标来访问元素,因此有些时候也可以充当哈希的作用,来快速找到一个元素。
源码
这是G2.9版本,G4.9版本源码已经有了大的改变.
template<class T,class Alloc=alloc>
class vector{
public:
typedef T value_type;
typedef value_type*iterator;
typedef value_type& reference;
typedef size_t size_type;
protected:
iterator start;//vector对象本身就是这些数据(指针)的大小
iterator finish;
iterator end_of_storage;
public:
iterator begin(){return start;}
iterator end(){return finish;}
size_type size()const{
return size_type(end()-begin());
}
size_type capactity()const{
return size_type(end_of_storage-begin());
}
bool empty()const{return begin()==end();}
reference operator[](size_type n){
return *(begin()+n);
}
reference front(){
return *begin();
}
reference back(){
return *(end()-1);
}
...
}
//关于扩容的问题,一定是发生在放入元素进去的时候,以push_back为例子
void push_back(const T&x){
if(finish!=end_of_storage){//尚有备用 空间
construct(finish,x);
++finish;//放入元素,调整高度
}
else insert_aux(end(),x);//没有备用空间了
}
template<class T,class Alloc>
void vector<T,Alloc>::insert_aux(iterator position,const T&x){
if(finish!=end_of_storage){
construct(finish,*(finish-1));
++finish;
T x_copy=x;
copy_backward(position,finish-2,finish-1);
*position =x_copy;
}
else{//已无可用空间
const size_type old_size=size();
const size_type len=old_size!=0?2*old_size:l;
//分配原则:如果原大小为0,则分配1个元素大小
//如果原大小不为0,则分配原大小的两倍
//前半段用来放置原数据,后半段用来放置新数据
iterator new_start =data_allocator::allocate(len);
//之后开始寻找一个2倍的空间,将原来的元素都拷贝到这个两倍空间中,再把新的元素放入到这个两倍空间中
iterator new_finish=new_start;
try{
//将原本vector的内容拷贝到新的vector
new_finish=uninitialized_copy(start,position,new_start);
construct(new_finish,x);//为新元素设置初值x
++new_finish;//拷贝
new_finish=uninitialized_copy(position,finish,new_finish);
}
catch(...){
destroy(new_start,new_finish);
data_alloctor::deallocate(new_start,len);
throw;
}
//释放原vector
destroy(begin(),end());
deallocate();
//调整迭代器,指向新vector
start=new_start;
finish=new_finish;
end_of_storage=new_start+len;
}
}
迭代器设置
//连续空间中,是个指针就行
vector<int>vec;
vector<int>::iterator ite=vec.begin();
几个问题:
- 扩容机制:VS中1.5倍,linuxGCC是2倍扩容,可能的原因:参考:vector为什么以1.5或者2倍扩容
- 插入元素:会先判断当前容器的容量是否还够,够用的话直接分配一个元素内存,不够的话会以1.5或者2倍的方式来扩充容器内存,在内存中找到这样一个倍增后的空间,然后将原本的元素拷贝进去,再将这个元素插入,接着释放原来的内存。
- 迭代器失效:对vector的任何操作,只要引起了内存空间的重新配置,迭代器都会失效。
list
介绍
占用了非连续的内存空间,众多结点组成链表,每个结点中的信息有数据和指针,所以计算链表的内存的时候不仅要计算数据还要计算指针本身所占用的内存。链表双向链表,相对于单向链表,指针本身占用了更多的内存资源。另外,访问链表中的元素因为其没有下标可以直接找到想访问的元素位置,必须要遍历整个链表,所以是O(n)的时间复杂度,但是插入和删除结点,只是对单个结点操作,然后指针改变指向,所以是O(1)的时间复杂度。
下面参考侯捷老师的讲解,从源码开始。
源码
G2.9版本,新的版本会有变化,可以参考实际的库。参照源码不好理解的话建议去看看侯捷老师的视屏,讲的真是太好了。
//节点结构。G2.9版本
template<class T>
struct _list_node{//一个节点包含两个指针和数据,相应内存也包含这些
typedef void* void_pointer;//还要进行类型转换,后面有改善
void_pointer prev;//指针和数据元素本身都占据内存
void_pointer next;
T data;
}
//链表结构
template<class T,class Alloc=alloc>//分配器alloc
class list{
protected:
typedef _list_node<T>list_node;
public:
typedef list_node*link_type;
typedef _list_iterator<T,T&,T*>iterator;
protected:
link_type node;
}
//迭代器结构,希望它是智能的,必须设计成class,而且肯定有大量的操作符重载
template<class T,class Ref,class Ptr>
struct _list_iterator{
typedef _list_iterator<T,Ref,Ptr>self;
..
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
..
link_type node;
//提取值
reference operator*()const{
return (*node).data;
}
pointer operator->()const {
return &(operator*());
}
//++和--类似
//前++,++iter
self&operator++(){//这里的iter++就会去找下一个结点
node=(link_type)((*node).next));//
return *this;
}
//后++,iter++
self operator++(int){
self tmp=*this;//记录原值
++*this;//操作
return tmp;//返回原值
}
...
}
//使用
参照侯捷老师的PPT
几个问题:
- 每个结点的内存占用:数据和指针都会占用内存
- 迭代器:模拟指针的动作,但是不能直接++;我们希望iter++之后会指向下一个结点,而直接进行指针的++不知道它指向哪了(非连续内存),所以iter要足够聪明,当iter++的时候会进去看结点的next指针,然后指向下一个结点,
- 主要两个关键点:提取值和iter。
- 迭代器失效:专门来一篇记录下迭代器失效问题。
参考:侯捷老师
书本:<STL源码刨析>