STL源码——序列式容器vector★★★

一.vector概述


任何特定的数据结构都是为了实现某种特定的算法.STL容器即是将运用最广的一些数据结构实现出来。

vector的数据安排以及操作方式,与array非常相似。两者的唯一差别在于空间的运用的灵活性。array是静态空间,一旦配置了就不能改变;要换个大(或小)一点的房子,可以,一切琐细得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。vector是动态空间,随着元素的加人,它的内部机制会自行扩充空间以容纳新元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始就要求一个大块头array 了,我们可以安心使用vector,吃多少用多少。vector 的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦vector 旧有空间满载,如果客户端每新增一个元素,vector内部只是扩充–个元素的空间,实为不智,因为所谓扩充空间(不论多大),一如稍早所说,是“配置新空间Ⅰ数据移动Ⅰ释还旧空间”的大工程,时间成本很高,应该加人某种未雨绸缪的考虑。稍后我们便可看到 SGI vector 的空间配置策略。

二、定义摘要


<stl_vector.h>


//alloc是SGI STL的空间配置器
template <class T, class Alloc = alloc>
class vector {
public:
    // vector 的嵌套类型定义
    typedef T value_type;
    typedef value_type* pointer;
    typedef value_type* iterator;// vector 的迭代器是普通指针
    typedef value_type& reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
protected:
    // simple_alloc是SGI STL的空间配置器,见前面空间适配器文章的介绍
    typedef simple_alloc<value_type, Alloc> data_allocator;
    iterator start; // 表示目前使用空间的头
    iterator finish; // 表示目前使用空间的尾
    iterator end_of_storage; // 表示目前可用空间的尾
    void insert_aux(iterator position, const T& x);
    void deallocate() {
        if (start)
        data_allocator::deallocate(start, end_of_storage - start);
    }
    void fill_initialize(size_type n, const T& value) {
            start = allocate_and_fill(n, value);
            finish = start + n;
            end_of_storage = finish;
    }
public:// vector 操作函数的实现
    iterator begin() { return start; }// 返回指向容器第一个元素的迭代器
    iterator end() { return finish; }// 返回指向容器尾端的迭代器
    size_type size() const { return size_type(end() - begin()); }
    size_type capacity() const {
        return size_type(end_of_storage - begin());
    }
    bool empty() const { return begin() == end(); } // 检查容器是否为空
    reference operator[](size_type n) { return *(begin() + n); }//[]操作符重载
    vector() : start(0), finish(0), end_of_storage(0) {}
    vector(size_type n, const T& value) { fill_initialize(n,value); }// 构造拥有 n 个有值 value 的元素的容器
    vector(int n, const T& value) { fill_initialize(n,value); }
    vector(long n, const T&value) { fill_initialize(n,value); }
    explicit vector(size_type n) { fill_initialize(n,T()); }
    ~vector()
        destroy(start, finish); //全局函式,见前面文章destroy函数的介绍
        deallocate(); //这是 vector的一个 member function
    }
    reference front() { return *begin(); } // 第一个元素
    reference back() { return *(end() - 1); } // 最后一个元素
    void push_back(const T& x) { // 将元素安插至最尾端
        if (finish != end_of_storage) {// 有备用空间
            construct(finish, x); //全局函式,// 全局函数,将x 设定到finish 指针所指的空间上,见前面文章construct函数的介绍
            ++finish; // 调整
        }
        else
            insert_aux(end(), x);  无备用空间,从新分配再插入,这是 vector的一个member function
        }
    void pop_back() { // 将最尾端元素取出
        --finish;
        destroy(finish); // 全局函式,见前面文章destroy函数的介绍
    }
    iterator erase(iterator position) { // 清除某位置上的元素
        if (position + 1 != end())
        copy(position + 1, finish, position); // 后续元素往前搬移,全局函数
        --finish;
        destroy(finish); // 全局函式,见前面文章destroy函数的介绍
        return position;
    }
    void resize(size_type new_size, const T& x) {// 改变容器中可存储元素的个数
        if (new_size < size())
            erase(begin() + new_size, end());
        else
            insert(end(), new_size - size(), x);
    }
        void resize(size_type new_size) { resize(new_size, T()); }
        void clear() { erase(begin(), end()); }
