Customizing new and delete

[toc]

Understande the behavior of the new-handler.

当operator new无法满足内存分配需求时,会抛出异常,用户可以通过调用set_new_handler来设置异常处理函数。

MSVC声明:

1
2
_EXPORT_STD using new_handler = void(__CLRCALL_PURE_OR_CDECL*)();
_EXPORT_STD extern "C++" _CRTIMP2 new_handler __cdecl set_new_handler(_In_opt_ new_handler) noexcept;

这段代码定义了new_handler类型为函数指针,并声明了set_new_handler函数,noexcept,表示不抛出异常

set_new_handler参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set_new_handler 被调用前正在执行(但马上就要被替换)的那个new-handler 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <new>
void my_new_handler() {
std::cerr << "Memory allocation failed. Exiting program." << std::endl;
std::abort();
}
int main() {
std::set_new_handler(my_new_handler);
while (1)
{
try {
int* p = new int[10000000];
}
catch (const std::bad_alloc& e) {
std::cerr << "Caught bad alloc: " << e.what() << std::endl;
}
}
std::cin.get();
return 0;
}
//最后一行输出 Memory allocation failed. Exiting program. ,并没有调用catch语句
//在x64模式下,能分配1000多次
  • 若不调用std::abort(),则程序将陷入死循环。即 不断调用new-handler函数,直到找到足够内存。

如果在写出错误信息至cerr 过程期间必须动态分配内存,考虑会发生什么事……)

如果在自定义的 new_handler 中尝试动态分配内存,而此时系统已经处于内存不足的状态,那么这个分配操作很可能会失败,从而再次触发 new_handler。这将导致一个递归调用链,可能会迅速耗尽栈空间,导致栈溢出,最终程序会崩溃。

为了避免这种情况,自定义的 new_handler 应该避免进行任何可能导致内存分配的操作,包括:

  • 创建新的对象(除非它们非常小并且可以分配在栈上)
  • 调用可能分配内存的函数(例如,某些 I/O 操作或字符串处理函数)
  • 抛出异常(因为异常处理需要分配内存来处理栈展开)

自定义的 new_handler 的主要目的是提供一个恢复机制,比如释放一些不必要的内存,或者尝试调整程序的状态以减少内存需求。如果无法恢复,处理函数应该以一种安全的方式终止程序,例如调用 std::abort()std::exit()

那么,一个设计良好的0handler处理函数必须做以下事情:

  • 让更多内存可被使用:实现此策略的一个做法是,程序一开始执行就为new-handler 分配一大块内存,而后当new-handler 第一次被调用,将它们释还给程序使用。
  • 安装另一个new-handler:这个旋律的变奏之一是让new-handler 修改自己的行为,于是当它下次被调用,就会做某些不同的事。为达此目的,做法之一是令new-handler 修改“会影响new-handler 行为"的static 数据、namespace 数据或global 数据。
  • 卸除new-handler:将nullptr传给set new handler 。一旦没有安装任何new-handler, operator new 会在内存分配不成功时抛出异常。
  • 抛出bad_alloc(或派生自bad_alloc)的异常:这样的异常不会被operator new 捕捉,因此会被传播到内存索求处。
  • 不返回:调用 std::abort()std::exit()

类专属new-handlers

//TODO

在C++11,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <new> // 用于 std::set_new_handler 和 std::new_handler

class MyClass {
public:
// 自定义 new_handler
static void customNewHandler() {
std::cerr << "Memory allocation failed for MyClass. Custom new handler invoked." << std::endl;
// 可以在这里尝试释放一些内存、调整程序状态或记录日志
// 如果无法恢复,可能需要终止程序
std::abort(); // 终止程序
}

// 重写 operator new
static void* operator new(std::size_t size) throw(std::bad_alloc) {
// 设置专属的 new_handler
std::new_handler prevHandler = std::set_new_handler(customNewHandler);
void* ptr = ::operator new(size); // 调用全局 new
// 恢复之前的 new_handler
std::set_new_handler(prevHandler);
return ptr;
}

// 重写 operator new[]
static void* operator new[](std::size_t size) throw(std::bad_alloc) {
// 设置专属的 new_handler
std::new_handler prevHandler = std::set_new_handler(customNewHandler);
void* ptr = ::operator new[](size); // 调用全局 new[]
// 恢复之前的 new_handler
std::set_new_handler(prevHandler);
return ptr;
}
};

