前些天工作中同事突然为我一句:class 中vector到底占用多大的空间?它占用的空间会不会随着push_back添加元素而产生越界呢??当时听到这个问题是一头雾水,甚至有种被对方的分析说服。作为一个好奇心极强的程序员,我还是打开了VS探究了这其中的奥秘,现在就以大家最为熟悉的vector为例,讲讲vector到底是如何管理内存和如何在class 中被存储的。
为了解决以上的疑惑,我们先看vector的数据定义和内存分配策略,最后以一个实例来验证我们的猜想。
- vector 的数据结构定义:
template <class T, class Alloc = alloc>
class vector {
...
protected:
iterator start; //表示目前已使用空间的头
iterator finish; //表示目前已使用空间的尾
iterator end_of_storage; //表示目前可用空间的尾(已分配)
...
public:
iterator begin() { return start; }
const_iterator begin() const { return start; }
iterator end() { return finish; }
const_iterator end() const { return finish; }
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());
}
//求取已使用容量
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的数据存储区主要是start,finish和end_of_storage这三个 iterator构成,而一个iterator的大小刚好是一个指针的大小,所以vector 的对象大小就有可能是3个指针了,为例验证自己的想法,写了以下的一个简答的测试程序:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <vector>
#include <list>
using namespace std;
class Myclass
{
public:
vector<int>m_vector;
};
int _tmain(int argc, _TCHAR* argv[])
{
Myclass instance;
int num0 = sizeof(instance); //会是多少呢
instance.m_vector.push_back(1);
instance.m_vector.push_back(2);
int num1 = sizeof(instance);//加入元素后是不是class的对象会改变size 呢?
return 0;
}
- 从上面的结果中可以看到vector 的大小变化,如下图所示:
由此可以知道的是vector的push_back 操作并不改变其大小,但是这也让我有了两个疑问:
1.vector 添加元素操作并没有改变类大小的话,那么添加的元素说明也就不在class的数据区了,那么它放在哪里了呢?
2.通过数据结构知道vector 中只有三个指针,为什么它的size不是12?
首先回答为什么size是16的问题,为此我们可以打开vector查看其中存储的数据:
从std::Vector_val中可以看出vector成员中的确包含上文提到的三个指针,并且从地址上看,它们是连续且属于class的数据区(可以从class对象的首地址看出),只是其中多了一个std::Container_base12成员变量,通过查询其地址发现其与_Myfirst地址是相差8位,所以知道std::Container_base12成员变量也在class中,并且是一个容器指针(所有的STL容器都有),因此也就好理解为什么是16了。
那么vector的元素数据是放在那里的呢,它又是如何实现数据的动态添加和管理的?为此我们可以看看vector的push_back时,计算机到底做了些什么:
void push_back(const T& x) {
if (finish != end_of_storage) { // 还有备用空间
construct(finish, x); // 直接在备用空间中构建元素
++finish; // 调整范围
}
else // 已无备用空间
insert_aux(end(), x);
}
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();//此时的size()等同于capacity()
const size_type len = old_size != 0 ? 2 * old_size : 1;
// 以上配置原则:如果原大小为0,那么则配置1(个元素大小)
// 如果原大小不为0,那么配置原大小的2倍
// 前半段用来放置原数据,后半段用来放置新数据
iterator new_start = data_allocator::allocate(len); // 实际配置
iterator new_finish = new_start;
__STL_TRY {
// 复制
new_finish = uninitialized_copy(start, position, new_start);
construct(new_finish, x);
++new_finish;
// 将安插点的原内容页拷贝过来(该函数也可能被insert(p,x)调用)
new_finish = uninitialized_copy(position, finish, new_finish);
}
catch(...) {
// "commit or rollback"(如果不成功那么一个都不留)
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
// 解构并释放原vector
destroy(begin(), end());
deallocate();
// 调整迭代器,指向新vector
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
有以上代码很容容易看到,vector在添加元素时,依据添加元素的大小动态的在堆上分配内存,并且在其生命周期结束时又释放掉 分配的空间。因此通过
allocate 分配出来的地址空间并不和class中数据区空间(上文为栈)在同一位置,而是在堆中分配的地址。因此也就实现了数据在class中大小不变,只需要保存动态内存首地址存储就可,动态变化空间在堆中被动态分配与管理。
例子:
#include "stdafx.h"
#include <vector>
#include <list>
#include <string>
#include <map>
using namespace std;
class Myclass
{
public:
vector<int>m_vector;
int m_age;
string m_name;
map<int, int>m_map;
};
int _tmain(int argc, _TCHAR* argv[])
{
Myclass instance;
int num0 = sizeof(instance);
instance.m_vector.push_back(1);
instance.m_vector.push_back(2);
int num1 = sizeof(instance);
//instance.m_list.push_back(10);
return 0;
}
通过断点调试,查看Myclass对象instance 中每一个元素的首地址知道:它们在数据区中是连续存储的;同时,通过查看vector中俩个int 类型的地址知,它们显然和class中其他成员变量的地址是不同的,依此知道容器中的元素是另外在堆中分配的,并且连续存储(下图中6fe888和6fe88c可以看出).这样也就不难解释为什么vector中添加元素不会引起class数据区的越界等问题了.
总结:
1.class的大小是其数据区元素的大小。
2.class中的数据成员是连续存储的。
3.容器变量在class中只是保留了三个指针和一个container,其元素的值是在堆中例外分配与管理的。
4.vector的大小是4个指针大小.。