protected:
    // 配置空间并填满内容
    iterator allocate_and_fill(size_type n, const T& x) {
        iterator result = data_allocator::allocate(n);
        uninitialized_fill_n(result, n, x); // 全局函式,见前面uninitialized_fill_n函数的介绍
        return result;
}

三、vector的迭代器

vector维护的是一个连续线性空间,所以不论其元素类别是什么,普通指针都可以作为vector的迭代器而满足所有必要条件

vector迭代器支持有操作有(普通指针也具备):operator*、operator->、operator++、operator–、operator+、operator-、operator+=、operator-=vector支持随机存取,而普通指针正有着这样的能力,所以,vector提供的是随机访问迭代器(Random Access iterators)

vector的迭代器定义如下:

template <class T, class Alloc = alloc>
class vector {
public:
    typedef T value_type;
    typedef value_type* iterator; //vector的迭代器是原生指针
    ...
};

可以推断出:

vector<int>::iterator ivite;   //等同于int* ivite;
vector<Shape>::iterator svite; //等同于Shape* svite;

四、vector的数据结构

vector的数据结构非常简单:一个线性连续空间

vector的3个数据结构:
start:表示目前使用空间的头
finish:表示目前使用空间的尾
end_of_storage:表示目前可用空间的尾为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求量更大一些,以备将来可能的扩充

如前文代码所示, 运用start、finish、end_of_storage三个迭代器,vector提供了首尾标示、大小、容量、空容器判断、注标[]运算符、最前端元素值、最后端元素值….等机能,如下:

template <class T, class Alloc = alloc>
class vector {
...
public:// vector 操作函数的实现
    iterator begin() { return start; }// 返回指向容器第一个元素的迭代器
    iterator end() { return finish; }// 返回指向容器尾端的迭代器
    size_type size() const { return size_type(end() - begin()); }
    size_type capacity() 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); } // 最后一个元素
    ...
};

五、vector的构造与内存管理(constructor、push_back)

vector的内存管理:vector默认使用alloc做为空间配置器,并据此另外定义了一个data_allocator,为的是更方便以元素大小为配置单位:

template <class T, class Alloc = alloc>
class vector {
protected:
    // simple_alloc<>见前面文章介绍
    typedef simple_alloc<value_type, Alloc> data_allocator;
    ...
};

于是,data_allocator::allocate(n) 表示配置n个元素空间构造函数:vector提供许多构造函数,其中一个允许我们指定空间大小及初值:

//构造函数,允许指定vector大小n和初值value
vector(size_type n, const T& value) { fill_initialize(n, value); }
// 充填并予初始化
void fill_initialize(size_type n, const T& value) {
    start = allocate_and_fill(n, value);
    finish = start + n;
    end_of_storage = finish;
}
// 配置而后充填
iterator allocate_and_fill(size_type n, const T& x) {
    iterator result = data_allocator::allocate(n); //配置n个元素空间
    uninitialized_fill_n(result, n, x); //全局函式,会根据第1个参数类型特性决定使用算法fill_n()或反复调用construct()来完成任务
    return result;
}

push_back()函数:当我们以push_back() 将新元素安插入于vector尾端时,该函式首先检查是否还有备用空间,如果有就直接在备用空间上建构元素,并调整迭代器finish,使vector变大。如果没有备用空间了,就扩充空间(重新配置、搬移数据、释放原空间)。push_back()原型如下:

