C++STL详解四:顺序容器
注:文中列出的实现代码都是简化版,仅仅实现了基本功能,于STL来说,是萤烛与太阳的差别。STL使用traits对大量的函数偏特化出了各种版本用来提升性能,我所使用的代码都是仿照最泛化的版本提取出的最基本的行为。
前言
在本章中,我将分析顺序容器的底层行为,并且简单的实现vector和deque的迭代器。
通过vector的实现,我想表现的是容器对于内存的操作过程
通过deque的迭代器的实现,我想表现的是容器迭代器对容器行为的影响
注意:所有的容器都定义于命名空间std中。
一、什么是顺序容器
所谓的顺序容器,并不是说他在内存中是顺序存储的,虽然类似于vector等在内存中确实是顺序存储的,但是List或者heap却不是。顺序容器的顺序指的是是访问方式为顺序访问的容器,或者说是没有一个键值与之相关的容器。
STL中定义的基本顺序容器有:
数组(array): 顺序存储、支持下标访问、不可扩增
向量(vector): 顺序存储、支持下标访问、可再尾部扩增
双向队列(deque): 非顺序存储,支持下标访问,可双向扩增
双向链表(list): 非顺序存储,不支持下标访问,可再任意位置扩增
堆(heap): 利用完全二叉树存储,不支持下标访问,可在尾部扩增
至于另外一些常见的如栈(stack)、单向队列(quque)、单向链表(forward_list)、优先队列(priority_queue)等等,都只是基本容器的适配器。
下面我们利用一张图来看看顺序容器:
这里的Array是在C++11之后新加入的容器,而堆(heap)由于是完全二叉树,他的实现有自己的一套方法,我就不再这里列出了。
二、数组(Array)
1.array的用法
array是在C++11之后才加入STL之中的,因为C++本身就有一个原生的数组,array的用法也和它完全相同,但是array可以使用STL的泛型算法,而原生数组因为没有定义迭代器,所以不能融入STL之中。
在C++11之后,我们可以通过以下方法去使用array
#include <array>//包含array的头文件
//创建一个array
//模板参数1是元素的类型,2是元素的个数,3是分配器,默认为std::allocator
array<int, 5> arr1 = {
1,2,3,4,5 };
int arr2[] = {
6,7,8,9,0 };//创建一个原生数组
//输出 1 2 3 4 5
for_each( arr1.begin(), arr1.end(), [&](const int& a) {
cout << a << " "; } );
cout << endl;
for (auto a : arr2) {
cout << a << " "; }//输出6 7 8 9 0
这里由于arr1是一个容器,所以可以使用STL中的算法for_each()去遍历整个数组;而arr2是原生的数组,所以只能使用for循环去遍历。
2.array的型别定义
3.array的函数成员
这里非成员函数中,各种比较操作符(除了==之外)在C++20标准下都被移除了重载,因为C++20标准出现了一个新的运算符<=>,只重载这个运算符就相当于重载了之前移除的运算符。但是在实际的使用中,使用方法和之前版本的相同。
4.底层内存结构和实现方法
和原生的数组相同,array的储存结构也是使用连续的存储。在实现方法上,更是直接使用原生的数组作为底层的容器,只不过外附了一个array的迭代器,使之可以融入STL之中。他的迭代器的类型是随机迭代器。
三、vector
1.vector的用法
vector对比于array来说,他的优点在于可以在尾后进行扩增,也就是说他的元素个数是可变的,用法如下:
// 创建含有整数的 vector
std::vector<int> v = {
7, 5, 16, 8};
// 添加二个整数到 vector
v.push_back(25);
v.push_back(13);
// 迭代并打印 vector 的值
for(int n : v) {
std::cout << n << '\n';
2.vector的型别定义
3.vector的成员函数
4.vector的基本行为
在实现vector的基本功能之前,我们需要先了解vector的行为。
- vector保证在内存中连续的存储
- vector需要保证可以在尾端进行扩增
- vector需要实现原生数组的所有功能
连续存储很容易实现,和原生数组一样,在申请内存的时候一次性申请一大片内存,之后进行分割使用就可以实现。
在尾端进行扩增这个行为就要求我们在进行增加元素的操作时进行判定,若已经超出了已有的内存空间,就另外开辟一个新的空间存储全新的vector,并释放原空间
接着我们来考虑下vector的迭代器:
由于vector在内存中是连续存储,且他的行为和原生数组类似,所以我们没有必要为他而外设计一个迭代器,直接使用原生指针作为vector的迭代器就可以。
5.vector的简单实现
在这个实现中,我是用的分配器是在之前的文章中放出的分配器,有兴趣的可以参考我之前的文章:C++STL详解二:萃取器与分配器
我们首先来看MyVector的定义:
//STL重点vector此处有第二模板参数,代表需要使用的分配器
//默认为std::allocator
//由于我只使用一种分配器,此处就省略了
template<typename T>
class MyVecotr
{
public:
//标准接口声明
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t differemce_type;
typedef value_type* iterator;
protected:
//数据成员的定义
iterator start;//正在使用的头
iterator finish;//正在使用的尾
iterator end_of_storage;//当前空间的尾
}
在栈中,vector只定义了三个迭代器(或者说是指针):
- start:指向有数据的空间的开头
- finish:指向有数据的空间的结尾
- end_of_storage:指向整个可用空间的结尾
为什么这里有两个指向结尾的指针呢?因为vector在申请内存的时候并不是需要多少就申请多少,他往往会额外申请一定的空间。在空间不足需要扩增时,会申请已占用的空间的二倍。
接下来我们看他的构造和析构:
public:
//构造析构函数
MyVecotr() :start(0), finish(0), end_of_storage(0) {
};
MyVecotr(size_type n, const T& value) {
fill_initialize(n, value); }
MyVecotr(int n, const T& value) {
fill_initialize(n, value); }
MyVecotr(long n, const T& value) {
fill_initialize(n, value); }
~MyVecotr() {
destroy(start, finish);//调用析构函数
deallocate();//释放内存
}
在这里我只列出了一种构造函数,也就是填充为N个value,其他构造函数的思想相同,我就不列出了
在构造函数中会调用fill_initialize()进行实际的空间构造,这个函数定义如下:
void fill_initialize(size_type n