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