条款10(上): 如果写了operator new就要同时写operator delete
本条款为C++内存管理的大头,我现在还有很多不理解的,一定要回头来再看
让我们回过头去看看这样一个基本问题:为什么有必要写自己的operatornew和operator delete?
是啊,我正在想呢,我为什么要重写呢,我也有些项目经验了,但从来就没用到过呀- -
答案通常是:为了效率。缺省的operator new和operator delete具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此。
哦。。soga。。小对象动态分配内存(手机游戏开发也许能用得到)还是继续看吧
例如有这样一个表示飞机的类:类airplane只包含一个指针,它指向的是飞机对象的实际描述(此技术在条款34进行说明):
class airplanerep { ... }; //表示一个飞机对象
//
class airplane {
public:
...
private:
airplanerep*rep; // 指向实际描述
};
一个airplane对象并不大,它只包含一个指针(正如条款14和m24所说明的,如果airplane类声明了虚函数,会隐式包含第二个指针)。但当调用operator new来分配一个airplane对象时,得到的内存可能要比存储这个指针(或一对指针)所需要的要多。之所以会产生这种看起来很奇怪的行为,在于operator new和operator delete之间需要互相传递信息。
传递信息,怎么传的啊。。
因为缺省版本的operator new是一种通用型的内存分配器,它必须可以分配任意大小的内存块。同样,operator delete也要可以释放任意大小的内存块。operatordelete想弄清它要释放的内存有多大,就必须知道当初operator new分配的内存有多大。有一种常用的方法可以让operator new来告诉operator delete当初分配的内存大小是多少,就是在它所返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。也就是说,当你写了下面的语句,
airplane *pa = new airplane;
你不会得到一块看起来象这样的内存块:
pa——> airplane对象的内存
而是得到象这样的内存块:
pa——> 内存块大小数据+ airplane对象的内存
对于象airplane这样很小的对象来说,这些额外的数据信息会使得动态分配对象时所需要的的内存的大小翻番(特别是类里没有虚拟函数的时候)。
突然对内存管理感兴趣了。。。
如果软件运行在一个内存很宝贵的环境中,就承受不起这种奢侈的内存分配方案了。为airplane类专门写一个operator new,就可以利用每个airplane的大小都相等的特点,不必在每个分配的内存块上加上附带信息了。
还是一头雾水
具体来说,有这样一个方法来实现你的自定义的operator new:先让缺省operator new分配一些大块的原始内存,每块的大小都足以容纳很多个airplane对象。airplane对象的内存块就取自这些大的内存块。当前没被使用的内存块被组织成链表——称为自由链表——以备未来airplane使用。听起来好象每个对象都要承担一个next域的开销(用于支持链表),但不会:rep域的空间也被用来存储next指针(因为只是作为airplane对象来使用的内存块才需要rep指针;同样,只有没作为airplane对象使用的内存块才需要next指针),这可以用union来实现。
具体实现时,就要修改airplane的定义,从而支持自定义的内存管理。可以这么做:
class airplane{ // 修改后的类 — 支持自定义的内存管理
public: //
static void * operator new(size_t size);
...
private:
union {
airplanerep *rep; // 用于被使用的对象
airplane *next; //用于没被使用的(在自由链表中)对象
};
// 类的常量,指定一个大的内存块中放多少个
// airplane对象,在后面初始化
static const int block_size;
static airplane *headoffreelist;
};
union就像一个结构体嘛。。。
上面的代码增加了的几个声明:一个operator new函数,一个联合(使得rep和next域占用同样的空间),一个常量(指定大内存块的大小),一个静态指针(跟踪自由链表的表头)。表头指针声明为静态成员很重要,因为整个类只有一个自由链表,而不是每个airplane对象都有。
下面该写operator new函数了:
void * airplane::operator new(size_t size)
{
// 把“错误”大小的请求转给::operator new()处理;
// 详见条款8
if (size != sizeof(airplane))
return ::operator new(size);
airplane *p= // p指向自由链表的表头
headoffreelist; //
// p 若合法,则将表头移动到它的下一个元素
//
if (p)
headoffreelist = p->next;
else {
// 自由链表为空,则分配一个大的内存块,
// 可以容纳block_size个airplane对象
airplane *newblock =
static_cast<airplane*>(::operatornew(block_size *
sizeof(airplane)));
// 将每个小内存块链接起来形成一个新的自由链表
// 跳过第0个元素,因为它要被返回给operator new的调用者
//
for (int i = 1; i < block_size-1; ++i)
newblock[i].next = &newblock[i+1];
// 用空指针结束链表
newblock[block_size-1].next = 0;
// p 设为表的头部,headoffreelist指向的
// 内存块紧跟其后
p = newblock;
headoffreelist = &newblock[1];
}
return p;
}
链表的基本操作,有空的话一定要实践一下!!
有了operator new,下面要做的就是给出airplane的静态数据成员的定义:
airplane*airplane::headoffreelist;
const int airplane::block_size = 512;
没必要显式地将headoffreelist设置为空指针,因为静态成员的初始值都被缺省设为0。block_size决定了要从::operatornew获得多大的内存块。
这个版本的operator new将会工作得非常好。它为airplane对象分配的内存要比缺省operator new更少,而且运行得更快,可能会快2次方的等级。这没什么奇怪的,通用型的缺省operator new必须应付各种大小的内存请求,还要处理内部外部的碎片;而你的operatornew只用操作链表中的一对指针。抛弃灵活性往往可以很容易地换来速度。
实践出真知啊,自己写一下试试吧,呵呵