读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定

Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定。遵守这些规则

不是很困难,但是它们其中有一些并不直观,所以知道这些规则是什么很重要。

1. 定义operator new的约定

1.1 约定列举

我们以operator new开始。实现一个一致的operator new需要有正确的返回值,在没有足够内存的时候调用**new-handling**函数(见Item 49),并且做好准备处理没有内存可分配的情况。你也想避免无端的隐藏“正常”版本的new,但这是一个类接口的问题而不是实现需求问题;它会在Item 52中进行处理。

Operator new的返回值部分很简单,因为operator new事实上会尝试多次分配内存,在内次分配失败之后都会调用new-handling函数。这里的假设是new-handling函数可能会做一些事情来释放一些内存。只有在指向new-handling函数的指针为null的情况下,operator new才会抛出异常。

好奇的是,C++即使在请求0个byte的时候也需要operator new返回一个合法的指针。(这个听上去很奇怪的要求简化了语言中的某些事情。)这就是基本情况,一个非成员operator new的伪代码会是像下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void* operator new(std::size_t size) throw(std::bad_alloc)
{ // your operator new might
using namespace std; // take additional params
if (size == 0) { // handle 0-byte requests
size = 1; // by treating them as
} // 1-byte requests
while (true) {
attempt to allocate size bytes;
if (the allocation was successful)
return (a pointer to the memory);
// allocation was unsuccessful; find out what the
// current new-handling function is (see below)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}

把请求0个byte当作请求1一个byte来进行处理的诡计看上去让人厌恶,但这是简单的实现并且合法,而且能够工作,无论如何,你对0个byte的请求会有多频繁呢?

对于伪代码中将new-handling函数指针设为null,然后迅速的将其复原的地方,你可能看上去比较怀疑。不幸的是,没有其他方法直接获得new-handling函数的指针,所以你必须调用set_new_handler来发现这个函数是什么。看上去粗糙但却是有效的,起码对于单线程来说是有效的。在多线程环境中,你可能需要某种类型的锁来安全的操作new-handling函数背后的(全局)数据结构。

Item 49中讨论过了,在operator new中包含一个无限循环,上面的代码中将其展示了出来;“while(true)”就 表示一个无限循环。跳出循环的唯一方法是成功的分配内存或者让new-handling函数做到Item 49中描述的事情中的其中一件:有更多的内存可供分配,安装一个不同的**new-handler,卸载new-handler,抛出一个异常,这个异常要么继承自bad_alloc**要么源于失败返回。现在你应该清楚为什么new-handler必须做到这些事情中的一件的了,如果做不到,operator new中的循环永远不会终止。

1.2 由继承导致的问题

许多人没有意识到operator new成员函数是要被派生类继承的。这可能会导致一些有趣的并发症。在上面的operator new的伪代码中,注意函数尝试分配size个bytes。这再合理不过了,因为这是传递到函数中的参数。然而,正如Item 50中解释的,实现一个自定义内存管理器的最一般的原因就是为特定类的对象进行内存分配优化,而不是为类或者它的任何派生类。也即是,我们为类X提供了一个operaor new,这个函数的行为是为大小正好为sizeof(X)的对象进行调整,即不大也不小。然而由于继承的存在,可能发生通过调用基类中的operator new来为派生类对象分配内存:

1
2
3
4
5
6
7
8
9
class Base {
public:
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; // calls Base::operator new!

如果基类中的operator new设计没有处理这种情况,处理它的最好的方法将对“错误”数量内存的请求丢弃掉,而是转而使用标准operator new来处理,就像下面这样:

1
2
3
4
5
6
7
8
9
10
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // if size is “wrong,”
return ::operator new(size); // have standard operator
// new handle the request
... // otherwise handle
// the request here
}

“等一下”我听见你大叫,“你忘记检查病态但是可能发生的情况,也就是size为0的情况了!”事实上,我没有忘记。测试仍然在那里,只不过是将测试并入size同sizeof(size)的测试之中了。C++用神秘的方式进行工作,其中之一的方式就是规定所有独立对象的大小不能为0(见Item 39)。根据定义,sizeof(Base)永远不会为0,所以如果size为0,内存请求将由::operator new来处理,它会以一种合理的方式来处理这个请求。

1.3 定义operator new[]的约定

如果你想在一个类中控制数组的内存分配,你需要实现operator new的数组形式,operator new[]。(这个函数通常被叫做“array new”,因为很难确定“operator new[]”该如何发音)。如果你决定实现operator new[],记住所有你正在做的是分配一大块原生内存——你不能对不存在于数组中的对象做任何事情。事实上,你甚至不能确定数组将会有多少对象。首先,你不会知道每个对象有多大。毕竟,很有可能通过继承来调用基类的operator new[]去为派生类对象数组分配内存,派生类对象通常比基类对象要大。因此,你不能假设在Base::operator new[]内部被放入数组的对象的大小为sizeof(Base),这就意味着你不能假设数组中对象的数量为(请求的字节数)/sizeof(Base)。第二,传递给operator new[]的参数size_t有可能比填入对象的内存更多,因为正如Item 16中解释的,动态分配的数组有可能包含额外的空间来存放数组元素的数量。

2. 定义operator delete的约定

当实现operator new的时候需要遵守的约定就这么多。对于operator delete,事情更加简单。所有你需要记住的是C++总是保证delete null指针是安全的,所以你需要遵守这个规定。下面是实现非成员 operator delete的伪代码:

1
2
3
4
5
6
void operator delete(void *rawMemory) throw()
{
if (rawMemory == 0) return; // do nothing if the null
// pointer is being deleted
deallocate the memory pointed to by rawMemory;
}

这个函数的成员函数版本也是简单的,但是你需要确保检查正在被delete的对象的size。假设你的属于类的operator new将对错误数量内存的请求转发给了::operator new,你同样得将对“错误大小”的delete请求转发给::operator delete:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base { // same as before, but now
public: // operator delete is declared
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()
{
if (rawMemory == 0) return; // check for null pointer
if (size != sizeof(Base)) { // if size is “wrong,”
::operator delete(rawMemory); // have standard operator
return; // delete handle the request
}
deallocate the memory pointed to by rawMemory;
return;
}

有趣的是,如果要被delete的对象派生自于一个没有虚析构函数的基类,那么传递给operator delete的size_t值有可能是不正确的。这就有了足够的理由来把你的基类中的析构函数声明为虚函数,但是Item 7中描述了第二个可能更好的原因。现在你需要注意的是如果你在基类中忽略了虚析构函数,operator delete函数的工作就有可能不正确。

3. 总结

  • operator new应该包含一个无限循环来尝试分配内存,如果不能满足对内存的请求应该调用new-handler,应该处理对0个byte的请求。类的特定版本应该处理比预期更大的内存块的请求。
  • operator delete中传递的指针如果是null,应该什么都不做。类特定版本需要处理比预期要大的内存块。