在默认状况下,new_handler的行为是抛出一个bad_alloc异常,所以上述循环只会执行一次。但若是咱们不但愿使用默认行为,可 以自定义一个new_handler,并使用std::set_new_handler函数使其生效。在自定义的new_handler中,咱们能够抛出 异常,能够结束程序,也能够运行一些代码使得有可能有内存被空闲出来,从而下一次分配时也许会成功,也能够经过set_new_handler来安装另外一 个可能更有效的new_handler。例如:
这里new_handler程序在抛出异常以前会输出一句话。应该注意,在new_handler的代码里应该注意避免再嵌套有对new的调 用,由于若是这里调用new再失败的话,可能会再致使对new_handler的调用,从而致使无限递归调用。——这是我猜的,并无尝试过。
在编程时咱们应该注意到对new的调用是有可能有异常被抛出的,所以在new的代码周围应该注意保持其事务性,即不能由于调用new失败抛出异常来致使不正确的程序逻辑或数据结构的出现。例如:
在《STL原码剖析》一书中详细分析了SGI STL的内存分配器的行为。与直接使用new operator不一样的是,SGI STL并不依赖C++默认的内存分配方式,而是使用一套自行实现的方案。首先SGI STL将可用内存整块的分配,使之成为当前进程可用的内存,当程序中确实须要分配内存时,先从这些已请求好的大内存块中尝试取得内存,若是失败的话再尝试 整块的分配大内存。这种作法有效的避免了大量内存碎片的出现,提升了内存管理效率。
为了实现这种方式,STL使用了placement new,经过在本身管理的内存空间上使用placement new来构造对象,以达到原有new operator所具备的功能。
此函数接收一个已构造的对象,经过拷贝构造的方式在给定的内存地址p上构造一个新对象,代码中后半截T1(value)即是placement new语法中调用构造函数的写法,若是传入的对象value正是所要求的类型T1,那么这里就至关于调用拷贝构造函数。相似的,因使用了 placement new,编译器不会自动产生调用析构函数的代码,须要手工的实现:
[Page]
template <class T>
inline void destory(T* pointer)
{
pointer->~T();
}
与此同时,STL中还有一个接收两个迭代器的destory版本,可将某容器上指定范围内的对象所有销毁。典型的实现方式就是经过一个循环来对 此范围内的对象逐一调用析构函数。若是所传入的对象是非简单类型,这样作是必要的,但若是传入的是简单类型,或者根本没有必要调用析构函数的自定义类型 (例如只包含数个int成员的结构体),那么再逐一调用析构函数是没有必要的,也浪费了时间。为此,STL使用了一种称为“type traits”的技巧,在编译器就判断出所传入的类型是否须要调用析构函数:
template <class ForwardIterator>
inline void destory(ForwardIterator first, ForwardIterator last)
{
__destory(first, last, value_type(first));
}
其中value_type()用于取出迭代器所指向的对象的类型信息,因而:
template<class ForwardIterator, class T>
inline void __destory(ForwardIterator first, ForwardIterator last, T*)
{
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destory_aux(first, last, trivial_destructor());
}
//若是须要调用析构函数:
template<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
for(; first < last; ++first)
destory(&*first); //因first是迭代器,*first取出其真正内容,而后再用&取地址
}
//若是不须要,就什么也不作:
tempalte<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __true_type)
{}
因上述函数全都是inline的,因此多层的函数调用并不会对性能形成影响,最终编译的结果根据具体的类型就只是一个for循环或者什么都没 有。这里的关键在于__type_traits<T>这个模板类上,它根据不一样的T类型定义出不一样的 has_trivial_destructor的结果,若是T是简单类型,就定义为__true_type类型,不然就定义为__false_type类 型。其中__true_type、__false_type只不过是两个没有任何内容的类,对程序的执行结果没有什么意义,但在编译器看来它对模板如何特 化就具备很是重要的指导意义了,正如上面代码所示的那样。__type_traits<T>也是特化了的一系列模板类:
struct __true_type {};
struct __false_type {};
template <class T>
struct __type_traits
{
public:
typedef __false _type has_trivial_destructor;
……
};
template<>
//模板特化
struct __type_traits<int>
//int的特化版本
{
public:
typedef __true_type has_trivial_destructor;
……
};
…… //其余简单类型的特化版本
若是要把一个自定义的类型MyClass也定义为不调用析构函数,只须要相应的定义__type_traits<T>的一个特化版本便可:
[Page]
template<>
struct __type_traits<MyClass>
{
public:
typedef __true_type has_trivial_destructor;
……
};
模板是比较高级的C++编程技巧,模板特化、模板偏特化就更是技巧性很强的东西,STL中的type_traits充分借助模板特化的功能,实 现了在程序编译期经过编译器来决定为每一处调用使用哪一个特化版本,因而在不增长编程复杂性的前提下大大提升了程序的运行效率。更详细的内容可参考《STL 源码剖析》第2、三章中的相关内容。
带有“[]”的new和delete
咱们常常会经过new来动态建立一个数组,例如:
char* s = new char[100];
……
delete s;
严格的说,上述代码是不正确的,由于咱们在分配内存时使用的是new[],而并非简单的new,但释放内存时却用的是delete。正确的写法是使用delete[]:
delete[] s;
可是,上述错误的代码彷佛也能编译执行,并不会带来什么错误。事实上,new与new[]、delete与delete[]是有区别的,特别是当用来操做复杂类型时。假如针对一个咱们自定义的类MyClass使用new[]:
MyClass* p = new MyClass[10];
上述代码的结果是在堆上分配了10个连续的MyClass实例,而且已经对它们依次调用了构造函数,因而咱们获得了10个可用的对象,这一点与
Java、C#有区别的,Java、C#中这样的结果只是获得了10个null。换句话说,使用这种写法时MyClass必须拥有不带参数的构造函数,不然会发现编译期错误,由于编译器没法调用有参数的构造函数。
当这样构形成功后,咱们能够再将其释放,释放时使用delete[]:
delete[] p;
当咱们对动态分配的数组调用delete[]时,其行为根据所申请的变量类型会有所不一样。若是p指向简单类型,如int、char等,其结果只 不过是这块内存被回收,此时使用delete[]与delete没有区别,但若是p指向的是复杂类型,delete[]会针对动态分配获得的每一个对象调用 析构函数,而后再释放内存。所以,若是咱们对上述分配获得的p指针直接使用delete来回收,虽然编译期不报什么错误(由于编译器根本看不出来这个指针 p是如何分配的),但在运行时(DEBUG状况下)会给出一个Debug assertion failed提示。
到这里,咱们很容易提出一个问题——delete[]是如何知道要为多少个对象调用析构函数的?要回答这个问题,咱们能够首先看一看new[]的重载。
class MyClass
{
int a;
public:
MyClass() { printf(/"ctor//n/"); }
~MyClass() { printf(/"dtor//n/"); }
};
void* operator new[](size_t size)
{
void* p = operator new(size);
printf(/"calling new[] with size=%d address=%p//n/", size, p);
return p;
}
// 主函数
MyClass* mc = new MyClass[3];
printf(/"address of mc=%p//n/", mc);
delete[] mc;
运行此段代码,获得的结果为:(VC2005)
calling new[] with size=
16address=
003A5A58
ctor
ctor
ctor
address of mc=
003A5A5C
dtor
dtor
dtor
虽然对构造函数和析构函数的调用结果都在预料之中,但所申请的内存空间大小以及地址的数值却出现了问题。咱们的类MyClass的大小显然是4个字节,而且申请的数组中有3个元素,那么应该一共申请12个字节才对,但事实上
系统却 为咱们申请了16字节,而且在operator new[]返后咱们获得的内存地址是实际申请获得的内存地址值加4的结果。也就是说,当为复杂类型动态分配数组时,系统自动在最终获得的内存地址前空出了 4个字节,咱们有理由相信这4个字节的内容与动态分配数组的长度有关。经过单步跟踪,很容易发现这4个字节对应的int值为0x00000003,也就是 说记录的是咱们分配的对象的个数。改变一下分配的个数而后再次观察的结果证明了个人想法。因而,咱们也有理由认为new[] operator的行为至关于下面的伪代码:
[Page]
template <class T>
T* New[](int count)
{
int size = sizeof(T) * count + 4;
void* p = T::operator new[](size);
*(int*)p = count;
T* pt = (T*)((int)p + 4);
for(int i = 0; i < count; i++)
new(&pt[i]) T();
return pt;
}
上述示意性的代码省略了异常处理的部分,只是展现当咱们对一个复杂类型使用new[]来动态分配数组时其真正的行为是什么,从中能够看到它分配 了比预期多4个字节的内存并用它来保存对象的个数,而后对于后面每一块空间使用placement new来调用无参构造函数,这也就解释了为何这种状况下类必须有无参构造函数,最后再将首地址返回。相似的,咱们很容易写出相应的delete[]的实 现代码:
template <class T>
void Delete[](T* pt)
{
int count = ((int*)pt)[-1];
for(int i = 0; i < count; i++)
pt[i].~T();
void* p = (void*)((int)pt – 4);
T::operator delete[](p);
}
因而可知,在默认状况下operator new[]与operator new的行为是相同的,operator delete[]与operator delete也是,不一样的是new operator与new[] operator、delete operator与delete[] operator。固然,咱们能够根据不一样的须要来选择重载带有和不带有“[]”的operator new和delete,以知足不一样的具体需求。
把前面类MyClass的代码稍作修改——注释掉析构函数,而后再来看看程序的输出:
calling new[] with size=12 address=003A5A58
ctor
ctor
ctor
address of mc=003A5A58
这一次,new[]老老实实的申请了12个字节的内存,而且申请的结果与new[] operator返回的结果也是相同的,看来,是否在前面添加4个字节,只取决于这个类有没有析构函数,固然,这么说并不确切,正确的说法是这个类是否需 要调用构造函数,由于以下两种状况下虽然这个类没声明析构函数,但仍是多申请了4个字节:一是这个类中拥有须要调用析构函数的成员,二是这个类继承自须要 调用析构函数的类。因而,咱们能够递归的定义“须要调用析构函数的类”为如下三种状况之一:
1 显式的声明了析构函数的
2 拥有须要调用析构函数的类的成员的
3 继承自须要调用析构函数的类的
相似的,动态申请简单类型的数组时,也不会多申请4个字节。因而在这两种状况下,释放内存时使用delete或delete[]均可以,但为养成良好的习惯,咱们仍是应该注意只要是动态分配的数组,释放时就使用delete[]。
释放内存时如何知道长度
但这同时又带来了新问题,既然申请无需调用析构函数的类或简单类型的数组时并无记录个数信息,那么operator delete,或更直接的说free()是如何来回收这块内存的呢?这就要研究malloc()返回的内存的结构了。与new[]相似的是,实际上在 malloc()申请内存时也多申请了数个字节的内容,只不过这与所申请的变量的类型没有任何关系,咱们从调用malloc时所传入的参数也能够理解这一 点——它只接收了要申请的内存的长度,并不关系这块内存用来保存什么类型。下面运行这样一段代码作个实验:
[Page]
char *p = 0;
for(int i = 0; i < 40; i += 4)
{
char* s = new char[i];
printf(/"alloc %2d bytes, address=%p distance=%d//n/", i, s, s - p);
p = s;
}
咱们直接来看VC2005下Release版本的运行结果,DEBUG版因包含了较多的调试信息,这里就不分析了:
alloc 0 bytes, address=003A36F0 distance=3815152
alloc 4 bytes, address=003A3700 distance=16
alloc 8 bytes, address=003A3710 distance=16
alloc 12 bytes, address=003A3720 distance=16
alloc 16 bytes, address=003A3738 distance=24
alloc 20 bytes, address=003A84C0 distance=19848
alloc 24 bytes, address=003A84E0 distance=32
alloc 28 bytes, address=003A8500 distance=32
alloc 32 bytes, address=003A8528 distance=40
alloc 36 bytes, address=003A8550 distance=40
每一次分配的字节数都比上一次多4,distance值记录着与上一次分配的差值,第一个差值没有实际意义,中间有一个较大的差值,多是这块 内存已经被分配了,因而也忽略它。结果中最小的差值为16字节,直到咱们申请16字节时,这个差值变成了24,后面也有相似的规律,那么咱们能够认为申请 所得的内存结构是以下这样的:
从图中不难看出,当咱们要分配一段内存时,所得的内存地址和上一次的尾地址至少要相距8个字节(在DEBUG版中还要更多),那么咱们能够猜测,这8个字节中应该记录着与这段所分配的内存有关的信息。观察这8个节内的内容,获得结果以下:
图中右边为每次分配所得的地址以前8个字节的内容的16进制表示,从图中红线所表示能够看到,这8个字节中的第一个字节乘以8即获得相临两次分 配时的距离,通过试验一次性分配更大的长度可知,第二个字节也是这个意义,而且表明高8位,也就说前面空的这8个字节中的前两个字节记录了一次分配内存的 长度信息,后面的六个字节可能与空闲内存链表的信息有关,在翻译内存时用来提供必要的信息。这就解答了前面提出的问题,原来C/C++在分配内存时已经记 录了足够充分的信息用于回收内存,只不过咱们日常不关心它罢了。