void push_back(const T& x) {
    if (finish != end_of_storage) { //还有备用空间
        construct(finish, x); //全局函式
        ++finish; //调整水位高度
    }
    else //已无备用空间
        insert_aux(end(), x); // vector member function,见下
}
template <class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x) {
    if (finish != end_of_storage) { //还有备用空间
        // 在备用空间起始处建构一个元素,并以 vector 最后一个元素值为其初值。
        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 : 1;
        // 以上配置原则:如果原大小为0,则配置 1(个元素大小)
        // 如果原大小不为 0,则配置原大小的两倍,
        // 前半段用来放置原数据,后半段准备用来放置新数据
        iterator new_start = data_allocator::allocate(len); // 实际配置
        iterator new_finish = new_start;
        try {
            // 将原 vector 的内容拷贝到新vector
            new_finish = uninitialized_copy(start, position, new_start);
            // 为新元素设定初值 x
            construct(new_finish, x);
            // 调整水位
            ++new_finish;
            // 将原vector的备用空间中的内容也忠实拷贝过来
            new_finish = uninitialized_copy(position, finish, new_finish);
        }
        catch(...) {
            // "commit or rollback" semantics.
            destroy(new_start, new_finish);
            data_allocator::deallocate(new_start, len);
            throw;
        }
        //析构并释放原vector
        destroy(begin(), end());
        deallocate();
        // 调整迭代器,指向新vector
        vector start = new_start;
        finish = new_finish;
        end_of_storage = new_start + len;
    }
}
如上面代码所示,Linux下的扩容规则是:比较简单,就是将大小扩充为原来的2倍
maxSize = maxSize*2;

六、vector内存重分配策略

vector的内存重分配策略:vector是以数组的形式存储的,当往vector中增加元素时,如果vector的容量不足,那么vector就会进行扩容。
扩容的规则是:并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是申请一块比现在大的新的内存空间(gcc和vc申请规则不同,见下面介绍),然后原来内存中的内容拷贝到新内存中,然后释放原来的内存

重点:在gcc和vc的环境下,vector的扩容规则是不一样的注意(重点): 对vector 的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。这是程序员易犯的一个错误,务需小心,这里会出core

Windows下:vector内存重分配即容量的增长是有规律的,可以通过下面的公式描述:maxSize = maxSize + ((maxSize >> 1) > 1 ? (maxSize >> 1) : 1)
图解:就是由1、2、3、4、6、9、13、19……依次增长。从4之后开始有规则:当前索引处的值等于前一个元素值和前前前元素的值之和

 测试程序如下:

#include <iostream>
using namespace std;
#include <string>
#include <vector>
#include <stdio.h>

int main(void) {
  vector<int> arr;
  for (int i = 0; i < 100; ++i) {
    arr.push_back(i);
    cout << "capacity=" << arr.capacity() << ",size=" << arr.size() << endl;
  }

  return 0;
}

capacity()返回的是当前vector对象缓冲区(后面的对vector维护的内存空间皆称为缓冲区)实际申请的空间大小,而size()返回的是当前对象缓冲区中存储数据的个数,capacity永远是大于等于size的,当size和capacity相等时继续添加数据时vector会扩容。

capacity=1,size=1
capacity=2,size=2
capacity=3,size=3
capacity=4,size=4
capacity=6,size=5
capacity=6,size=6
capacity=9,size=7
capacity=9,size=8
capacity=9,size=9
capacity=13,size=10
capacity=13,size=11
capacity=13,size=12
capacity=13,size=13
capacity=19,size=14
capacity=19,size=15
capacity=19,size=16
capacity=19,size=17
capacity=19,size=18
capacity=19,size=19
capacity=28,size=20
capacity=28,size=21
capacity=28,size=22
capacity=28,size=23
capacity=28,size=24
capacity=28,size=25
capacity=28,size=26
capacity=28,size=27
capacity=28,size=28
capacity=42,size=29
capacity=42,size=30
capacity=42,size=31
capacity=42,size=32
capacity=42,size=33
capacity=42,size=34
capacity=42,size=35
capacity=42,size=36
capacity=42,size=37
capacity=42,size=38
capacity=42,size=39
capacity=42,size=40
capacity=42,size=41
capacity=42,size=42
capacity=63,size=43
capacity=63,size=44
capacity=63,size=45
capacity=63,size=46
capacity=63,size=47
capacity=63,size=48
capacity=63,size=49
capacity=63,size=50
capacity=63,size=51
capacity=63,size=52
capacity=63,size=53
capacity=63,size=54
capacity=63,size=55
capacity=63,size=56
capacity=63,size=57
capacity=63,size=58
capacity=63,size=59
capacity=63,size=60
capacity=63,size=61
capacity=63,size=62
capacity=63,size=63
capacity=94,size=64
capacity=94,size=65
capacity=94,size=66
capacity=94,size=67
capacity=94,size=68
capacity=94,size=69
capacity=94,size=70
capacity=94,size=71
capacity=94,size=72
capacity=94,size=73
capacity=94,size=74
capacity=94,size=75
capacity=94,size=76
capacity=94,size=77
capacity=94,size=78
capacity=94,size=79
capacity=94,size=80
capacity=94,size=81
capacity=94,size=82
capacity=94,size=83
capacity=94,size=84
capacity=94,size=85
capacity=94,size=86
capacity=94,size=87
capacity=94,size=88
capacity=94,size=89
capacity=94,size=90
capacity=94,size=91
capacity=94,size=92
capacity=94,size=93
capacity=94,size=94
capacity=141,size=95
capacity=141,size=96
capacity=141,size=97
capacity=141,size=98
capacity=141,size=99
capacity=141,size=100


