前言源码之前,了无秘密。
在 STL 编程中,容器是我们经常会用到的一种数据结构,容器分为序列式容器和关联式容器。
两者的本质区别在于:序列式容器是通过元素在容器中的位置顺序存储和访问元素,而关联容器则是通过键 (key) 存储和读取元素。
本篇着重剖析序列式容器相关背后的知识点。
容器分类前面提到了,根据元素存储方式的不同,容器可分为序列式和关联式,那具体的又有哪些分类呢,这里我画了一张图来看一下。
限于篇幅,这篇文章小贺会来重点讲解一下经常使用到的那些容器,比如 vector,list,deque,以及衍生的栈和队列其背后核心的设计和奥秘,不多 BB, 马上就来分析。
vector写 C++ 的小伙伴们,应该对 vector 都非常熟悉了,vector 基本能够支持任何类型的对象,同时它也是一个可以动态增长的数组,使用起来非常的方便。
但如果我问你,知道它是如何做到动态扩容的吗?哎,是不是一时半会答不上来了,哈哈,没事,我们一起来看看。
vector 基本数据结构
基本上,STL 里面所有的容器的源码都包含至少三个部分:
迭代器,遍历容器的元素,控制容器空间的边界和元素的移动;
构造函数,满足容器的多种初始化;
属性的获取,比如 begin(),end()等;
vector 也不例外,其实看了源码之后就发现,vector 相反是所有容器里面最简单的一种。
template 《class T, class Alloc = alloc》
class vector {public:
// 定义 vector 自身的嵌套型别
typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
// 定义迭代器, 这里就只是一个普通的指针
typedef value_type* iterator;
typedef const value_type* const_iterator;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
。。.
protected:
typedef simple_alloc《value_type, Alloc》 data_allocator; // 设置其空间配置器
iterator start; // 当前使用空间的头
iterator finish; // 当前使用空间的尾
iterator end_of_storage; // 当前可用空间的尾
。。.
};
因为 vector 需要表示用户操作的当前数据的起始地址,结束地址,还需要其真正的最大地址。所以总共需要 3 个迭代器,分别来指向数据的头(start),数据的尾(finish),数组的尾(end_of_storage)。
构造函数
vector 有多个构造函数, 为了满足多种初始化。
我们看到,这里面,初始化满足要么都初始化成功, 要么一个都不初始化并释放掉抛出异常,在 STL 里面,异常机制这块拿捏的死死的呀。
因为 vector 是一种 class template, 所以呢,我们并不需要手动的释放内存, 生命周期结束后就自动调用析构从而释放调用空间,当然我们也可以直接调用析构函数释放内存。
void deallocate() {
if (start)
data_allocator::deallocate(start, end_of_storage - start);
}
// 调用析构函数并释放内存
~vector() {
destroy(start, finish);
deallocate();
}
属性获取
下面的部分就涉及到了位置参数的获取, 比如返回 vector 的开始和结尾,返回最后一个元素,返回当前元素个数,元素容量,是否为空等。
这里需要注意的是因为 end() 返回的是 finish,而 finish 是指向最后一个元素的后一个位置的指针,所以使用 end() 的时候要注意。
public:
// 获取数据的开始以及结束位置的指针。 记住这里返回的是迭代器, 也就是 vector 迭代器就是该类型的指针。
iterator begin() { return start; }
iterator end() { return finish; }
reference front() { return *begin(); } // 获取值
reference back() { return *(end() - 1); }
。。.
size_type size() const { return size_type(end() - begin()); } // 数组元素的个数
size_type max_size() const { return size_type(-1) / sizeof(T); } // 最大能存储的元素个数
size_type capacity() const { return size_type(end_of_storage - begin()); } // 数组的实际大小
bool empty() const { return begin() == end(); }
//判断 vector 是否为空, 并不是比较元素为 0,是直接比较头尾指针。
push 和 pop 操作
vector 的 push 和 pop 操作都只是对尾进行操作, 这里说的尾部是指数据的尾部。当调用 push_back 插入新元素的时候,首先会检查是否有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器 finish。
当如果没有备用空间,就扩充空间(重新配置-移动数据-释放原空间),这里实际是调用了另外一个函数:insert_aux 函数。
在上面这张图里,可以看到,push_back 这个函数里面又判断了一次 finish != end_of_storage 这是因为啥呢?这里的原因是因为 insert_aux 函数可能还被其他函数调用哦。
在下面的 else 分支里面,我们看到了 vector 的动态扩容机制:如果原空间大小为 0 则分配 1 个元素,如果大于 0 则分配原空间两倍的新空间,然后把数据拷贝过去。
pop 元素:从尾端删除一个元素。
public:
//将尾端元素拿掉 并调整大小
void pop_back() {
--finish;//将尾端标记往前移动一个位置 放弃尾端元素
destroy(finish);
}
erase 删除元素
erase 函数清除指定位置的元素, 其重载函数用于清除一个范围内的所有元素。实际实现就是将删除元素后面所有元素往前移动,对于 vector 来说删除元素的操作开销还是很大的,所以说 vector 它不适合频繁的删除操作,毕竟它是一个数组。
//清楚[first, last)中的所有元素
iterator erase(iterator first, iterator last) {
iterator i = copy(last, finish, first);
destroy(i, finish);
finish = finish - (last - first);
return first;
}
//清除指定位置的元素
iterator erase(iterator position) {
if (position + 1 != end())
copy(position + 1, finish, position);//copy 全局函数
}
--finish;
destroy(finish);
return position;
}
void clear() {
erase(begin(), end());
}
我们结合图解来看一下:
清楚范围内的元素,第一步要将 finish 迭代器后面的元素拷贝回去,然后返回拷贝完成的尾部迭代器,最后在删除之前的。
删除指定位置的元素就是实际就是将指定位置后面的所有元素向前移动, 最后析构掉最后一个元素。
insert 插入元素
vector 的插入元素具体来说呢,又分三种情况:
1、如果备用空间足够且插入点的现有元素多于新增元素;
2、如果备用空间足够且插入点的现有元素小于新增元素;
3、如果备用空间不够;
我们一个一个来分析。
插入点之后的现有元素个数 》 新增元素个数
插入点之后的现有元素个数 《= 新增元素个数
如果备用空间不足
这里呢,要注意一个坑,就是所谓的迭代器失效问题。通过图解我们就明白了,所谓的迭代器失效问题是由于元素空间重新配置导致之前的迭代器访问的元素不在了,总结来说有两种情况:
由于插入元素,使得容器元素整体迁移导致存放原容器元素的空间不再有效,从而使得指向原空间的迭代器失效。
由于删除元素,使得某些元素次序发生变化导致原本指向某元素的迭代器不再指向期望指向的元素。
前面提到的一些全局函数,这里在总结一下:
copy(a,b,c):将(a,b)之间的元素拷贝到(c,c-(b-a))位置
uninitialized_copy(first, last, result):具体作用是将 [first,last)内的元素拷贝到 result 从前往后拷贝
copy_backward(first, last, result):将 [first,last)内的元素拷贝到 result 从后往前拷贝
vector 总结
到这里呢,vector 分析的就差不多了,最后提醒需要注意的是:vector 的成员函数都不做边界检查 (at方法会抛异常),使用者要自己确保迭代器和索引值的合法性。
我们来总结一下 vector 的优缺点。
优点
在内存中是分配一块连续的内存空间进行存储,可以像数组一样操作,并且支持动态扩容。
因此元素的随机访问方便,支持下标访问和 vector.at() 操作。
节省空间。
缺点
由于其顺序存储的特性,vector 插入删除操作的时间复杂度是 O(n)。
只能在末端进行 pop 和 push。
当动态长度超过默认分配大小后,要整体重新分配、拷贝和释放空间。
list好了,下面我们来看一下 list,list 是一种双向链表。
list 的设计更加复杂一点,好处是每次插入或删除一个元素,就配置或释放一个元素,list 对于空间的运用有绝对的精准,一点也不浪费。而且对于任何位置的元素插入或删除,list 永远是常数空间。
注意:list 源码里其实分了两个部分,一个部分是 list 结构,另一部分是 list 节点的结构。
那这里不妨思考一下,为什么 list 节点分为了两个部分,而不是在一个结构体里面呢? 也就是说为什么指针变量和数据变量分开定义呢?
如果看了后面的源码就晓得了,这里是为了给迭代器做铺垫,因为迭代器遍历的时候不需要数据成员的,只需要前后指针就可以遍历该 list。
list 的节点结构如下图所示:
list 数据结构-节点
__list_node 用来实现节点,数据结构中就储存前后指针和属性。
template 《class T》 struct __list_node {
// 前后指针
typedef void* void_pointer;
void_pointer next;
void_pointer prev;
// 属性
T data;
};
来瞅一瞅,list 的节点长啥样,因为 list 是一种双向链表,所以基本结构就是下面这个样子:
基本类型
template《class T, class Ref, class Ptr》 struct __list_iterator {
typedef __list_iterator《T, T&, T*》 iterator; // 迭代器
typedef __list_iterator《T, const T&, const T*》 const_iterator;
typedef __list_iterator《T, Ref, Ptr》 self;
// 迭代器是bidirectional_iterator_tag类型
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
。。.
};
构造函数
template《class T, class Ref, class Ptr》 struct __list_iterator {
。。.
// 定义节点指针
typedef __list_node《T》* link_type;
link_type node;
// 构造函数
__list_iterator(link_type x) : node(x) {}
__list_iterator() {}
__list_iterator(const iterator& x) : node(x.node) {}
。。.
};
重载
template《class T, class Ref, class Ptr》 struct __list_iterator {
。。.
// 重载
bool operator==(const self& x) const { return node == x.node; }
bool operator!=(const self& x) const { return node != x.node; }
。。.
// ++和--是直接操作的指针指向next还是prev, 因为list是一个双向链表
self& operator++() {
node = (link_type)((*node).next);
return *this;
}
self operator++(int) {
self tmp = *this;
++*this;
return tmp;
}
self& operator--() {
node = (link_type)((*node).prev);
return *this;
}
self operator--(int) {
self tmp = *this;
--*this;
return tmp;
}
};
list 结构
list 自己定义了嵌套类型满足 traits 编程, list 迭代器是 bidirectional_iterator_tag 类型,并不是一个普通指针。
list 在定义 node 节点时, 定义的不是一个指针,这里要注意。
template 《class T, class Alloc = alloc》
class list {protected:
typedef void* void_pointer;
typedef __list_node《T》 list_node; // 节点 就是前面分析过的
typedef simple_alloc《list_node, Alloc》 list_node_allocator; // 空间配置器public:
// 定义嵌套类型
typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef list_node* link_type;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
protected:
// 定义一个节点, 这里节点并不是一个指针。
link_type node;
public:
// 定义迭代器
typedef __list_iterator《T, T&, T*》 iterator;
typedef __list_iterator《T, const T&, const T*》 const_iterator;
。。.
};
list 构造和析构函数实现
构造函数前期准备:
每个构造函数都会创造一个空的 node 节点,为了保证我们在执行任何操作都不会修改迭代器。
list 默认使用 alloc 作为空间配置器,并根据这个另外定义了一个 list_node_allocator,目的是更加方便以节点大小来配置单元。
template 《class T, class Alloc = alloc》
class list {protected:
typedef void* void_pointer;
typedef __list_node《T》 list_node; // 节点
typedef simple_alloc《list_node, Alloc》 list_node_allocator; // 空间配置器
其中,list_node_allocator(n) 表示配置 n 个节点空间。以下四个函数,分别用来配置,释放,构造,销毁一个节点。
class list {protected:
// 配置一个节点并返回
link_type get_node() { return list_node_allocator::allocate(); }
// 释放一个节点
void put_node(link_type p) { list_node_allocator::deallocate(p); }
// 产生(配置并构造)一个节点带有元素初始值
link_type create_node(const T& x) {
link_type p = get_node();
__STL_TRY {
construct(&p-》data, x);
}
__STL_UNWIND(put_node(p));
return p;
}
//销毁(析构并释放)一个节点
void destroy_node(link_type p) {
destroy(&p-》data);
put_node(p);
}
// 对节点初始化
void empty_initialize() {
node = get_node();
node-》next = node;
node-》prev = node;
}
};
基本属性获取
template 《class T, class Alloc = alloc》
class list {
。。.
public:
iterator begin() { return (link_type)((*node).next); } // 返回指向头的指针
const_iterator begin() const { return (link_type)((*node).next); }
iterator end() { return node; } // 返回最后一个元素的后一个的地址
const_iterator end() const { return node; }
// 这里是为旋转做准备, rbegin返回最后一个地址, rend返回第一个地址。 我们放在配接器里面分析
reverse_iterator rbegin() { return reverse_iterator(end()); }
const_reverse_iterator rbegin() const {
return const_reverse_iterator(end());
}
reverse_iterator rend() { return reverse_iterator(begin()); }
const_reverse_iterator rend() const {
return const_reverse_iterator(begin());
}
// 判断是否为空链表, 这是判断只有一个空node来表示链表为空。
bool empty() const { return node-》next == node; }
// 因为这个链表, 地址并不连续, 所以要自己迭代计算链表的长度。
size_type size() const {
size_type result = 0;
distance(begin(), end(), result);
return result;
}
size_type max_size() const { return size_type(-1); }
// 返回第一个元素的值
reference front() { return *begin(); }
const_reference front() const { return *begin(); }
// 返回最后一个元素的值
reference back() { return *(--end()); }
const_reference back() const { return *(--end()); }
// 交换
void swap(list《T, Alloc》& x) { __STD::swap(node, x.node); }
。。.
};
template 《class T, class Alloc》
inline void swap(list《T, Alloc》& x, list《T, Alloc》& y) {
x.swap(y);
编辑:jq
-
函数
+关注
关注
3文章
4276浏览量
62316 -
迭代器
+关注
关注
0文章
43浏览量
4296
原文标题:2 万字 + 20 图带你手撕 STL 容器源码
文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论