13.5 重载new和delete
当我们创建一个new表达式时,会发生两件事。首先,使用operator new()分配内存,然后调用构造函数。在delete表达式里,调用了析构函数,然后使用operator delete()释放内存。我们无法控制构造函数和析构函数的调用(否则可能会意外地搅乱它们),但可以改变内存分配函数operator new()和operator delete()。
使用了new和delete的内存分配系统是为通用目的而设计的。但在特殊的情形下,它并不能满足需要。最常见的改变分配系统的原因是出于效率考虑:也许要创建和销毁一个特定的类的非常多的对象以至于这个运算变成了速度的瓶颈。C++允许重载new和delete来实现我们自己的存储分配方案,所以可以用它来处理问题。
另一个问题是堆碎片:分配不同大小的内存可能会在堆上产生很多碎片,以至于很快用完内存。虽然内存可能还有,但由于都是碎片,也就找不到足够大的内存块满足需要。通过为特定类创建自己的内存分配器,可以确保这种情况不会发生。
在嵌入和实时系统里,程序可能必须在有限的资源情况下运行很长时间。这样的系统也可能要求分配内存花费相同的时间且不允许出现堆内存耗尽或出现很多碎片的情况。由客户定制的内存分配器是一种解决办法,否则程序设计者在这种情况下要避免使用new和delete,而这将错过了C++很有价值的优点。
当重载operator new()和operator delete()时,我们只是改变了原有的内存分配方法,记住这一点是很重要的。编译器将用重载的new代替默认的版本去分配内存,然后为那个内存调用构造函数。所以,虽然当编译器看到new时,编译器分配内存并调用构造函数,但是当重载new时,可以改变的只是内存分配部分(delete也有相似的限制。)。
当重载operator new()时,也可以替换它用完内存时的行为,所以必须在operator new()里决定做什么:返回0、写一个调用new-handler的循环、再试着分配或者(典型的)产生一个bad_alloc的异常信息(在第2卷中讨论,可从www.BruceEckel.com处获得)。
重载new和delete与重载任何其他运算符一样。但可以选择重载全局内存分配函数或者是针对特定类的分配函数。
13.5.1 重载全局new和delete
当全局版本的new和delete不能满足整个系统时,对其重载是很极端的方法。如果重载全局版本,就使默认版本完全不能被访问—甚至在这个重新定义里也不能调用它们。
重载的new必须有一个size_t参数(sizes的标准C类型)。这个参数由编译器产生并传递给我们,它是要分配内存的对象的长度。必须返回一个指向等于这个长度(或大于这个长度,如果有这样做的原因)的对象的指针,如果没有找到存储单元(在这种情况下,构造函数不被调用),则返回一个0。然而如果找不到存储单元,不能仅仅返回0,也许还应该做一些诸如调用new-handler或产生一个异常信息之类的事,通知这里存在问题。
operator new()的返回值是一个void*,而不是指向任何特定类型的指针。所做的是分配内存,而不是完成一个对象建立—直到构造函数调用了才完成对象的创建,它是编译器确保做的动作,不在我们的控制范围之内。
operator delete()的参数是一个指向由operator new()分配的内存的void。参数是一个void是因为它是在调用析构函数后得到的指针。析构函数从存储单元里移去对象。operator delete()的返回类型是void。
下面提供了一个如何重载全局new和delete的简单的例子:
这里可以看到重载new和delete的通常形式。这里的内存分配使用了标准C库函数malloc()和free()(可能默认的new和delete也使用这些函数)。并且,它们还打印出了有关正在做什么的信息。注意,这里使用printf()和puts()而不是iostreams。因此,当创建了一个iostream对象时(像全局的cin、cout和cerr),它们调用new去分配内存。用printf()不会进入死锁状态,因为它不调用new来初始化本身。
在main()里,创建内建数据类型对象以证明在这种情况下也调用重载的new和delete。然后创建一个类型S的单个对象,接着创建一个类型S的数组。对于这个数组,从所需要的字节数目中可以看到,额外的内存被分配用于存放它所包含对象的数量的信息。在所有情况下,都使用了全局重载版本的new和delete。