有时候我们觉得,C++的术语仿佛是要故意让人难以理解似的。
这里就有一个例子:请说明new operator 和operator new 之间的差异(译注:本书所说的new operator,即某些C++教程如C++ Primer 所谓的new expression)
当你写出这样的代码:
string *ps= new string("Memory Management");
你所使用的 new 是所谓的new operator。
这个操作符是由语言内建的,就像sizeof那样,不能被改变意义,总是做相同的事情。
它的动作分为两方面。
- 第一,它分配足够的内存,用来放督某类型的对象。以上例而言,它分配足够放置一个string 对象的内存。
- 第二,它调用一个constructor,为刚才分配的内存中的那个对象设定初值。
new operator总是做这两件事,无论如何你不能够改变其行为。
你能够改变的是用来容纳对象的那块内存的分配行为。也就是上面的第一步
new operator 调用某个函数,执行必要的内存分配动作,你可以重写或重载那个函数,改变其行为。这个函数的名称叫做operator new。头昏了吗?真的,我说的是真的。
函数operator new 通常声明如下:
void * operator new(size_t size);
其返回值类型是void*。此函数返回一个指针,指向一块原始的、未设初值的内存(如果你喜欢,可以写一个新版的operator new,在其返回内存指针之前先将那块内存设定初值。只不过这种行为颇为罕见就是了)。
函数中的size_t 参数表示需要分配多少内存。
你可以将operator new 重载,加上额外的参数,但第一参数的类型必须总是 size_t(如何撰写 operator new,相关信息请参考条款E8~E10)
吸中协从不想到要直接调用operator new,但如果你要,你可以像调用任何其他函数一样地调用它:
void *rawMemory = operator new(sizeof(string));
这里的operator new将返回指针,指向一块足够容纳一个string对象的内存。
和malloc一样,operator new的唯一任务就是分配内存。它不知道什么是constructors, operator new只负责内存分配。
取得 operator new 返回的内存并将之转换为一个对象,是new operator 的责任。
当你的编译器看到这样一个句子:
string *ps= new string("Memory Management");
它必须产生一些代码,或多或少会反映以下行为(见条款E8和条款E10,以及发表于C/C++ Users Journal, April1998 的文章《Counting Objects inC++》中的方块内容)
void *memory =
operator new(sizeof(string));//取得原始内存(raw memory)。用来放置一个string对象。call string::string("Memory Management")//将内存中的对象初始化。
on *memorystring *ps=
static_cast<string*>(memory);//让ps指向新完成的对象。
注意上述第二步骤涉及“调用一个constructor”,身为程序员的你没有权力这么做。
然而你的编译器百无禁忌,可以为所欲为。这就是为什么如果你想要做出一个heap-based object,一定得使用new operator 的原因:你无法直接调用“对象初始化所必需的constructor”
(尤其它可能得为重要成分vtbl设定初值,见条款24)。
Placement new
有时候你真的会想直接调用一个constructor。
针对一个已存在的对象调用其constructor 并无意义,因为 constructors 用来将对象初始化,而对象只能被初始化一次。
但是偶尔你会有一些分配好的原始内存,你需要在上面构建对象。有一个特殊版本的operator new,称为placement new,允许你那么做。
下面示范如何使用 placement new:
class Widget {
public:
widget(int widgetsize);
//...
}widget * constructWidgetInBuffer (void *buffer, int widgetSize)
{
return new (buffer) Widget(widgetsize);
}
此函数返回指针,指向一个widget object,它被构造于传递给此函数的一块内存缓冲区上。
当程序运行到shared memory 或memory-mapped I/O,这类函数可能是有用的,因为在那样的运用中,对象必须置于特定地址,或是置于以特殊函数分配出来的内存上(条款4列有placement new的另一个运用实例)
在 constructWidgetInBuffer 函数内部,唯一一个表达式是,
new (buffer) Widget(widgetSize)
乍见之下有点奇怪,其实不足为奇,这只是new operator 的用法之一,其中指定一个额外自变量(buffer)作为 new operator “隐式调用operator new”时所用。
于是,被调用的operator new除了接受“一定得有的size_t 自变量”之外,还接受了一个void*参数,指向一块内存,准备用来接受构造好的对象。这样的operator new 就是所谓的placement new,看起来像这样:
void * operator new (size_t,void *location)//注意size_t后面没名字
{
return location;
}
似乎比你预期得更简单,但这便是placement new必须做的一切。
毕竟operator new的目的是要为对象找到一块内存,然后返回一个指针指向它。
在placement new的情况下,调用者已经知道指向内存的指针了,因为调用者知道对象应该放在哪里。因此placement new 唯一需要做的就是将它获得的指针再返回。
至于没有用到(但一定得有)的size_t参数,之所以不赋予名称,为的是避免编译器发出“某物未被使用”的警告(见条款6)Placement new 是C++标准程序库(见条款E49)的一部分。
欲使用 placement new,你必须用#include <new>。如果你的编译器尚未支持新式头文件名称的话(见条款E49),就用#include<new.h>。
花几分钟回头想想 placement new,我们便能了解 new operator 和 operator new之间的关系,两个术语虽然表面上令人迷惑,概念上却十分直接易懂。
- 如果你希望将对象产生于heap,请使用 new operator。它不但分配内存而且为该对象调用一个constructor。
- 如果你只是打算分配内存,请调用 operator new,那就没有任何constructor会被调用。
- 如果你打算在heap objects 产生时自己决定内存分配方式,请写一个自己的 operator new,并使用 new operator,它将会自动调用你所写的operator new。
- 如果你打算在已分配(并拥有指针)的内存中构造对象,请使用placement new(若想更深入地了解new和delete,请见条款E7及发表于C/C++Users Journal, April1998的文章《Counting Objects in C++》)
删除(Deletion)与内存释放(Deallocation)
为了避免resource leaks(资源泄漏),每一个动态分配行为都必须匹配一个相应但相反的释放动作。函数operator delete 对于内建的delete operator,就好像operator new对于new operator 一样。当你写出这样的代码:
string *ps;
//。。。
delete ps;// 使用 delete operator。
你的编译器必须产生怎样的代码?
它必须既能够析构 ps所指对象,又能够释放被该对象占用的内存。
内存释放动作是由函数operator delete 执行,通常声明如下:
void operator delete(void *memoryToBeDeallocated);
因此,下面这个动作:
delete ps;
会造成编译器产生近似这样的代码:
ps->~string();// 调用对象的dtoroperator。operator delete (ps);//释放对象所占用的内存。
这里呈现的一个暗示就是,如果你只打算处理原始的、未设初值的内存,应该完全回避new operator 和 delete operators,改调用operator new取得内存并以operator delete 归还给系统:
void *buffer=operator new(50*sizeof(char));//分配足够的内存,放置50个 chars;没有调用任何ctorsoperator delete(buffer);//释放内存,没有调用任何dtors。
这组行为在C++中相当于调用malloc和free。
如果你使用placement new,在某内存块中产生对象,你应该避免对那块内存使用delete opcrator.因为 delete operator会调用 operator delete来释放内存,但是该内存内含的对象最初并非是由 operator new分配得来的。
毕竟placemen new只是返回它所接收的指针而已,谁知道那个指针从哪里来呢?所以为了抵消该对象的constructor的影响,你应该直接调用该对象的destructor:
//以下函数用来分配及释放 shared memory 中的内存。
void* mallocshared(size_t size);
void freeShared(void *memory);void*sharedMemory=mallocShared(sizeof(Widget));//和先前相同,运用Widget*pw=constructWidgetInBuffer(sharedMemory, 10); // placement new。
//..
delete pw;//无定义!因为sharedMemory 来自mallocshared,不是来自 operator new。pw->~Widget();
//可!析构 pw 所指的widget 对象,
//但并未释放widget占用的内存。freeShared(pw);
//可!释放 pw所指的内存,
//不调用任何 destructor。
如此例所示,如果交给placement new 的原始内存(raw memory)本身是动态分配而得(通过某种非传统做法),那么你最终还是得释放那块内存,以免遭受内存泄漏(memory leak)之苦(请参考文章《Counting Objects inC++》之中的方块内容,其中对所谓的“placement delete”有些介绍)。
数组(Arrays)
目前为止一切都好,但我们还有更远的路要走。截至目前我们考虑的每件事情都只在单一对象身上打转。面对数组怎么办?下面会发生什么事情;
string *ps= new string[10);//分配一个对象数组
上述使用的new 仍然是那个new operator,但由于诞生的是数组,所以new operator的行为与先前产生单一对象的情况略有不同。
是的,内存不再以operator new分配,而是由其“数组版”兄弟,一个名为operator new[]的函数负责分配(通常被称为“aray new”)。
和operator new一样,operator new[]也可以被重载。这使你得夺取数组的内存分配权,就像你可以控制单一对象的内存分配一样(不过条款E8对此有些警告)。
operatornew[]是相当晚的时候才加入C++的一个特性,所以你的编译器或许尚未支持它。
如果是这样,全局operator new会被用来为每个数组分配内存一不论数组中的对象类型是什么。
在这样的编译器下定制“数组内存分配行为”很困难,因为你得改写全局版的 operator new才行。这可不是件容易的工作。
默认情况下全局版的operator new负责程序中所有的动态内存分配,所以其行为的任何改变都可能带来剧烈而普遍的影响。此外,全局版本的operator new,其正规形式的型构(eignature)(我的意思是,只有唯一size_t参数的那个,见条款E9)只有一个,所以如果你决定声称它为你所拥有,你的软件便立刻不容于任何做了相同决定的程序库(见条款 27)。
多方考虑之下,如果你面对的是尚未支持 。perator new[]的编译器,定制“数组内存管理行为”往往不是个理想的决定。
“数组版”与“单一对象版”的newoperator的第二个不同是,它所调用的constructor数量。数组版new operator 必须针对数组中的每个对象调用一个constructor:
string *ps =//调用operator new[]以分配足够容纳new string[10];//10个 string 对象的内存,然后
//针对每个元素调用 string default ctor。
同样道理,当delete operator 被用于数组,它会针对数组中的每个元素调用其 destructor,然后再调用 operator delete[]释放内存:
delete [] ps;
//为数组中的每个元素调用 string dtor.
然后调用 operator delete[]以释放内存。
就好像你可以取代或重载 operator delete一样,你也可以取代或重载operator delete[]。不过两者的重载有着相同的限制。请你找一本好的C++教程,查阅其细节。说到好的C++教程,本书p285列有我的一份推荐名单。
现在,你有了完整的知识。
new operator 和delete operator 都是内建操作符,无法为你所控制,但是它们所调用的内存分配/释放函数则不然。
当你想要定制new operator 和 delete operator的行为,记住,你其实无法真正办到。你可以修改它们完成任务的方式,至于它们的任务,已经被语言规范固定死了。