C++ 中 new 操作符内幕:new operator、operator new、placement new
一、new 操作符(new operator)
人们有时好像喜欢有益使C++语言的术语难以理解。比方说new操作符(new operator)和operator new的差别。
当你写这种代码:
string *ps = new string("Memory Management");
你使用的new是new操作符。
这个操作符就象sizeof一样是语言内置的。你不能改变它的含义,它的功能总是一样的。它要完毕的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。
第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以不论什么方式改变它的行为。
(总结就是,new操作符做两件事,分配内存+调用构造函数初始化。
你不能改变它的行为。)
二、operator new
你所能改变的是怎样为对象分配内存。
new操作符调用一个函数来完毕必需的内存分配,你可以重写或重载这个函数来改变它的行为。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的职责仅仅是分配内存。
它对构造函数一无所知。operator new所了解的是内存分配。把operator new 返回的未经处理的指针传递给一个对象是new操作符的工作。当你的编译器遇见这种语句:
string *ps = new string("Memory Management");
它生成的代码或多或少与以下的代码相似(很多其它的细节见Effective C++条款8和条款10。还有我的文章Counting object里的凝视。):
<pre name="code" class="cpp">void *memory = operator new(sizeof(string)); // 得到未经处理的内存,为String对象
call string::string("Memory Management")
on *memory; // 内存中的对象
string *ps = static_cast<string*>(memory); // 使ps指针指向新的对象
注意第二步包括了构造函数的调用,你做为一个程序猿被禁止这样去做。你的编译器则没有这个约束,它能够做它想做的一切。
因此假设你想建立一个堆对象就必须用new操作符。不能直接调用构造函数来初始化对象。(总结:operator new是用来分配内存的函数,为new操作符调用。能够被重载(有限制))
三、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,一个怎样使用placement new的一个不同样例。)
在constructWidgetInBuffer里面。返回的表达式是: new (buffer) Widget(widgetSize)
这初看上去有些陌生,可是它是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必须做的就是返回转递给它的指针。(没实用的(可是强制的)參数size_t没有名字,以防止编译器发出警告说它没有被使用。见条款6。
) placement new是标准C++库的一部分。为了使用placement new。你必须使用语句#include <new>(或者假设你的编译器还不支持这新风格的头文件名称)。
(总结: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)
释放对象数组:delete与delete[]
Account *parray=new Account[100];
delete parray;
delete [] parray;
方括号的存在会使编译器获取数组大小(size)然后析构函数再被依次应用在每个元素上,一共size次。否则,只有一个元素被析构。
无论哪种情况,分配的全部空间被返还给自由存储区。
我的问题是:为什么无论哪种情况,分配的全部空间被返还给自由存储区?
对于delete parray,为什么不是删除单个Account元素,而是删除了100个.
编译器怎么知道parray这个指针实际指向的是数组还是单个元素,即便知道指向的是自由存储区的数组,这个数组的大小又怎么知道。
难道是编译器辅助行为?
总结:空间释放(肯定有个log记录分配的大小)和调用析构函数(类型识别,不同的编译器实现不同)采用不同的机制.
(1)一般在分配时分配器会自动写一个日志(一般在分配使用得内存之前又一个结构),用于记录分配的大小,分配内容的sizeof等等。
直观得想想,delete和delete[]都是传入一个void*如果不保存日志就无法知道分配时到底是分配了一个还是多个单元.
所以虽然delete和delete[]不同但是分配器在执行释放过程中都会读取这个日志,从而了解到底应该释放多少内存,但是从程序员的角度来说,既然分配了数组,就应该用delete[]
(2)在VC下用汇编跟过delete[]的执行情况,发现这个 "日志 "就是一个4字节长的整数记录数组元素个数,紧挨在数组第一个元素之前.
但是有个前提:对象类型(或其基类)有显式析构函数.换句话说,析构函数是非trivial的.
否则的话,数组前面是没有这个日志的.其实对于析构函数是trivial的情况,delete[]时无需调用其析构函数,因此此时VC把delete[]当做delete同样处理.
(3)因为释放数组空间和为数组调用析构函数是两个独立的部分,可以使用不同的机制来实现。
释放空间的机制是需要绝对保证的。因此,即使你不写delete[],它也会将所有空间释放,其机制可以是前置的长度信息,也可以不是(如后置的特征分割符等等)。
而调用析构函数可以一般采用前置长度信息的方式(当然也可以有其他方式)。在没有[]提示时,编译器在调用析构就将它当一个元素,而不会使用数组方式来调用每一个析构函数了。
LS:“但是有个前提:对象类型(或其基类)有显式析构函数.换句话说,析构函数是非trivial的. 否则的话,数组前面是没有这个日志的.”
——这说明,LS使用的编译器在释放数组空间时,并没有用前置的长度信息的方式。由此可见,释放数组空间和为数组调用析构函数确实可以使用不同的机制
(4)delete parray,编译器得到类型信息是Account单个的指针,那么释放时,只调用一次析构函数。
delete[] parray,编译器得到的类型信息是Account[]类型,则按照Account数组来处理,依次调用每个元素的析构函数。
注意,以上是在编译期间就确定下来的,编译器识别到类型信息的不同会决定调用析构函数的情况有不同。
但是对于内存释放,delete操作则不是通过类型信息来确定分配的内存大小,那么内存大小的信息从什么地方得到呢?
当我们使用operator new为一个自定义类型对象分配内存时,实际上我们得到的内存要比实际对象的内存大一些,这些内存除了要存储对象数据外,还需要记录这片内存的大小,此方法称为 cookie。这一点上的实现依据不同的编译器不同。(例如 MFC 选择在所分配内存的头部存储对象实际数据,而后面的部分存储边界标志和内存大小信息。g++ 则采用在所分配内存的头4个自己存储相关信息,而后面的内存存储对象实际数据。)当我们使用 delete operator 进行内存释放操作时,delete operator 就可以根据这些信息正确的释放指针所指向的内存块。
对于parray指针,可以根据这样的cookie信息来得到指向内存空间的大小,delete parray和delete[] parray都是一样的,同样一个指针,cookie信息是相同的,所以对应的内存都会被释放掉。但是由于编译器理解两种情况下的类型是不同的,所以调用析构函数会有不同。
(5)难道是编译器辅助行为?
没错,就是。不同的编译器可能采用的具体方法有可能不一样,但不管采用什么方法,编译器必须记住那块大小。
(6)转自<<effective c++>>
条款5:对应的new和delete要采用相同的形式
下面的语句有什么错?
string *stringarray = new string[100];
delete stringarray;
一切好象都井然有序——一个new对应着一个delete——然而却隐藏着很大的错误:程序的运行情况将是不可预测的。至少,stringarray指向的100个string对象中的99个不会被正确地摧毁,因为他们的析构函数永远不会被调用。
用new的时候会发生两件事。首先,内存被分配(通过operator new 函数,详见条款7-10和条款m8),然后,为被分配的内存调用一个或多个构造函数。用delete的时候,也有两件事发生:首先,为将被释放的内存调用一个或多个析构函数,然后,释放内存(通过operator delete 函数,详见条款8和m8)。对于 delete来说会有这样一个重要的问题:内存中有多少个对象要被删除?答案决定了将有多少个析构函数会被调用。
这个问题简单来说就是:要被删除的指针指向的是单个对象呢,还是对象数组?这只有你来告诉delete。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组:
string *stringptr1 = new string;
string *stringptr2 = new string[100];
...
delete stringptr1;// 删除一个对象
delete [] stringptr2;// 删除对象数组
如果你在stringptr1前加了"[]"会怎样呢?答案是:那将是不可预测的;
如果你没在stringptr2前没加上"[]"又会怎样呢?答案也是:不可预测。
int这样的固定类型来说,结果也是不可预测的,即使这样的类型没有析构函数。所以,解决这类问题的规则很简单:如果你调用new时用了[],调用delete时也要用[]。如果调用new时没有用[],那调用delete时也不要用[]。