条款八:理解各种不同含义的new和delete
1.区别 new操作符(new operator)和operator new
string *ps = new string("Memory Management");
这里的new是new operator。就象sizeof一样是语言内置的,你不能改变它的含义,它完成两个功能。
1)分配内存
2)调用构造函数初始化内存中的对象。(C语言中malloc分配内存后需要手动初始化)
operator new是专门用来分配内存的,C中的malloc函数功能一样,但它对构造函数一无所知。
void * operator new(size_t size);//函数声明
说明1:返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。(如果你喜欢,你能写一种operator new函数,在返回一个指针之前能够初始化内存以存储一些数值,但是一般不这么做。)
说明2:参数size_t确定分配多少内存。你能增加额外的参数重载函数operator new,但是第一个参数类型必须是size_t。
说明3:一般不会直接调用operator new,但是一旦这么做,象调用其它函数一样调用它:
void *rawMemory = operator new(sizeof(string));
operator new函数将返回一个指针,指向一块足够容纳一个string类型对象的内存。
说明4:遇到如下代码,编译器如此翻译
string *ps = new string("Memory Management");
void *memory = // 得到未经处理的内存
operator new(sizeof(string)); // 为String对象
call string::string("Memory Management") //调用构造函数进行初始化
on *memory; // 内存中的对象
string *ps = // 是ps指针指向
static_cast<string*>(memory); // 新的对象
注意第二步包含了构造函数的调用,程序员被禁止这样去做。编译器则没有这个约束。因此如果你想建立一个堆对象就必须用new操作符,不能直接调用构造函数来初始化对象,因为初始化之前必须要先有分配好的内存存在。
总结:new operator 等价于operator new + 初始化
2.Placement new
有没有方法直接调用构造函数?在一个已存在的对象上调用构造函数是没有意义的,因为构造函数用来初始化对象,而一个对象仅仅能在给它初值时被初始化一次。
所以有时你有一些已经被分配但是尚未处理的的(raw)内存,此时可以在这些内存中调用构造函数初始化一个对象。你可以使用一个特殊的operator new函数 ,它被称为placement new。
例:placement new如何使用,考虑一下:
class Widget {
public:
Widget(int widgetSize);
...
};
Widget * constructWidgetInBuffer(void *buffer,
int widgetSize)
{
return new (buffer) Widget(widgetSize);
}//此函数功能相当于在指定内存空间处完成指定大小对象的初始化工作
这个函数返回一个指针,指向一个Widget对象,对象再转递给函数的buffer里分配。当程序使用共享内存或memory-mapped I/O时这个函数可能有用,因为在这样程序里对象必须被放置在一个确定地址上或一块被例程分配的内存里。(参见条款4中的另一个例子)
在constructWidgetInBuffer里面,返回的表达式是:
new (buffer) Widget(widgetSize) //这就是placement new
这初看上去有些陌生,但是它是new操作符的一个用法,需要使用一个额外的变量(buffer),当new操作符隐含调用operator new函数时,把这个变量传递给它。被调用的operator new函数除了待有强制的参数size_t外,还必须接受void*指针参数,指向构造对象占用的内存空间。这 个operator new就是placement new,它看上去象这样:
void * operator new(size_t, void *location)
{
return location;
}
这可能比你期望的要简单,但是这就是placement new需要做的事情。毕竟operator new的目的是为对象分配内存然后返回指向该内存的指针。在使用placement new的情况下,调用者已经获得了指向内存的指针,因为调用者知道对象应该放在哪里。placement new必须做的就是返回转递给它的指针。
说明1:没有用的(但是强制的)参数size_t没有名字,以防止编译器发出警告说它没有被使用;
说明2: placement new是标准C++库的一部分(见Effective C++ 条款49)。为了使用placement new,你必须使用语句#include <new>
(或者编译器不支持新的头文件,使用<new.h>
)
3.这些操作符或函数需要成对使用,不可混合使用
为了避免内存泄漏,每个动态内存分配必须与一个等同相反的deallocation对应。函数operator delete与delete操作符的关系与operator new与new操作符的关系一样。
string *ps;
…
delete ps; // 使用delete 操作符
你的编译器会生成代码来析构对象并释放对象占有的内存。
Operator delete用来释放内存,它被这样声明:
void operator delete(void *memoryToBeDeallocated);
delete ps;导致编译器生成类似于这样的代码:
ps->~string(); // call the object's dtor
operator delete(ps); // deallocate the memory, the object occupied
这有一个隐含的意思是如果你只想处理未被初始化的内存,你应该绕过new和delete操作符,而调用operator new 获得内存和operator delete释放内存给系统:
void *buffer = // 分配足够的
operator new(50*sizeof(char)); // 内存以容纳50个char
//没有调用构造函数
…
operator delete(buffer); // 释放内存
// 没有调用析构函数
这与在C中调用malloc和free等同。
如果你用placement new在内存中建立对象,你应该避免在该内存中用delete操作符。因为delete操作符调用operator delete来释放内存,但是包含对象的内存最初不是被operator new分配的,placement new只是返回转递给它的指针。谁知道这个指针来自何方?而你应该显式调用对象的析构函数来解除构造函数的影响:
// 在共享内存中分配和释放内存的函数
void * mallocShared(size_t size);
void freeShared(void *memory);
void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw = // 如上所示,
constructWidgetInBuffer(sharedMemory, 10); // 使用
// placement new
…
delete pw; // 结果不确定! 共享内存来自
// mallocShared, 而不是operator new
pw->~Widget(); // 正确。 析构 pw指向的Widget,
// 但是没有释放
//包含Widget的内存
freeShared(pw); // 正确。 释放pw指向的共享内存
// 但是没有调用析构函数
如上例所示,如果传递给placement new的raw内存是自己动态分配的(通过一些不常用的方法),如果你希望避免内存泄漏,你必须释放它。(参见我的文章Counting objects里面关于placement delete的注释。)
Arrays
到目前为止一切顺利,但是还得接着走。到目前为止我们所测试的都是一次建立一个对象。怎样分配数组?会发生什么?
string *ps = new string[10]; // allocate an array of objects
被使用的new仍然是new操作符,但是建立数组时new操作符的行为与单个对象建立有少许不同。第一是内存不再用operator new分配,代替以等同的数组分配函数,叫做operator new[](经常被称为array new)。它与operator new一样能被重载。这就允许你控制数组的内存分配,就象你能控制单个对象内存分配一样(但是有一些限制性说明,参见Effective C++ 条款8)。
(operator new[]对于C++来说是一个比较新的东西,所以你的编译器可能不支持它。如果它不支持,无论在数组中的对象类型是什么,全局operator new将被用来给每个数组分配内存。在这样的编译器下定制数组内存分配是困难的,因为它需要重写全局operator new。这可不是一个能轻易接受的任务。缺省情况下,全局operator new处理程序中所有的动态内存分配,所以它行为的任何改变都将有深入和普遍的影响。而且全局operator new有一个正常的签名(normal signature)(也就是单一的参数size_t,参见Effective C++条款9),所以如果你决定用自己的方法声明它,你立刻使你的程序与其它库不兼容(参见条款27)基于这些考虑,在缺乏operator new[]支持的编译器里为数组定制内存管理不是一个合理的设计。)
第二个不同是new操作符调用构造函数的数量。对于数组,在数组里的每一个对象的构造函数都必须被调用:
string *ps = // 调用operator new[]为10个
new string[10]; // string对象分配内存, 然后对每个数组元素调用string对象的缺省构造函数。
同样当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。
就象你能替换或重载operator delete一样,你也替换或重载operator delete[]。在它们重载的方法上有一些限制。请参考优秀的C++教材。(有关优秀的C++教材的信息,参见本书285页的推荐)
new和delete操作符是内置的,其行为不受你的控制,凡是它们调用的内存分配和释放函数则可以控制。当你想定制new和delete操作符的行为时,请记住你不能真的做到这一点。你只能改变它们为完成它们的功能所采取的方法,而它们所完成的功能则被语言固定下来,不能改变。