数据有点多,提炼下就是这样的:


capacity=1
capacity=2
capacity=3
capacity=4
capacity=6
capacity=9
capacity=13
capacity=19
capacity=28
capacity=42
capacity=63
capacity=94
capacity=141

看出其中的规律没?对,就是每次扩容都是增加当前空间的50%(第一次除外);
9+9/2=13;13+13/2=19;19+19/2=28……
\VC\include\vector中的STL源码是这样扩容的:

if (_Count == 0)//这里进行了判断,但是什么都不做,不知道为什么???????
            ;
        else if (max_size() - size() < _Count)//编译器可以申请的最大容量也装不下,抛出异常_THROW(length_error, "vector<T> too long");
            _Xlen();    // result too long
        else if (_Capacity < size() + _Count)//当前空间不足,需要扩容
            {    // not enough room, reallocate
            _Capacity = max_size() - _Capacity / 2 < _Capacity
                ? 0 : _Capacity + _Capacity / 2;    // try to grow by 50%,扩容50%
            if (_Capacity < size() + _Count)//扩容50%后依然不够容下,则使容量等于当前数据个数加上新增数据个数
                _Capacity = size() + _Count;
            pointer _Newvec = this->_Alval.allocate(_Capacity);//申请新的空间
            pointer _Ptr = _Newvec;
            _TRY_BEGIN
            _Ptr = _Umove(_Myfirst, _VEC_ITER_BASE(_Where),
                _Newvec);    // copy prefix  <span style="white-space:pre">    </span>//拷贝原有数据到新的内存中
            _Ptr = _Ucopy(_First, _Last, _Ptr);    // add new stuff<span style="white-space:pre">    </span>//拷贝新增数据到新的内存的后面
            _Umove(_VEC_ITER_BASE(_Where), _Mylast, _Ptr);    // copy suffix
            _CATCH_ALL
            _Destroy(_Newvec, _Ptr);
            this->_Alval.deallocate(_Newvec, _Capacity);//释放原来申请的内存
            _RERAISE;
            _CATCH_END

就是每次扩容50%。至于删除容器中数据的时候,缓冲区大小并不会改变,仅仅只是清除了其中的数据,只有在析构函数调用的时候vector才会自动释放缓冲区。

可以看出来,VC和GCC的扩容策略是不一样的,一个是2倍,一个1.5倍,为啥不一样呢,其中有什么奥秘吗?我在下一篇文章中发布。

七、vector的元素操作(pop_back、erase、clear、insert)

pop_back:

//将尾端元素拿掉,并调整大小
void pop_back() {
    --finish; //将尾端标记往前移一格,表示将放弃尾端元素
    destroy(finish); // destroy是全局函式
}

erase:

// 清除[first,last)中的所有元素
iterator erase(iterator first, iterator last) {
    iterator i = copy(last, finish, first); //copy是全局函数
    destroy(i, finish); //destroy是全局函式
    finish = finish - (last - first);
    return first;
}

clear:

//清除容器内所有元素
void clear() { erase(begin(), end()); }

insert:


//从position开始,插入n个元素,元素初值为x
template<class T,class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T& x)
{
    if (n != 0) { //当n!= 0才进行以下所有动作
        if (size_type(end_of_storage - finish) >= n){
            //备用空间大于等于“新增元素个数”
            T x_copy = x;
            // 以下计算插入点之后的现有元素个数
            const size_type elems_after = finish - position;
            iterator old_finish = finish;
            if (elems_after > n){
                //“插入点之后的现有元素个数”大于“新增元素个数”
                uninitialized_copy(finish - n, finish, finish);
                finish += n; // 将vector尾端标记后移
                copy_backward(position, old_finish - n, old_finish);
                fill(position, position + n, x_copy); //从插入点开始填入新值
            }
        }
        else {
            //“插入点之后的现有元素个数”小于等于“新增元素个数”
            uninitialized_fill_n(finish, n - elems_after, x_copy);
            finish += n - elems_after;
            uninitialized_copy(position, old_finish, finish);
            finish += elems_after;
            fill(position, old_finish, x_copy);
        }
    }
    else {
        // 备用空间小于“新增元素个数”(那就必须配置额外的内存)
        // 首先决定新长度:旧长度的两倍,或旧长度+新增元素个数
        const size_type old_size = size();
        const size_type len = old_size + max(old_size, n);
        // 以下配置新的vector空间
        iterator new_start = data_allocator::allocate(len);
        iterator new_finish = new_start;
        
        STL_TRY {
            // 以下首先将旧vector的安插点之前的元素复制到新空间
            new_finish = uninitialized_copy(start, position, new_start);
            // 以下再将新增元素(初值皆为n)填入新空间
            new_finish = uninitialized_fill_n(new_finish, n, x);
            // 以下再将旧 vector 的插入点之后的元素复制到新空间
            new_finish = uninitialized_copy(position, finish, new_finish);
        }
# ifdef STL_USE_EXCEPTIONS
        catch(...) {
        // 如有异常发生,实现"commit or rollback" semantics.
        destroy(new_start, new_finish);
        data_allocator::deallocate(new_start, len);
        throw;
        }
# endif /* STL_USE_EXCEPTIONS */
        // 以下清除并释放旧的vector
        destroy(start, finish);
        deallocate();
        // 以下调整水位标记
        start = new_start; finish =
        new_finish; end_of_storage =
        new_start + len;
    }
}