int main() {
while(1)
try {
MyClass* obj = new MyClass[1000000000];
std::cout << "Memory allocated successfully for MyClass." << std::endl;
}
catch (const std::bad_alloc& e) {
std::cerr << "Caught bad alloc: " << e.what() << std::endl;
}

// 程序的其他部分...


return 0;
}
//Memory allocated successfully for MyClass.
//Memory allocation failed for MyClass. Custom new handler invoked.

可以看到我们可以通过重写new操作符来实现类专属new-handlers。

nothrow new

Nothrownew 对异常的强制保证性并不高。要知道,表达式new (std: :nothrow)widget,发生两件事,第一, nothrow 版的operator new 被调用,如果分配成功。Widget构造函数可以做它想做的任何事。它有可能又new 一些内存,而没人可以强迫它再次使用nothrow new 。因此虽然”new (std: :nothrow) Widget” 调用的operator new 并不抛掷异常,但Widget构造函数却可能会。如果它真那么做,该异常会一如往常地传播。

请记住

  • set new handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。

Understand when it makes sense to replace new and delete

替换默认new和delete的理由:

  • 用来检测运行上的错误:new时我们可以在客户所得区块边界添加特定的byte patterns(签名),在delete我们可以检测标记,判断对分配的内存的操作是否越界。
  • 为了强化效能:默认的operator new 和operator delete 主要用于一般目的,它们不但可被长时间执行的程序(例如网页服务器, web servers) 接受,也可被执行时间少于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题C fragmentation) ,这最终会导致程序无法满足大区块内存要求,即使彼时有总量足够但分散为许多小区块的自由内存。它们的工作对每个人都是适度地好,但不对特定任何人有最佳表现。定制版之operator new 和operator delete 性能胜过缺省版本。
  • 为了收集使用上的统计数据:在一头栽进定制型news 和定制型deletes 之前,理当先收集你的软件如何使用其动态内存。分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO (先进先出)次序或LIFO (后进先出)次序或随机次序来分配和归还?它们的运用型态是否随时间改变,也就是说你的软件在不同的执行阶段有不同的分配/归还形态吗?任何时刻所使用的最大动态分配量(高水位)是多少?自行定义opera七or new 和operator delete 使我们得以轻松收集到这些信息。

这些我们可以通过使用成熟的第三方库或者工具解决。

  • 增加分配和归还的速度:例如如果你的程序是单线程的,那么你可以使用不具备线程安全的分配器来提升速度。
  • 为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往(虽然并非总是)不只比定制型慢,它们往往还使用更多内存。
  • 为了弥补缺省分配器中的非最佳齐位C suboptimal alignment) 。在C++11, 已经解决了这一问题。
  • 为了将相关对象成簇集中。如果你知道特定之某个数据结构往往被一起使用,而你又希望在处理这些数据时将“内存页错误”(page faults) 的频率降全最低,那么为此数据结构创建另一个heap 就有意义,这么一来它们就可以被成簇集中
    在尽可能少的内存页(pages) 上。(即增加缓存命中的几率)new 和delete 的“placement 版本” (见条款52) 有可能完成这样的集簇行为。
  • 获得非传统的行为。 有时候你会希望operators new 和delete 做编译器附带版没做的某些事情。例如你可能会希望分配和归还共享内存(shared memory)内的区块,但唯一能够管理该内存的只有CAPI 函数,那么写下一个定制版new和delete (很可能是placement 版本,见条款52) ,你便得以为CAPI 穿上一件C++ 外套。你也可以写一个自定的operator delete, 在其中将所有归还内存内容覆盖为o, 藉此增加应用程序的数据安全性。

