分类: C/C++
2013-01-07 00:23:09
关于我们为什么要重载new和delete,我想看过我前一基础篇的朋友,应该都知道了。最基本的原因就是我们想控制内存分配的过程。
如果你要问更详细的条条框框,来解释重载new/delete可以做什么的话,我推荐你去读读《Effective C++》条款50。
这里我不想描述这些书本上已经有的东西,而是想记录一些我实战中的经验。
重载new和delete需要注意些什么 a. 我们知道,内存分配在C中是由两个函数实现的,而在C++中是由两个操作符实现的。所以在我们重载new和delete的时候就应该遵守一定的编程规则:从“基础”篇,我们知道,new一个对象包含两个过程,一是分配内存,二是执行对象的constructor。如果constructor执行失败的话(抛出了异常,比如在constructor里面,我们还有一个内存分配,但是没得到),编译器会帮我们调用对应的delete去释放分配得到的内存。什么是对应的delete?就是有相同的signature。
比如:void * operator new (size_t size, Pool *memPool), 它对应的delete就是 void operator delete(void *p, Pool *memPool)。
这里要注意的是,这种情况只发生在new的过程中,在真正的delete时,你可以调用系统默认的delete。所以编译器不会报错,如果你没有实现一个对应的delete,因为在你的代码中,你是用系统提供的delete操作符进行内存回收的。
c. 大部分情况下,我们重载的都是C++默认的,也就是那个抛出异常的new。这里要注意的就是,这个new在G++中,不允许你返回一个NULL(在Visual C++中,你可以)。这意味着什么呢?还记得我们提到过的,编译器会为我们在new后面添加代码来调用对象的constructor吗?对的,G++之所以不允许你返回NULL,是因为这是个抛出异常的new,你只能通过异常来通知用户,出现了错误,如果你返回NULL,编译器添加的代码就会在0地址去执行constructor,这样......
为什么在Visual C++中,我们可以呢?这是因为Visual C++添加的代码中,包含了一个NULL 判断,如果是NULL, 它就跳过constructor,而返回这个NULL 到客户那里。
你可定会问,我们都不用异常,那怎么办?有没有解决方案?别急,当然有,如果没有,那g++不就是太挫了?你只要在你的编译参数中加入 -fcheck-new 就可以了。
从这里你可以看到,虽然C++提供了不抛出异常的new,但是它并不鼓励我们用它。相反,它让抛出异常的new包含了这部分功能。这就是为什么,在现实中,我们很少看到有人用C style的new。
d. 有没有试过对一个delete过的对象,再调用它的虚函数,比如:
猜猜结果........................当然是segment fault :)
为什么?因为delete后,pB的vtable指针已经被设置成NULL了。那么是谁设置的呢?我们的destructor没这个动作,那么肯定是operator delete做的了。
是不是呢?
好的,我们来试试这个类:
再试试....嗯,你可以看见,你成功了,程序正常结束。这就是野指针的危害,也是为什么,我们总是被教导,把delete过的指针设置为NULL。
各位看看还有什么问题。嗯,在多线程程序中,把delete过的指针设置成NULL,并不能解决所有问题。
那我们应该怎么做?
这里有见很好玩的事情,就是你delete一个对象两次,那个对象的destructor并不会被调用两次。第二次调用会在第一次调用结束后也相应的结束了,它什么也不做。
所有这里就提醒我们,在destructor中,把constructor中设置的初始值都reset掉。这样会让我们的程序更加健壮。
你们可以自己写代码试试。这里所有的秘密都是基于我在基础篇写的东西。
e. 你在父类中重载了new和delete,子类中还要做类似的事情吗?
其实这是个函数范围的问题。类也是一个scope(范围),一个继承体系也会继承类的范围,特别是public的函数,包括static函数。
所以,如果子类和父类的new/delete做同样的事情,那么你不需要再写一遍。
累了,今天就写到这里,下一篇,我将给大家介绍一个设计模式,它可以帮助我们为一个memory pool 设计一个接口,以方便用户使用。