注意,插入完成后,新节点将位于哨兵迭代器(即上面按的position,标示出插入点) 所指之节点的前方——这是STL对于“插入操作”的标准规范。下面的图片展示了insert(position,n,x)的操作备用空间>=新增元素个数的情况:
①备用空间2>=新增元素个数2

②插入点之后的现有元素个数3>新增元素个数2

③插入点之后的现有元素个数2<=新增元素个数3
备用空间>=新增元素个数的情况:例如下面备用空间2<新增元素个数n==3

Q&A

1、vector里底层的finish 和 end_of_storage有什么区别?

在 C++ 的 vector 类中,finishend_of_storage 是两个与 vector 内部存储管理相关的指针,它们有以下区别:

  1. finishfinish 是指向 vector 中当前元素范围的下一个位置的指针。换句话说,finish 指向的是已经存储的元素的尾后位置(即最后一个元素的下一个位置)。当我们向 vector 中添加元素时,会将新元素放入 finish 指向的位置,并将 finish 向后移动一个位置。因此,finish 标记了当前 vector 中有效元素的结束位置。

  2. end_of_storageend_of_storage 是指向 vector 内部已分配内存空间的尾后位置的指针。它标记了 vector 内部存储空间的结束位置。当 vector 中的元素个数达到 end_of_storage 所指向的位置时,如果需要再添加元素,vector 将进行内存重新分配,扩大存储空间。

总结来说,finish 指向已存储元素的尾后位置,而 end_of_storage 指向内部存储空间的尾后位置。finish 可以用于确定当前 vector 中有效元素的范围,而 end_of_storage 则用于判断是否需要进行内存重新分配以容纳更多元素。

2、

参考资料:

1.侯捷、参考csdn相关文章

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值