请记住

  • 有许多理由需要写个自定的new 和delete, 包括改善效能、对heap 运用错误进行调试、收集heap 使用信息。

51. Adhere to convention when writing new and delete.

new:

  • 实现一致性operator new 必得返回正确的值,内存不足时必得调用new-handling 函数(见条款49) ,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new。
  • 如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,就遵循条款49 描述的规则,并抛出一个bad_alloc异常。
  • operator new 实际上不只一次尝试分配内存,并在每次失败后调用new-handling 函数。这里假设new-handling 函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling 函数的指针是null。operator new 才会抛出异常。
  • C++规定,即使客户要求0 bytes, opera七or new 也得返回一个合法指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void * operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;

if (size == 0) { //0 bytes 申请量视为1 byte 申请量
size = 1;
}

// 循环尝试分配 size 字节的内存
while (true) {
// 尝试分配 size 字节的内存
_attempt to allocate size bytes;_

// 如果分配成功,则返回指向内存的指针
if (_the allocation was successful_)
return (_a pointer to the memory_);

// 如果分配失败,则处理内存不足的情况
// 获取当前的 new-handling 函数
new_handler globalHandler = set_new_handler(0);
// 重新设置 new-handling 函数
set_new_handler(globalHandler);

// 如果存在全局的 new-handling 函数,则调用它
if (globalHandler) (*globalHandler)();
// 否则抛出 std::bad_alloc 异常
else throw std::bad_alloc();
}
}

只有在以下情况才能终止循环:内存被成功分配或new-handling 函数做了一件描述于条款49 的事情:让更多内存可用、安装另一个new-handler 、卸除new-handler 、抛出bad_alloc 异常(或其派生物),或是承认失败而直接return 。

注意operator new 成员函数会被derived classes 继承,子类大小和父类大小不一致时可能引发错误

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
// 声明静态的 operator new 函数,用于动态分配内存
static void * operator new(std::size_t size) throw(std::bad_alloc);
...
};

class Derived: public Base // Derived doesn't declare
{ ... }; // operator new

Derived *p = new Derived; // 调用 Base::operator new!

那么我们只需在基类中加入对类型大小的判断,注意我们去掉了对分配大小为0的判断,因为已经交由标准的 operator new 处理。

1
2
3
4
5
6
7
8
9
10
11
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
// 如果请求的大小不等于 Base 类对象的大小
if (size != sizeof(Base))
// 则交由标准的 operator new 处理请求
return ::operator new(size);

// 否则在这里处理请求
...
}

//TODO 类的new[]

delete:

  • C++保证“删除null 指针永远安全”,所以你必须兑现这项保证。
1
2
3
4
5
6
7
8
void operator delete(void *rawMemory) throw()
{
// 如果要删除的是空指针,则什么也不做
if (rawMemory == 0) return;

// 释放 rawMemory 指向的内存
_deallocate the memory pointed to by rawMemory;_
}

成员函数还要检测对象大小是否等于 Base 类对象的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base {
public:
// 声明静态的 operator new 和 operator delete 函数
static void * operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void *rawMemory, std::size_t size) throw();
...
};

void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
// 检查是否为 null 指针
if (rawMemory == 0) return;

// 如果请求的大小不等于 Base 类对象的大小
if (size != sizeof(Base)) {
// 则交由标准的 operator delete 函数处理请求
::operator delete(rawMemory);
return; // 结束函数执行
}

// 在这里释放 rawMemory 指向的内存
_deallocate the memory pointed to by rawMemory;_
return; // 结束函数执行
}

如果即将被删除的对象派生自某个base class 而后者欠缺virtual 析构函数,那么C++传给operator delete 的size_t 数值可能不正确。这是“让你的base classes 拥有virtual 析构函数”的一个够好的理由。此刻只要你提高警觉,如果你的base classes 遗漏virtual析构函数, operator delete 可能无法正确运作。

