STL的空间配置器主要用于内存的分配、填充、释放等操作,在学习它之前,需要深入一点理解new和delete。由于new和delete分两段操作:内存配置和对象构建(析构),本文亦将按此进行分类,其中第1节简单回顾一下new和delete,第2节主要介绍内存配置;第3节主要介绍对象构建(析构)。
1、new 与delete
1.1、简单介绍
复习一下,平常我们使用的new和delete其实包含两部分操作,如下一段代码1.1所示:
class TestClass{….} TestClass* pTestClass = new TestClass; …… delete p;
代码段1.1
其中的new包括了两部分操作:首先从堆中分配一块空间,该操作由operator new来完成;第二,在申请的地址空间上构造出一个TestClass对象,该操作有placement new来完成。对于delete也是分为两步:先释放对象,然后释放所占用的内存。
另外,需注意delete与delete[]:
对于 delete来说会有这样一个重要的问题:内存中有多少个对象要被删除?答案决定了将有多少个析构函数会被调用。这个问题简单来说就是:要被删除的指针指向的是单个对象呢,还是对象数组?这只有你来告诉delete。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组:
string *stringptr1 = new string;
string *stringptr2 = new string[100];
...
delete stringptr1;// 删除一个对象
delete [] stringptr2;// 删除对象数组
如果你在stringptr1前加了"[]"会怎样呢?答案是:那将是不可预测的;如果你没在stringptr2前没加上"[]"又会怎样呢?答案也是:不可预测。而且对于象int这样的固定类型来说,结果也是不可预测的,即使这样的类型没有析构函数。所以,解决这类问题的规则很简单:如果你调用new时用了[],调用delete时也要用[]。如果调用new时没有用[],那调用delete时也不要用[]。(上述内容摘自《effective C++》)
operator new可以重载,重载之后即可自己负责管理内存,在《内存池学习总结》一文(如想了解该《内存池学习总结》请访问http://houjixin.blog.163.com/)中就介绍了通过重载operator new来自己管理内存,这里不再重复介绍operator new。接下来再复习一下placement new的用法。
1.2、placement new的使用
placement new的常用语法形式为:new(p) 类型T;其含义为在地址为p的空间上构造类型T的对象。下面以代码段1.2为例,说明placement new的用法:
首先定义了两种类型“TestPlacementNew”和“TestPlacementNew_V2”,为了方便举例,在例子中定义的这两个类大小一样。
第二,在main函数中申请了“sizeof(TestPlacementNew)*10”大的空间buf,由于“TestPlacementNew”和“TestPlacementNew_V2”大小一样,因此该空间同样也可以放下10个“TestPlacementNew_V2”的对象。
第三,在空间buf上在for循环里使用placement new产生了10个“TestPlacementNew”对象;其对应语句为:“new (pTestPlacementNew+i)TestPlacementNew(i);”,接下来显示新分配的10个对象的内容。
第四,在“TestPlacementNew”类使用完空间buf后(其使用也就是将空间内10个对象的值显示出来),在同一个地址空间buf上又构造了10个“TestPlacementNew_V2”对象;并把这10个对象的内容显示出来。
第五,释放空间buf。
总结:可以看到placement new主要任务是在指定的内存上构造出对象,它不负责内存申请(其所用内存是提供好的),只负责往已有的内存上填充对象。
//placement new 使用 #include <iostream> using namespace std; //定义类型TestPlacementNew class TestPlacementNew { public: TestPlacementNew(int ivalue) :iVal(ivalue){}; int Get_iVal(){return iVal;} private: int iVal; }; //定义类型TestPlacementNew_V2 class TestPlacementNew_V2 { public: TestPlacementNew_V2(int ivalue) :iVal2(ivalue+10){}; int Get_iVal(){return iVal2;} private: int iVal2; }; void main() { //这里申请一段内存 char* buf = new char[sizeof(TestPlacementNew)*10]; TestPlacementNew* pTestPlacementNew = (TestPlacementNew*)buf; //在申请的buf内存是构建10个TestPlacementNew对象 for (int i=0;i<10;++i) {//这里使用placement new 在地址pTestPlacementNew+i上 //构造类TestPlacementNew的对象 new (pTestPlacementNew+i)TestPlacementNew(i); } for (int j=0;j<10;++j) { cout<<pTestPlacementNew[j].Get_iVal()<<endl; } //在未释放的buf内存上构建10个pTestPlacementNew_V2对象, //注意喽:这里是重复利用了刚才的buf TestPlacementNew_V2* pTestPlacementNew_V2 = (TestPlacementNew_V2*)buf; for (int ii=0;ii<10;++ii) { new (pTestPlacementNew+ii)TestPlacementNew_V2(ii); } for (int jj=0;jj<10;++jj) { cout<<pTestPlacementNew[jj].Get_iVal()<<endl; } delete[] buf; }
代码段1.2
2、内存配置
在STL中,内存的分配(包括申请和释放)共有两种方法:如果需配置的内存空间超过128byte,则直接调用malloc()或者free(),此即所谓第一级配置;如果所申请的内存空间小于128byte,则调用STL的内存池进行内存分配,即是第二级配置。
3、构造与析构
在STL中,内存的配置定义于文件memory.h中,该文件中包含了stl_alloc.h和stl_construct.h这两个文件,其中前者负责内存配置的操作(包括内存申请和释放);后者负责对象内容的构造和析构。
3.1、construct()
在文件stl_construct.h的函数construct()中主要调用placement new来构造对象,其部分代码如代码段3.1所示:
template <typename T1,typename T2> inline void construct(T1* p, const T2& value) { new(p) T1(value); //使用placement new 在地址为p的内存空间上构造对象 }
3.2、destroy()
在使用destroy()将对象的释放过程中,STL充分考虑了执行的效率,其提供了两个版本的destroy()定义形式,一个版本接受一个指针作为参数,通过调用该指针所指对象的析构函数完成对象的析构;另一个版本接受一对指针,destroy()函数将释放这对指针之间的所有对象,该版本的destroy()函数由于需要释放大段空间的对象,在实现时更加注重了效率,下面将介绍其具体做法:
首先,需要了解对象类型的trivial与non-trivial,可以简单这么理解:trivial(英文无关紧要的、不重要的)类型的对象使用的系统默认的构造函数;non-trivial类型自己定义了析构函数;在析构对象时,如果对象的类型是non-trivial,则必须调用其自身的析构函数来完成析构,例如图3.1所示的类A,他共定义了三个成员iIndex,fValue和链表头指针p;在类A的析构函数中必须有链表p的释放的操作,如果在destroy函数释放这种类型的对象时没有调用其析构函数,则链表的存储空间(item1…item n 所占用的内存)就不会被释放;
图3.1
由此可见,凡是non-trivial类型,即该类型自己定义了析构函数,则在该析构函数中必有该类自己需要完成的操作(例如图3.1中的释放链表),在释放这种类型的对象时就必须调用其自己定义的析构函数,否则析构中需要进行的操作就不会被执行;对于trivial类型的对象的destroy在其析构函数中没有做任何自己的操作,因此析构时也无需对其做任何操作(注意:不需要将其所占用的内存内容进行删除,在刚接触编程时总以为内存在释放时系统会删除掉其中的内容,其实没有,它只是将内存标志为可用)。
接下来看下面代码段4所示STL利用何种方法高效释放对象,destroy()函数重载的第一个版本用于接受一个指针并调用该指针所指对象的析构函数完成对象的释放过程,非常简单的过程,无需详述。
//第一个版本的destroy定义,接受一个指针, //通过调用指针所指对象的析构函数完成对象的释放 template <typename T> inline void destroy(T* pointer) { pointer->~T(); } //第二个版本的destroy定义,接受一对地址,释放这一对地址之间的对象 template <typename T> inline void destroy(T start, T end) { __destroy(start,end,valuetype(start)); } //__destroy将根据所释放地址空间内对象的类型调用不同的重载函数完成对象释放 template <typename Iterator,typename T> inline void __destroy(Iterator start, Iterator end, T*) { typedef typename __type_traits<T>::has_trival_destructor trivial_destructor; __destroy_aux(start, end,trivial_destructor()); } //下面这个__destroy_aux函数是对象类型为non_trivial的释放版本, //即挨个调用对象的析构函数进行释放 template <typename Iterator> inline void __destroy_aux(Iterator start, Iterator end, __false_type) { for (;start<end; ++start) { destroy(&*start); } } //下面这个__destroy_aux函数是对象类型为trivial的释放版本,什么都不做 template <typename Iterator> inline void __destroy_aux(Iterator start, Iterator end, __true_type) { }
代码段3.2
destroy()函数的第二个版本主要用于释放一对迭代器之间的对象,在该函数中调用内部函数__destroy对所释放对象的类型进行判断,具体做法为通过traits(如果想了解更多关于traits的内容请可以访问http://blog.csdn.net/hjx_1000/article/details/7595825中的“STL源码学习之迭代器”一文)。获取元素对象的类型是否为trivial,结果有__false_type和__true_type两种,如果类型为__false_type表示对象类型为non-trivial,需调用函数__destroy_aux(Iterator start, Iterator end, __false_type)来挨个调用对象的析构函数完成对象的释放;如果如果类型为__true _type表示对象类型为rivial,需调用函数__destroy_aux(Iterator start, Iterator end, __ true _type)什么都不做,立即返回!!!此时效率果然很高吧?