Effective C++条款51:定制new和delete——编写new和delete时需固守常规

  • 条款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版本)

  • 下面是operator new的伪代码形式:

  • 相关细节如下:
    • 处理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;

}

  1. 可能会发生错误的原因:
    • Base中的new(),函数中的相关操作是针对于参数为sizeof(Base)大小而设定的,所以当Base调用new()时没问题
    • 但如果是Derived调用new(),那么调用的是基类中的new(),并且传入的大小为sizeof(Derived),但是该new()不是针对于参数大小为sizeof(Derived)而谁定的
  2. 因此我们需要在基类的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专属版本则还应该处理“比正确大小更大的(错误)申请”

猜你喜欢

转载自blog.csdn.net/www_dong/article/details/113856795