请记住:

  • operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler 。它也应该有能力处理0 bytes 申请。Class专属版本则还应该处理”比正确大小更大的(错误)申请”。
  • operator delete 应该在收到null 指针时不做任何事。Class 专属版本则还应该处理“比正确大小更大的(错误)申请”。

52. Write placement delete if you write placement new.

考虑如下例子

1
Widget* pw = new Widget;

假设其中第一个函数调用成功,第二个函数却抛出异常。既然那样,步骤一的内存分配所得必须取消并恢复旧观,否则会造成内存泄漏(memory leak)

当你只使用正常形式的new 和delete, 运行期系统毫无问题可以找出那个“知道如何取消new 所作所为并恢复旧观”的delete 。然而当你开始声明非正常形式的operator new, 也就是带有附加参数的operator new, “究竟哪一个delete 伴随这个new” 的问题便浮现了。

什么是placement new和placement delete

  • placement new:即除了size_t还有其他参数。
  • placement delete:直接派生自placement new。即除了size_t还有其他参数。

为什么我们要提起说placement delete直接派生自placement new?:

  • 如果一个带额外参数的operator new 没有“带相同额外参数”的对应版operator delete, 那么当new 的内存分配动作需要取消并恢复旧观时就没有任何operator delete 会被调用。因此,为了消除稍早代码中的内存泄漏,Widget 有必要声明一个placement delete。

我们使用一个ostream来记录分配信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Widget {
public:

static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
//声明placement new
static void operator delete(void *pMemory) throw();

static void operator delete(void *pMemory, std::ostream& logStream) throw();
//我们还需要声明placement delete
};


Widget* pw = new (std::cerr) Widget; //这里不会发生内存泄露

delete pw //调用正常的operator delete;

delete pw调用正常的operator delete。placement delete 只有在“伴随placement new 调用而触发的构造函数”出现异常时才会被调用。

这意味着我们必须同时提供一个正常的operator delete (用于构造期间无任何异常被抛出)和一个placement
版本(用于构造期间有异常被抛出)。后者的额外参数必须和operator new 一样。

名称掩盖

因为 member function(成员函数)的名字会覆盖外围的具有相同名字的函数Item 33,你需要小心避免用 class-specific(类专用)的 news 覆盖你的客户所希望看到的其它 news(包括其常规版本)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:

static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
// 这个自定义的 new 操作符接受一个额外的 std::ostream 引用参数,
// 并且隐藏了标准的全局 new 操作符。如果内存分配失败,会抛出 std::bad_alloc 异常。

};

Base *pb = new Base;
// 错误!标准形式的 operator new 被隐藏了。

Base *pb = new (std::cerr) Base;
// 正确,调用 Base 类中定义的带额外参数的 placement new。

在派生类上也是如此:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Derived: public Base {                   // 从上面的 Base 类继承
public:
static void* operator new(std::size_t size) // 重新声明标准形式的 new
throw(std::bad_alloc); // 如果内存分配失败,将抛出 std::bad_alloc 异常
};

Derived *pd = new (std::clog) Derived; // 错误!Base 类的带参数的 placement new 被隐藏
// 试图使用 Base 类中定义的带 std::ostream 参数的 new 操作符来创建 Derived 对象时失败,
// 因为 Derived 类中重新声明的标准 new 操作符覆盖了它。

Derived *pd = new Derived; // 正确,调用 Derived 类的 operator new
// 这行代码使用了 Derived 类重新声明的标准形式的 new 操作符来创建对象,是合法的操作。

如果想使用全局的new和delete参见条款33,使用作用域操作符using声明

请记住

  • 当你写一个placement operator new, 请确定也写出了对应的placement operator delete 。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
  • 当你声明placement new 和placement delete, 请确定不要无意识(非故意)地遮掩了它们的正常版本。