operator new
operator delete
set_new_handler
49 了解new-handler的行为
- 如果operator new分配内存失败,在抛出异常之前,它会先调用一个new-handler函数用以处理内存不足的错误,这个错误处理函数需要通过调用头文件<new>中的set_new_handler函数指定
- set_new_handler做三件事
- 尝试使更多内存可用
- 终止程序(如调用std::terminate)
- 抛出std::bad_alloc或std::bad_alloc的派生类异常
namespace std {
typedef void(*new_handler)();
new_handler set_new_handler(new_handler new_p) noexcept;
}
- new_handler是个无参数无返回值函数的typedef,它定义出一个函数指针,用作set_new_handler函数的实参和返回类型
- set_new_handler的参数是个函数指针,指向operator new无法分配足够内存时该调用的new-handler函数,如果是空指针,默认的分配函数直接抛std::bad_alloc异常
- set_new_handler的返回值也是个指针,指向set_new_handler被调用前正在执行的那个new-handler函数
- 可以这样使用set_new_handler函数
#include <iostream>
#include <new>
void handler()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(handler);
while (true) new char[1024*1024*1024]; // 循环分配1GB内存
// 分配失败时调用handler()函数
};
- 如果operator new无法分配足够空间,new-handler函数会被调用,于是程序发出错误信息后abort。当operator new无法满足内存申请时,他会不断调用new-handler函数,直到找到足够内存。 一个设计良好的new-handler函数必须做到以下事情
- 让更多的内存可以被使用。一个做法是程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释放给程序使用
- 安装另一个new-handler。如果目前这个new-handler无法取得更多内存,或许它知道另外哪个有此能力,这样目前这个就可以调用set_new_handler安装另外那个替换自己
- 卸载new-handler。也就是将空指针传给set_new_handler,一旦没安装任何new-handler,operator new会在内存分配不成功时抛出异常
- 抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处
- 不返回。通常调用std::abort()或者std::exit(1)
#include <iostream>
#include <new>
void handler()
{
std::cout << "Memory allocation failed, terminating\n";
std::set_new_handler(nullptr); // 分配失败则抛std::bad_alloc异常
}
int main()
{
std::set_new_handler(handler);
try {
while (true) {
new int[100000000ul];
}
} catch (const std::bad_alloc& e) {
std::cout << e.what() << '\n';
}
}
- 要对类对象处理内存分配失败情况,需要声明一个类型为new_handler的static成员,用以指向类的new-handler,当operator new无法为对象分配足够内存时调用
class A {
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* opreator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
// static成员必须在class定义式之外被定义
std::new_handler A::currentHandler = 0;
- set_new_handler函数会将类当前的指针存储起来,再将当前指针设置为new-handler,然后返回存储的旧指针,这也是标准版set_new_handler的行为
std::new_handler A::set_new_handler(std::new_handler p) noexcept
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
- 最后,operator new做以下事情
- 调用标准set_new_handler,告知类的错误处理函数,即将new-handler安装为global new-handler
- 调用global operator new,执行实际分配内存。如果分配失败,global operator new会调用类的new-handler(上一步刚被安装为global new-handler)
- 如果global new最终无法分配足够内存,会抛出一个std::bad_alloc异常,在此情况下类的operator new必须恢复为原本的global new-handler然后再传播该异常
- 如果global operator new能够分配足够一个类对象所用的内存,类的operator new会返回一个指针,指向分配所得。析构函数会自动恢复operator new被调用前的global new-handler
- 下面通过代码详细解释。使用基础RAII操作,构造时存储原有的new-handler,析构时将通过调用set_new_handler安装原有的new-handler
class A {
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* opreator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
std::new_handler A::currentHandler = 0;
std::new_handler A::set_new_handler(std::new_handler p) noexcept
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) // 取得目前new-handler
: handler(nh) {}
~NewHandlerHolder() // 释放它
{ std::set_new_handler(handelr); }
NewHandlerHolder(const NewHandlerHolder&) = 0; // 阻止copying
NewHandlerHolder& operator=(const NewHandlerHolder&) = 0;
private:
std::new_handler handlder; // 记录下来
};
// 这样operator new的实现就很简单
void* A::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler)); // A安装new-handler
// 返回值为A之前拥有的currentHandler,即旧的指针被传给NewHandlerHolder存储
return ::operator new(size); // 分配内存或者抛出异常
}; // NewHandlerHolder执行析构函数,安装存储的旧指针,从而恢复global new-handler
// A的用户应该类似这样使用其new-handling
void outOfMem(); // 函数声明,此函数在对象分配失败时被调用
A::set_new_handler(outOfMem); // 设定outOfMem为类的new-handling函数
A* p1 = new A; // 如果内存分配失败,调用outOfMem
std::string* ps = new std::string; // 如果内存分配失败,调用global new-handling函数
A::set_new_handler(0); // 设定类专属的new-handling函数为空
A* p2 = new A; // 如果内存分配失败,立刻抛出异常
- 上述实现并不因class的不同而不同,因此创建适用于所有类的类模板new-handler是个好想法。创建一个用于设置new-handler的类模板,然后在子类中只需简单继承即可
template <typename T>
class NewHandlerSupport {
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
private:
static std::new_handler currentHandler;
};
template <typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) noexcept
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template <typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandlder));
return ::operator new(size);
}
template <typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
// 有了这个class template,为A添加set_new_handler就很容易了
class A::public NewHandlderSupport<A> {
...
// 和先前一样,但不必声明set_new_handler或者operator new
};
- 上述做法就是CRTP,看起来模板参数T从未被使用,实际上也不需要使用。这种做法只是希望每个派生类拥有实例互异的NewHandlerSupport复件,更确切地说是其static成员currentHandler,T只是用来区分不同的派生类,模板机制会为每个T生成一份currentHandler
50 了解new和delete的合理替换时机
- 替换标准库提供的operator new或operator delete通常基于以下三个理由
- 用来检测运行上的错误。将"new所得内存"delete失败会导致内存泄露,多次对同一块"new所得内存"delete会导致未定义行为。如果operator new持有一串动态分配所得地址,而operator delete将地址从中移走,很容易检测出上述错误。各式各样的编程错误会导致数据"overruns"(写入点在分配区块末端之后)或"underruns"(写入点在分配区块起点之前)。如果自定义一个operator new便可超额分配内存,以额外空间放置特定的byte patterns(即签名,signatures)。operator delete便可以上述签名是否原封不动,若否就表示在分配区的某个生命时间点发生overrun或underrun,这时operator delete可以志记(log)那个事实以及进行非法操作的指针
- 为了强化效能。标准库所提供的operator new和operator delete主要用于一般目的,它们不但可被长时间执行的程序(如网页服务器,web server)接受,也可被执行时间小于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题,这最终会导致程序无法满足大区块内存要求。由于对内存管理器的要求多种多样,因此标准库所提供的operator new和operator delete采取中庸之道。如果对自己程序的动态内存运行形态有深刻的了解,就会发现定制版的operator new和operator delete性能胜过缺省版本,它们比较快,需要的内存比较少
- 为了收集使用上的统计数据。自行定义operator new和operator delete可以帮助收集软件内存区块大小分布、寿命分布、内存归还次序、最大动态分配量等信息
- 以下是一个初阶段global operator new的例子,它能够促进并协助检测"overruns"和"underruns"
static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
//这段代码还有若干小错误,详下
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
size_t realSize = size + 2 * sizeof(int);
void* pMem = malloc(realSize);
if (!pMem) throw bad_alloc();
// 将signature写入内存的最前段落和最后段落
*(static_cast<int*>(pMem)) = signatrue;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature;
// 返回指针,指向恰位于第一个signature之后的内存位置
return static_cast<Byte*>(pMem) + sizeof(int);
}
- 这个operator new的主要缺点在于它忽略了其应该具备的坚持C++规则的态度,比如条款51提到所有operator new都应该内含一个循环,反复调用某个new-handling函数,这里却没有。此外,还有一个更加微妙的主题:内存对齐(alignment)
- 许多计算机体系结构要求特定类型必须放在特定的内存地址上,例如指针的地址必须是4的倍数,double的地址必须是8的倍数。如果不满足这个条件可能会导致运行期硬件异常。有些体系结构如Intel x86架构的double可被对其于任何边界,但如果它是8-byte齐位,其访问速度将会快许多
- 在定制operator new的过程中,内存对齐意义重大,因为C++要求所有operator new返回的指针都有适当的对齐。malloc就是在这样的要求下工作,所以令malloc返回一个得自malloc的指针是安全的。然而上述operator new并未提供这样的保证,而是返回一个得自malloc且偏移一个int大小的指针,并不安全。如果客户端调用operator new来获取给一个double所用的内存,同时在一部int为4bytes且doubles必须为8-byte齐位的机器上运行,可能会获得一个未有适当齐位的指针,这可能会导致程序崩溃或执行速度变慢。因此写一个能优良运作的内存管理器可能并不容易,许多平台已有可以替代编译器自带之内存管理器的商业产品,开放源码领域的内存管理器(如Boost库的Pool)也都可用,因而可能并不需要自己定制operator new和operator delete
- 总结下来,定制operator new和operator delete的作用
- 为了检测运行错误
- 为了收集动态分配内存之使用统计信息
- 为了增加分配和归还的速度
- 为了降低缺省内存管理器所带来的额外开销
- 为了弥补缺省分配器中的非最佳齐位
- 为了将相关对象成簇集中
- 为了获得非传统行为
51 编写new和delete时需固守常规
- 上条已经说明为什么要写自己的operator new和operator delete,本条款解释在编写时遵循什么守则
- 从operator new开始。operator new必须返回正确的值,内存不足时必须调用new-handling函数,要有对付零内存需求的准备,避免不慎掩盖正常形式的new——虽然这比较偏近class接口的要求而非实现要求。正常形式的new描述于条款52
- operator new如果申请内存成功,就返回指向那块内存的指针,否则抛出bad_alloc异常。operator new实际上不只一次尝试内存分配,并在每次失败后都调用new-handling函数。这里假设new-handling函数能做某些动作将某些内存释放出来,只有指向new-hangling函数的指针为空operator new才会抛出异常。C++规定,即使客户要求0 byte,operator new也要返回一个合法指针,这种看似诡异的行为其实是为了简化语言其他部分,下面是个non-member operator new的伪码
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
if (size == 0) { // 处理0-byte申请
size = 1; // 将其视为1-byte申请
}
while (true) {
尝试分配size bytes;
if (分配成功)
return (一个指针,指向分配得来的内存);
// 分配失败,找到当前的new-handling函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
- 如果在多线程环境下,还需要某种锁机制,以便处理new-handling函数背后的global数据结构。上面包含一个无穷循环,退出此循环的唯一办法是:内存分配成功或new-handling做了一件描述于条款49中的事:让更多内存可用、安装另一个new-handler、卸载new-handler、抛出bad_alloc异常(或其派生类),或承认失败直接return。上面的operator new成员函数可能会被derived classes继承。函数尝试分配size bytes,这很合理,因为size是函数接收的实参。条款50提到,定制内存分配器往往是为了针对特定的class对象分配行为提供最优化,却不是为了该class的派生类
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derived : public Base
{ ... }; // 假设重新定义operator new
Derived* p = new Derived; // 这里调用了Base::operator new
- 如果Base class专属的operator new并非被设计用来对付上述情况,处理此情势的最佳做法是将“内存申请量错误”的调用行为改采标准operator new
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base))
return ::operator new(size);
...
}
- 如果希望控制class专属版的arrays内存分配,那么需要实现operator new[]。编写operator new[]时,唯一要做的一件事就是分配一块未加工内存,因为无法对array之内迄今尚未存在的元素对象做任何事,无法知道这个array含有多少个元素对象。可能不知道每个对象多大,因为base class的operator new[]有可能经由继承被调用,将内存分配给derived class对象的array使用。因此不能在Base::operator new[]中假设每个元素对象大小是sizeof(Base),这也意味着不能假设array元素个数是
bytes申请数/sizeof(Base)
。此外,传递给operator new[]的size_t参数,其值有可能比“将被填以对象”的内存数量更多,因为条款16说过,动态分配的array可能包含额外空间用来存放元素个数 - operator delete情况就简单很多,唯一需要记住的就是C++保证删除空指针永远安全,你必须兑现这项保证。下面是non-member operator delete的伪码
void operator delete(void* rawMemory) noexcept
{
if(rawMemory == 0) return; // 如果被删除的是个空指针就什么都不做
归还rawMemory所指内存;
}
- 这个函数的member版本也很简单,只需多加一个检查删除数量
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void* rawMemory, std::size_t size) noexcept
...
};
void Base::operator delete(void rawMemory, std::size_t size) noexcept
{
if (rawMemory == 0) return;
if (size != sizeof(Base)) {
::operator delete(rawMemory);
return;
}
归还rawMemory所指内存;
return;
}
- operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”
- operator delete应该在收到null指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”
52 写了placement new也要写placement delete
- placement new和placement delete是C++经常用到但是却不常见的两个操作符。当我们使用new创建一个对象时
A* p = new A;
- 有两个函数被调用,第一个函数就是用以分配内存的operator new,第二个是类的默认构造函数。如果第一个函数调用成功,但是第二个函数抛出异常,这时需要释放第一步分配的内存,否则就造成了内存泄露。此时用户无法归还内存,因为构造函数抛出异常,p尚未被赋值,用户手中没有指向要被归还的内存的指针
- 释放内存的任务落到了C++运行期系统身上,运行期系统会调用第一个函数operator new所对应的operator delete版本,operator delete可能有多个版本,因此它必须知道该调用的版本
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void* rawMemory) noexcept; // global作用域中的正常签名式
void operator delete(void* rawMemory, std::size_t size) noexcept; // class作用域中典型的签名式
- 正常的operator new对应于正常的operator delete,如果使用正常的operator new和operator delete,运行期系统可以找到释放new开辟内存的delete函数。但是如果使用非正常形式的operator new就会有问题了。假设有一个类专属的operator new,它接受一个用来记录相关分配信息的ostream,同时又写了一个正常形式的类专属的operator delete
class A {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) // 非正常形式的new
throw(std::bad_alloc);
static void operator delete(void* pMemory, std::size_t size) noexcept; // 正常的class专属delete
...
};
- 这个设计有问题,在讨论问题前先说明若干术语。operator new除了必需的size_t,还接受其他参数,这就是所谓的placement new。因此,上述的operator new是个placement版本
- 特别有用的一个placement new版本会接受一个指向对象构造之处的指针,形式如下
void* operator new(std::size_t, void* pMemory) noexcept; // placement new
- 这个版本的new已被纳入标准库<new>中,它的用途之一是负责在vector的未使用空间上创建对象,它同时也是最早的placement new版本,谈到placement new时通常指的就是这一特定版本
- 现在再来看一下类的声明式,这个类会引起微妙的内存泄露。例如动态创建一个类对象时将分配信息记录于cerr
A* p = new (std::cerr) A; // 调用operator new,并传递cerr作为ostream实参
// 这个动作会在构造函数抛出异常时泄露内存
- 如果内存分配成功,而构造函数抛出异常,运行期系统负责释放operator new分配的内存,但运行期系统不知道真正被调用的operator new行为。于是运行期系统寻找参数个数和类型都与operator new相同的operator delete,这里的operator new接受ostream&类型的额外实参,对应的operator delete就应该是
void operator delete(void*, std::ostream&) noexcept;
- 类似于new的placement版本,接收额外参数的operator delete称为placement delete。由于类没有声明placement delete,运行期系统不知道如何取消并恢复对placement new的调用,于是什么都不做,因此类必须声明一个对应的placement delete
class A {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMemory) noexcept;
static void operator delete(void* pMemory, std::ostream& logStream) noexcept;
...
};
- 这样,如果下面的代码导致构造函数抛出异常,就会调用对应的placement delete
A* p = new (std::cerr) A; // 一如既往,但这次不再泄漏
- 但如果没有抛出异常(通常如此),即使有对应的placement delete,调用的也是正常形式的operator delete,因为只有伴随placement new调用的构造函数抛出异常,对应的placement delete才会被调用
- 成员函数的名称会覆盖其外围作用域中的相同名称,必须避免类专属的new覆盖外围作用域的new。比如一个类中声明了唯一的placement operator new,客户将无法使用正常形式的new
class Base {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc); // 这个new会掩盖正常形式的new
...
};
Base* pb1 = new Base; // 错误,因为正常形式的operator new被掩盖
Base* pb2 = new (std::cerr) Base; // 正确,调用Base的placement new
- 同理,派生类中的operator new会覆盖全局版本和继承版本
class Derived: public Base {
public:
...
static void* operator new(std::size_t size) throw(std::bad_alloc); // 重新声明正常形式的new
};
Derived* pd1 = new (std::clog) Derived; // 错误,因为Base的placement new被掩盖了
Derived* pd2 = new Derived; // 正确,调用Derived的operator new
- 以下标准形式operator new在全局作用域默认提供
void* operator new(std::size_t) throw(std::bad_alloc); // normal new
void* operator new(std::size_t, void*) noexcept; // placement new
void* operator new(std::size_t, const std::nothrow_t&) noexcept; // nothrow new
- 类内声明任何形式的operator new都会覆盖这些标准形式,对于每个可用的operator new必须提供对应的operator delete。一个简单做法是建立一个包含所有正常形式的new和delete的类
class StadardNewDeleteForms {
public:
// normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc)
{ return ::operator new(size); }
static void operator delete(void* pMemory) noexcept
{ ::operator delete(pMemory); }
// placement new/delete
static void* operator new(std::size_t size, void* ptr) noexcept
{ return ::operator new(size, ptr); }
static void operator delete(void* pMemory, void* ptr) noexcept
{ return ::operator delete(pMemory, ptr); }
// nothrow new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) noexcept
{ return ::operator new(size,nt); }
static void operator delete(void* pMemory,const std::nothrow_t&) noexcept
{ ::operator delete(pMemory); }
};
- 使用继承机制和using声明就能以自定义方式扩充标准形式
class A : public StandardNewDeleteForms {
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 添加自定义的placement new
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
// 添加自定义的placement delete
static void operator detele(void* pMemory, std::ostream& logStream) noexcept;
...
};
网友评论