C++中new和delete运算符详解

 

本文内容摘自More Effective C++(Scott Meyers 著)一书,详细解释了new和delete操作符的用法。

一、new 操作符(new operator)

人们有时好像喜欢有意使C++语言的术语难以理解。比方说new操作符(new operator)和operator new的差别。 

当你写这种代码:

string *ps = new string("Memory Management");

你使用的new是new操作符

这个操作符就像sizeof运算符一样是语言内置的。你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以不论什么方式改变它的行为。
(总结就是,new操作符做两件事,分配内存+调用构造函数初始化,你不能改变它的行为。)
 

二、operator new
 

你所能改变的是怎样为对象分配内存

new operator调用一个函数来完成必要的内存分配,你可以重写或重载这个函数来改变它的行为。new操作符为分配内存所调用函数的名字operator new

函数operator new 通常这样声明:

void * operator new(size_t size);

返回值类型是void*,由于这个函数返回一个未经处理(raw)的指针。未初始化的内存。(如果你喜欢,你能写一个新版的operator new函数,在其返回内存指针之前先将那块内存设定初值,只不过这种行为很罕见)。參数size_t表示需要分配多少内存。你可以重载函数operator new,添加额外的參数,但第一个參数类型必须是size_t。(有关operator new很多其它的信息參见Effective C++ 条款8至条款10。)

或许你从未想到要直接调用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");

它必须产生一些代码,或多或少会反映一下行为(很多其它的细节见Effective C++条款8和条款10。还有我的文章Counting object in C++中的方块内容。):

void *memory = operator new(sizeof(string)); // 得到原始内存(raw memory),为String对象
call string::string("Memory Management") 
on *memory; // 将内存中的对象初始化

string *ps = static_cast(memory); // 使ps指针指向新的对象注意第二步涉及constructor的调用,身为一个程序猿没有权利这样做。你的编译器则没有这个约束,它能够做它想做的一切。

这就是为什么如果你想做出一个堆对象就必须用new operator的原因:无法直接调用“对象初始化所必需的constructor”。

(总结:operator new是用来分配内存的函数,为new操作符调用。能够被重载(有限制))

三、placement new

有时你确实想直接调用constructor。针对一个已存在的对象调用其构造函数是没有意义的,因为构造函数用来初始化对象,而对象只能在给它初值时被初始化一次。

可是偶尔你会有一些分配好的原始(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对象,它被构造于传递给此函数的一块内存缓冲区上。

当程序使用共享内存或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)
{
 return location;
}

似乎比你预期的要简单,但这便是placement new必须做的一切。毕竟operator new的目的是要为对象找到一块内存,然后返回一个指针指向它。在placement new的情况下,调用者已经知道指向内存的指针了,因为调用者知道对象应该放在哪里。因此placement new唯一要做的就是将它获得的指针再返回。至于没有用到(但一定得有)的size_t参数,之所以不赋予名称,为的是避免编译器发出“某物未被使用”的警告。placement new是C++标准库的一部分。欲使用placement new,你必须使用语句#include (或者假设你的编译器还不支持这新风格的头文件名称,就使用#Include)。

(总结:placement new是一种特殊的operator new,作用于一块已分配但未处理或未初始化的raw内存)

四、小结

让我们从placement new回来片刻,看看new操作符(new operator)与operator new的关系,(new操作符调用operator new)

  • 你想在堆上建立一个对象,应该用new操作符。它既分配内存又为对象调用构造函数。
  • 假设你只想分配内存,就应该调用operator new函数;它不会调用构造函数。
  • 假设你想定制自己的在堆对象被建立时的内存分配过程,你应该写你自己的operator new函数。然后使用new操作符,new操作符会调用你定制的operator new。
  • 假设你想在一块已经获得指针的内存里建立一个对象。应该用placement new。
     

五、Deletion and Memory Deallocation 

为了避免内存泄漏,每一个动态内存分配必须与一个等同相反的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等同。

2.placement new建立的对象怎样释放?

假设你用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的凝视。)

六、数组

到眼下为止一切顺利。可是还得接着走。

到眼下为止我们所測试的都是一次建立一个对象。

如何分配数组?会发生什么?

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)。所以假设你 决定用自己的方法声明它,你立马使你的程序与其他库不兼容基于这些考虑,在缺乏operator new[]支持的编译器里为数组定制内存管理不是一个合理的设计。)

第二个不同是new操作符调用构造函数的数量。对于数组,在数组里的每个对象的构造函数都必须被调用:

string *ps = new string[10]; // 调用operator new[]为10个string对象分配内存,

// 然后对每一个数组元素调用string对象的缺省构造函数。

相同当delete操作符用于数组时,它为每一个数组元素调用析构函数,然后调用operator delete来释放内存。(buxizhizhou530注:这里应该是operator delete[]吧)

就象你能替换或重载operator delete一样,你也替换或重载operator delete[]。

在它们重载的方法上有一些限制。

请參考优秀的C++教材。

(总结:数组时,两个不同点,一时调用operator new[]函数,二是new操作符调用构造函数的数量不同。)

七、总结

new和delete操作符是内置的,其行为不受你的控制。凡是它们调用的内存分配和释放函数则能够控制。当你想定制new和delete操作符的行为时,请记住你不能真的做到这一点。你仅仅能改变它们为完毕它们的功能所採取的方法,而它们所完毕的功能则被语言固定下来。不能改变。(You can modify how they do what they do, but what they do is fixed by the language)

相关:

C++中的new、operator new与placement new  有额外补充

C++中placement new操作符(经典)  一般。可略看

你可能感兴趣的:(#,C++,C++,new,delete)