Class中的容器问题

前些天工作中同事突然为我一句: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个指针大小.。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值