vector的增长模式

引言

我们都知道vector对象是动态存储的,从这一点看有点像链表,可以动态的增加或减少元素。我们也知道链表中是有指针变量,专门用于存储上一个和下一个元素的地址。正是因为这两个指针的存在,我们才能做到动态的存储数据,即不用像数组那样必须事先申请好空间。链表的缺点就是不能够快速的随机访问其中元素,必须通过指针层层查找。

但是,vector既可以实现动态存储数据,而且支持快速随机访问(用下标或者指针访问元素)。对于能够用下标查找的数据类型,其存储方式必定是连续的,即每个元素紧挨着前一个元素存储。

这样对于vector来说,就会出现一个问题,我在初始定义vector的时候该给其申请多少内存空间?如果申请的很小,那么我来了新的数据改存放在哪里?如果申请的很大,我用不完,那岂不是很浪费内存空间?

当然对于vector这种标准库类型,通常我们只关心如何使用它,而不关心其实如何实现的。不过对于其在存储空间的实现方式还是了解一下比较好。

我们知道容器中元素连续存储,且容器大小是可变的,考虑向vector中添加元素会发生什么?如果没有空间容纳新的元素,容器不可能简单的将其添加到内存的其他位置,因为vector的元素必须连续存储。因此容器必须分配新的空间来保存已用的元素和新的元素。将已有元素从旧位置移动到新空间,然后添加新元素,释放旧空间。如果说我们每添加一个新的元素就执行一次这样的操作,显然性能会慢到我们不可接受。

为了避免上面的代价,标准库采用了可以减少容器空间重新分配的策略。当不得不获取新的内存空间时,vector的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,从而用来保存更多新的元素。这样,就不需要每次添加新的元素都重新分配容器的内存空间了。

在插入新元素时,若遇到已分配容量不足的情况,会自动拓展容量大小,而这个拓展容量的过程为:

  • 开辟另外一块更大的内存空间,该空间大小通常为原空间大小的两倍(理论分析上是1.5最好,但从时间和空间上综合考虑,一般取2);
  • 将原内存空间中的数据拷贝到新开辟的内存空间中;
  • 析构原内存空间的数据,释放原内存空间,并调整各种指针指向新内存空间。

vector类型提供了一些成员函数,允许我们与它的现实中内存分配部分互动。

c.capacity()     不重新分配内存空间的话,c可以保存多少元素

c.reserve(n)     分配至少能容纳n个元素的内存空间

c.shrink_to_fit()  将capacity()减少为size()相同大小,size()为vector已经保存元素个数。

预先保留空间—reserve() 和 resize() 函数。

reserve()是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素,因此当加入新的元素时,需要用push_back()/insert()函数。
resize是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。
再者,两个函数的形式是有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。下面是这两个函数使用例子:

vector<int> myVec;
 
myVec.reserve( 100 );     // 新元素还没有构造,
 
                          // 此时不能用[]访问元素
 
for (int i = 0; i < 100; i++ )
 
...{
 
     myVec.push_back( i ); //新元素这时才构造
 
}
 
myVec.resize( 102 );      // 用元素的默认构造函数构造了两个新的元素
 
myVec[100] = 1;           //直接操作新元素
 
myVec[101] = 2;

空间归还—shrink_to_fit()

如代码

int main()
{
    vector<int> v1;
    for (int i = 1; i < 11; i++)
    {
        v1.push_back(i);
    }
    cout << "capacity = " << v1.capacity() << "    " << "size = " << v1.size() << endl;
    v1.shrink_to_fit();
    cout << v1.capacity() << endl;
 
}

从结果中可以看出,当 内存用不完的时候,可以请求系统归还内存,最后,使得 capacity()==size()

空间释放—swap()

需要注意的是,如果采用vector存储数据,执行clear()并不能释放内存,它只是清空了数据,其实这个问题在《Effective STL》中的“条款17”已经指出了

当vector、string大量插入数据后,即使删除了大量数据(或者全部都删除,即clear) 并没有改变容器的容量(capacity),所以仍然会占用着内存。 为了避免这种情况,我们应该想办法改变容器的容量使之尽可能小的符合当前 数据所需(shrink to fit)

《Effective STL》给出的解决方案是:

vector<type> v;
//.... 这里添加许多元素给v
//.... 这里删除v中的许多元素
vector<type>(v).swap(v);
//此时v的容量已经尽可能的符合其当前包含的元素数量
//对于string则可能像下面这样
string(s).swap(s);

下面是网友的测试代码

#include <iostream>
#include <vector>

using namespace std;

vector <string> v;
char ch;

int main ()
{

    for(int i=0; i<1000000; i++)
        v.push_back("abcdefghijklmn");
    cin >> ch;
    // 此时检查内存情况 占用54M

    v.clear();
    cin >> ch;
    // 此时再次检查, 仍然占用54M

    cout << "Vector 的 容量为" << v.capacity() << endl;
    // 此时容量为 1048576

    vector<string>(v).swap(v);

    cout << "Vector 的 容量为" << v.capacity() << endl;
    // 此时容量为0
    cin >> ch;
    // 检查内存,释放了 10M+ 即为数据内存
    return 0;
}

当然,上面这种方法虽然释放了内存,但是同时也增加了拷贝数据的时间消耗。 不过一般需要重新调整容量的情况都是 vector本身元素较少的情况,所以 时间消耗可以忽略不计。

因此建议以后大家都将调用 clear() 改为 swap() 吧。


参考文献

https://blog.csdn.net/autocyz/article/details/45194001

https://blog.csdn.net/dengheCSDN/article/details/78984842

https://lgcagithub.github.io/2016/08/20/cpp-std-vector-grow/

http://www.drdobbs.com/c-made-easier-how-vectors-grow/184401375

https://www.zhihu.com/question/36538542

http://blog.jobbole.com/37700/

posted @ 2018-11-05 14:04  OVS98  阅读(469)  评论(0编辑  收藏  举报