条款50介绍了为何要重载operator new和operator delete,本条款用来讲述重载operator new和operator delete时所要遵守的规则
一、operator new所要遵守的规则
operator new的一般规则为:
必须拥有正确的返回值 :如果申请成功就返回指针指向于那块内存。如果不足就抛出bad_alloc异常(未设置new-handler的情况下)内存不足时调用new-handling函数(见条款49) :operator new实际上不只一次尝试分配内存,每次失败后调用new-handling函数。这里假设new-handling函数能够做某些事情将内存释放出来必须针应对申请零内存的情况 :C++允许用户申请0byte的内存,这一诡异的需求其实是为了简化语言其他部分避免不谨慎掩盖了正常形式的new——比较接近class的接口
operator new伪代码形式(non-member版本)
相关细节如下:
处理0bytes的情况: 源码中将0bytes改为了1bytes,虽然看起来有些别扭,但是做法简单、可行。毕竟客户端申请0bytes的情况也比较少见set_new_handler()为什么先要设置参数为0:
如果内存分配失败,我们需要调用new_handler()函数或者抛出bad_alloc异常 但是我们不知道当前程序是否有new_handler函数,只有set_new_handler()返回值可以获取,因此我们先获取new_handler,然后再用if判断是否存在,然后是否可以调用这个函数 while循环:
当我们申请内存成功之后: while循环就有终止并返回所申请的内存指针但是如果申请内存失败了: 我们不能立即返回,而是执行while循环。如果当前程序有new_handler函数,那么就执行new_handler函数(我们在条款49介绍了,new_handler可能做得事情有:释放内存、安装另一个new_handler、卸载new_handler、抛出bad_alloc异常(或该异常的派生类),或者直接承认失败而return)。因此我们使用while循环
operator new在继承中的注意事项
对于一个成员函数版本的operator new来说,如果其有派生类,那么派生类的new可能会产生不明确的行为 例如:
class Base {
public:
static void* operator new(std::size_t size)throw(std::bad_alloc);
};
class Derived :public Base { };
int main()
{
Derived* p = new Derived; //调用Base::operator new
return 0;
}
可能会发生错误的原因:
Base中的new(),函数中的相关操作是针对于参数为sizeof(Base)大小而设定的,所以当Base调用new()时没问题 但如果是Derived调用new(),那么调用的是基类中的new(),并且传入的大小为sizeof(Derived),但是该new()不是针对于参数大小为sizeof(Derived)而谁定的 因此我们需要在基类的operator new()函数中对申请进行判断, 代码如下:
下面的代码不要判断参数大小是否为0,因为如果参数为0,那么会调用标准库的operator new,标准库的operator new会对参数为0进行处理
void* Base::operator new(std::size_t size)throw(std::bad_alloc)
{
//如果不是Base调用,那么调用标准库的operator new
if (size != sizeof(Base))
return ::operator new(size);
//...
}
注意:
“非附属(独立式)对象”必须有非0大小(见条款39) 例如上面的Base,其sizeof(Base)不能为,如果其大小为0,那么上面的new函数申请操作将被转交给::operator new中
operator new[]注意事项
operator new[]通常被称为“array new” 对于operator new[]唯一要做的就是: 分配一块未加工内存,因为你无法对array之内尚未存在的元素做任何事情当在继承体系时: 为class设置operator new[]成员函数,你设置的参数是要分配的数组个数,但是这个数目可能不准确,因为基类的operator new[]可能被派生类使用
二、operator delete所要遵守的规则
operator delete唯一要做的事情就是:保证“删除null指针”是正确的。 伪代码如下:
void operator delete(void * rawMemory)throw()
{
if (rawMemory == 0) //如果删除的是null指针,什么都不做
return;
}
成员函数版本的operator delete
成员函数版本的delete函数需要两个参数, 其中第二个参数用来检查删除的大小 其中第二个参数 与成员函数版本的operator new的参数概念是一样的,我们需要用这个大小来比较删除对象的大小。因为其delete可能会被派生类使用, 而派生类的大小与基类的大小不一致,因此需要在delete中进行与new函数一样的判断代码如下:
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
if (rawMemory == 0) //删除空指针,直接返回
return;
//删除的不是Base,可能是其派生类调用
//那么就调用全局delete进行删除
if (size != sizeof(Base)) {
::operator delete(rawMemory);
return;
}
//...执行内存归还
}
一个重要的注意事项(虚析构函数):
如果被删除(delete)的对象属于某个基类的派生类,而该派生类没有定义虚析构函数,那么C++传给operator delete的size_t数值可能不正确 例如,通过基类指向指向于派生类,当delete这个指针时:
正确的情况是:调用派生类的虚析构函数,并向delete函数传入派生类的大小 不正确的情况是:派生类没有定义虚析构函数,delete时传入的是基类的大小
三、总结
operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new_handler。它也应该有能力处理0bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请” operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”