重载新建和删除运算符
注意:这只处理重载new和delete的语法,而不处理此类重载运算符的实现。我认为重载new和delete的语义应该有自己的常见问题解答,在运算符重载的主题中,我永远无法做到这一点。
基础
在C++中,当您编写像new T(arg)这样的新表达式时,在计算该表达式时会发生两件事:首先调用运算符new以获取原始内存,然后调用T的适当构造函数以将该原始内存转换为有效对象。同样,当您删除一个对象时,首先调用其析构函数,然后将内存返回给delete运算符。C++允许您调整这两个操作:内存管理和在分配的内存中构造/销毁对象。后者是通过为类编写构造函数和析构函数来实现的。微调内存管理是通过编写自己的运算符new和运算符delete来完成的。
运算符重载的第一个基本规则——不要这样做——尤其适用于重载new和delete。导致这些运算符过载的几乎唯一原因是性能问题和内存限制,在许多情况下,其他操作(如对所用算法的更改)将提供比试图调整内存管理高得多的成本/收益比。
C++标准库附带一组预定义的新建和删除运算符。最重要的是:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete[](void*) throw();
前两个为对象分配/解除分配内存,后两个为一个对象数组。如果您提供自己的版本,它们将不会过载,而是替换标准库中的版本。如果重载运算符new,则应始终重载匹配的运算符delete,即使您从未打算调用它。原因是,如果构造函数在计算新表达式时抛出,则运行时系统会将内存返回给运算符delete,该运算符delete与被调用以分配内存以在其中创建对象的运算符new相匹配。如果不提供匹配的delete运算符,则会调用默认的delete,这几乎总是错误的。如果重载new和delete,也应该考虑重载数组变量。
放置新的
C++允许new和delete运算符接受额外的参数。所谓的placement new允许您在某个地址创建一个对象,该地址将传递给:
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
标准库附带了相应的new和delete运算符重载:
void* operator new(std::size_t,void* p) throw(std::bad_alloc);
void operator delete(void* p,void*) throw();
void* operator new[](std::size_t,void* p) throw(std::bad_alloc);
void operator delete[](void* p,void*) throw();
注意,在上面给出的放置new的示例代码中,除非X的构造函数抛出异常,否则永远不会调用运算符delete。
还可以使用其他参数重载new和delete。与放置new的附加参数一样,这些参数也列在关键字new之后的括号内。仅仅出于历史原因,这种变体通常也被称为新放置,即使它们的参数不是用于将对象放置在特定地址。
类特定的新建和删除
最常见的情况是,您需要对内存管理进行微调,因为测量表明,特定类或一组相关类的实例经常被创建和销毁,而运行时系统的默认内存管理(针对一般性能进行了调整)在这种特定情况下处理效率低下。为了改进这一点,可以为特定类重载new和delete:
class my_class {
public:
// ...
void* operator new(std::size_t);
void operator delete(void*);
void* operator new[](std::size_t);
void operator delete[](void*);
// ...
};
因此重载后,new和delete的行为类似于静态成员函数。对于my_class的对象,std::size_t参数将始终为sizeof(my_class)。然而,对于派生类的动态分配对象,也会调用这些运算符,在这种情况下,它可能会更大。
全局新建和删除
要重载全局new和delete,只需将标准库的预定义运算符替换为我们自己的运算符即可。然而,很少需要这样做。