在 C++ 中,你也许经常使用 new 和 delete 来动态申请和释放内存,但你可曾想过以下问题呢?
- new 和 delete 是函数吗?
- new [] 和 delete [] 又是什么?什么时候用它们?
- 你知道 operator new 和 operator delete 吗?
- 为什么 new [] 出来的数组有时可以用 delete 释放有时又不行?
- …
如果你对这些问题都有疑问的话,不妨看看我这篇文章。
new 和 delete 到底是什么?
如果找工作的同学看一些面试的书,我相信都会遇到这样的题:sizeof 不是函数,然后举出一堆的理由来证明 sizeof 不是函数。在这里,和 sizeof 类似,new 和 delete 也不是函数,它们都是 C++ 定义的关键字,通过特定的语法可以组成表达式。和 sizeof 不同的是,sizeof 在编译时候就可以确定其返回值,new 和 delete 背后的机制则比较复杂。
继续往下之前,请你想想你认为 new 应该要做些什么?也许你第一反应是,new 不就和 C 语言中的 malloc 函数一样嘛,就用来动态申请空间的。你答对了一半,看看下面语句:
string *ps = new string("hello world");
你就可以看出 new 和 malloc 还是有点不同的,malloc 申请完空间之后不会对内存进行必要的初始化,而 new 可以。所以 new expression 背后要做的事情不是你想象的那么简单。在我用实例来解释 new 背后的机制之前,你需要知道 operator new
和 operator delete
是什么玩意。
operator new 和 operator delete
这两个其实是 C++ 语言标准库的库函数,原型分别如下:
void *operator new(size_t); //allocate an object
void *operator delete(void *); //free an object
void *operator new[](size_t); //allocate an array
void *operator delete[](void *); //free an array
后面两个你可以先不看,后面再介绍。前面两个均是 C++ 标准库函数,你可能会觉得这是函数吗?请不要怀疑,这就是函数!C++ Primer 一书上说这不是重载 new 和 delete 表达式(如 operator=
就是重载 = 操作符),因为 new 和 delete 是不允许重载的。但我还没搞清楚为什么要用 operator new 和 operator delete 来命名,比较费解。我们只要知道它们的意思就可以了,这两个函数和 C 语言中的 malloc 和 free 函数有点像了,都是用来申请和释放内存的,并且 operator new 申请内存之后不对内存进行初始化,直接返回申请内存的指针。
我们可以直接在我们的程序中使用这几个函数。
new 和 delete 背后机制
知道上面两个函数之后,我们用一个实例来解释 new 和 delete 背后的机制:
我们不用简单的 C++ 内置类型来举例,使用复杂一点的类类型,定义一个类 A:
class A
{
public:
A(int v) : var(v)
{
fopen_s(&file, "test", "r");
}
~A()
{
fclose(file);
}
private:
int var;
FILE *file;
};
很简单,类 A 中有两个私有成员,有一个构造函数和一个析构函数,构造函数中初始化私有变量 var 以及打开一个文件,析构函数关闭打开的文件。
我们使用
来创建一个类的对象,返回其指针 pA。如下图所示 new 背后完成的工作:
id="iframe_0.27411716617643833" src="data:text/html;charset=utf8,%3Cimg%20id=%22img%22%20src=%22https://github-camo.global.ssl.fastly.net/48182649501319d27121ede912b19be491b1b0a2/687474703a2f2f692e696d6775722e636f6d2f454a736e6b4a342e6a7067?_=0.28783797565847635%22%20style=%22border:none;max-width:911px%22%3E%3Cscript%3Ewindow.onload%20=%20function%20()%20%7Bvar%20height%20=%20document.getElementById('img').height;window.parent.postMessage(%7BiframeId:'iframe_0.27411716617643833',height:height%7D,%20'http://www.cnblogs.com');%7D%3C/script%3E" frameborder="0" scrolling="no" height="844" style="margin: 0px; padding: 0px; border-style: none; border-width: initial; width: 911px;">
简单总结一下:
- 首先需要调用上面提到的 operator new 标准库函数,传入的参数为 class A 的大小,这里为 8 个字节,至于为什么是 8 个字节,你可以看看《深入 C++ 对象模型》一书,这里不做多解释。这样函数返回的是分配内存的起始地址,这里假设是 0x007da290。
- 上面分配的内存是未初始化的,也是未类型化的,第二步就在这一块原始的内存上对类对象进行初始化,调用的是相应的构造函数,这里是调用
A:A(10);
这个函数,从图中也可以看到对这块申请的内存进行了初始化,var=10, file 指向打开的文件
。 - 最后一步就是返回新分配并构造好的对象的指针,这里 pA 就指向 0x007da290 这块内存,pA 的类型为类 A 对象的指针。
所有这三步,你都可以通过反汇编找到相应的汇编代码,在这里我就不列出了。
好了,那么 delete 都干了什么呢?还是接着上面的例子,如果这时想释放掉申请的类的对象怎么办?当然我们可以使用下面的语句来完成:
delete 所做的事情如下图所示:
id="iframe_0.5878946108277887" src="data:text/html;charset=utf8,%3Cimg%20id=%22img%22%20src=%22https://github-camo.global.ssl.fastly.net/8bfb4701b5c270dc3b27c86bce57f2d31562d82f/687474703a2f2f692e696d6775722e636f6d2f3156767239467a2e6a7067?_=0.7471509999595582%22%20style=%22border:none;max-width:911px%22%3E%3Cscript%3Ewindow.onload%20=%20function%20()%20%7Bvar%20height%20=%20document.getElementById('img').height;window.parent.postMessage(%7BiframeId:'iframe_0.5878946108277887',height:height%7D,%20'http://www.cnblogs.com');%7D%3C/script%3E" frameborder="0" scrolling="no" height="469" style="margin: 0px; padding: 0px; border-style: none; border-width: initial; width: 911px;">
delete 就做了两件事情:
- 调用 pA 指向对象的析构函数,对打开的文件进行关闭。
- 通过上面提到的标准库函数 operator delete 来释放该对象的内存,传入函数的参数为 pA 的值,也就是 0x007d290。
好了,解释完了 new 和 delete 背后所做的事情了,是不是觉得也很简单?不就多了一个构造函数和析构函数的调用嘛。
如何申请和释放一个数组?
我们经常要用到动态分配一个数组,也许是这样的:
string *psa = new string[10]; //array of 10 empty strings
int *pia = new int[10]; //array of 10 uninitialized ints
上面在申请一个数组时都用到了 new []
这个表达式来完成,按照我们上面讲到的 new 和 delete 知识,第一个数组是 string 类型,分配了保存对象的内存空间之后,将调用 string 类型的默认构造函数依次初始化数组中每个元素;第二个是申请具有内置类型的数组,分配了存储 10 个 int 对象的内存空间,但并没有初始化。
如果我们想释放空间了,可以用下面两条语句:
delete [] psa;
delete [] pia;
都用到 delete []
表达式,注意这地方的 [] 一般情况下不能漏掉!我们也可以想象这两个语句分别干了什么:第一个对 10 个 string 对象分别调用析构函数,然后再释放掉为对象分配的所有内存空间;第二个因为是内置类型不存在析构函数,直接释放为 10 个 int 型分配的所有内存空间。
这里对于第一种情况就有一个问题了:我们如何知道 psa 指向对象的数组的大小?怎么知道调用几次析构函数?
这个问题直接导致我们需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
还是用图来说明比较清楚,我们定义了一个类 A,但不具体描述类的内容,这个类中有显示的构造函数、析构函数等。那么 当我们调用
时需要做的事情如下:
id="iframe_0.2150536400731653" src="data:text/html;charset=utf8,%3Cimg%20id=%22img%22%20src=%22https://github-camo.global.ssl.fastly.net/1a2da83d54ac0110f8cdb13a6645dd5f9240e760/687474703a2f2f692e696d6775722e636f6d2f366857304431702e6a7067?_=0.41488513606600463%22%20style=%22border:none;max-width:911px%22%3E%3Cscript%3Ewindow.onload%20=%20function%20()%20%7Bvar%20height%20=%20document.getElementById('img').height;window.parent.postMessage(%7BiframeId:'iframe_0.2150536400731653',height:height%7D,%20'http://www.cnblogs.com');%7D%3C/script%3E" frameborder="0" scrolling="no" height="501" style="margin: 0px; padding: 0px; border-style: none; border-width: initial; width: 911px;">
从这个图中我们可以看到申请时在数组对象的上面还多分配了 4 个字节用来保存数组的大小,但是最终返回的是对象数组的指针,而不是所有分配空间的起始地址。
这样的话,释放就很简单了:
id="iframe_0.18718571006320417" src="data:text/html;charset=utf8,%3Cimg%20id=%22img%22%20src=%22https://github-camo.global.ssl.fastly.net/db76cce60aaa6f0ad3ab65cd00b0ca26a5271547/687474703a2f2f692e696d6775722e636f6d2f3155425444316c2e6a7067?_=0.9617116556037217%22%20style=%22border:none;max-width:911px%22%3E%3Cscript%3Ewindow.onload%20=%20function%20()%20%7Bvar%20height%20=%20document.getElementById('img').height;window.parent.postMessage(%7BiframeId:'iframe_0.18718571006320417',height:height%7D,%20'http://www.cnblogs.com');%7D%3C/script%3E" frameborder="0" scrolling="no" height="559" style="margin: 0px; padding: 0px; border-style: none; border-width: initial; width: 911px;">
这里要注意的两点是:
- 调用析构函数的次数是从数组对象指针前面的 4 个字节中取出;
- 传入
operator delete[]
函数的参数不是数组对象的指针 pAa,而是 pAa 的值减 4。
为什么 new/delete 、new []/delete[] 要配对使用?
其实说了这么多,还没到我写这篇文章的最原始意图。从上面解释的你应该懂了 new/delete、new[]/delete[] 的工作原理了,因为它们之间有差别,所以需要配对使用。但偏偏问题不是这么简单,这也是我遇到的问题,如下这段代码:
int *pia = new int[10];
delete []pia;
这肯定是没问题的,但如果把 delete []pia;
换成 delete pia;
的话,会出问题吗?
这就涉及到上面一节没提到的问题了。上面我提到了在 new []
时多分配 4 个字节的缘由,因为析构时需要知道数组的大小,但如果不调用析构函数呢(如内置类型,这里的 int 数组)?我们在 new []
时就没必要多分配那 4 个字节, delete [] 时直接到第二步释放为 int 数组分配的空间。如果这里使用 delete pia;
那么将会调用 operator delete
函数,传入的参数是分配给数组的起始地址,所做的事情就是释放掉这块内存空间。不存在问题的。
这里说的使用 new []
用 delete 来释放对象的提前是:对象的类型是内置类型或者是无自定义的析构函数的类类型!
我们看看如果是带有自定义析构函数的类类型,用 new []
来创建类对象数组,而用 delete 来释放会发生什么?用上面的例子来说明:
class A *pAa = new class A[3];
delete pAa;
那么 delete pAa;
做了两件事:
- 调用一次 pAa 指向的对象的析构函数;
- 调用
operator delete(pAa);
释放内存。
显然,这里只对数组的第一个类对象调用了析构函数,后面的两个对象均没调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你却在销毁数组对象时少调用了析构函数,这会造成内存泄漏。
上面的问题你如果说没关系的话,那么第二点就是致命的了!直接释放 pAa 指向的内存空间,这个总是会造成严重的段错误,程序必然会奔溃!因为分配的空间的起始地址是 pAa 指向的地方减去 4 个字节的地方。你应该传入参数设为那个地址!
同理,你可以分析如果使用 new 来分配,用 delete []
来释放会出现什么问题?是不是总会导致程序错误?
总的来说,记住一点即可:new/delete、new[]/delete[] 要配套使用总是没错的!
二. 简介
new有三种使用方式:plain new,nothrow new和placement new。
(1)plain new顾名思义就是普通的new,就是我们惯常使用的new。在C++中是这样定义的:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
提示:plain new在分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL,因此通过判断返回值是否为NULL是徒劳的。
(2)nothrow new是不抛出异常的运算符new的形式。nothrow new在失败时,返回NULL。定义如下:
void * operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();
(3)placement new意即“放置”,这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:
void* operator new(size_t,void*);
void operator delete(void*,void*);
提示1:palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组。
提示2:placement new构造起来的对象或其数组,要显示的调用他们的析构函数来销毁,千万不要使用delete。
char* p = new(nothrow) char[100];
long *q1 = new(p) long(100);
int *q2 = new(p) int[100/sizeof(int)];
三.实例
1.plain new/delete.普通的new
定义如下:
void *operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
注:标准C++ plain new失败后抛出标准异常std::bad_alloc而非返回NULL,因此检查返回值是否为NULL判断分配是否成功是徒劳的。
测试程序:
#include "stdafx.h"
#include <iostream>
using namespace std;
char *GetMemory(unsigned long size)
{
char *p=new char[size];//分配失败,不是返回NULL
return p;
}
int main()
{
try
{
char *p=GetMemory(10e11);// 分配失败抛出异常std::bad_alloc
//...........
if(!p)//徒劳
cout<<"failure"<<endl;
delete [] p;
}
catch(const std::bad_alloc &ex)
{
cout<<ex.what()<<endl;
}
return 0;
}
2.nothrow new/delete不抛出异常的运算符new的形式,new失败时返回NULL。
定义如下:
void *operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();
struct nothrow_t{}; const nothrow_t nothrow;//nothrow作为new的标志性哑元
测试程序:
#include "stdafx.h"
#include <iostream>
#include <new>
using namespace std;
char *GetMemory(unsigned long size)
{
char *p=new(nothrow) char[size];//分配失败,是返回NULL
if(NULL==p)
cout<<"alloc failure!"<<endl;
return p;
}
int main()
{
try
{
char *p=GetMemory(10e11);
//...........
if(p==NULL)
cout<<"failure"<<endl;
delete [] p;
}
catch(const std::bad_alloc &ex)
{
cout<<ex.what()<<endl;
}
return 0;
}
3.placement new/delete 主要用途是:
反复使用一块较大的动态分配成功的内存来构造不同类型的对象或者它们的数组。例如可以先申请一个足够大的字符数组,然后当需要时在它上面构造不同类型的对象或数组。placement new不用担心内存分配失败,因为它根本不分配内存,它只是调用对象的构造函数。
测试程序:
#include "stdafx.h"
#include <iostream>
#include <new>
using namespace std;
class ADT
{
int i;
int j;
public:
ADT()
{
}
~ADT()
{
}
};
int main()
{
char *p=new(nothrow) char[sizeof(ADT)+2];
if(p==NULL)
cout<<"failure"<<endl;
ADT *q=new(p) ADT; //placement new:不必担心失败
// delete q;//错误!不能在此处调用delete q;
q->ADT::~ADT();//显示调用析构函数
delete []p;
return 0;
}
注:
使用placement new构造起来的对象或数组,要显式调用它们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete.这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。
四、new创建类对象与不new区别
下面是自己总结的一些关于new创建类对象特点:
- new创建类对象需要指针接收,一处初始化,多处使用
- new创建类对象使用完需delete销毁
- new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间
- new对象指针用途广泛,比如作为函数返回值、函数参数等
- 频繁调用场合并不适合new,就像new申请和释放内存一样
五、new创建类对象实例
1、new创建类对象例子:
CTest* pTest = new CTest();
delete pTest;
pTest用来接收类对象指针。
不用new,直接使用类定义申明:
CTest mTest;
此种创建方式,使用完后不需要手动释放,该类析构函数会自动执行。而new申请的对象,则只有调用到delete时再会执行析构函数,如果程序退出而没有执行delete则会造成内存泄漏。
2、只定义类指针
这跟不用new申明对象有很大区别,类指针可以先行定义,但类指针只是个通用指针,在new之前并为该类对象分配任何内存空间。比如:
CTest* pTest = NULL;
但使用普通方式创建的类对象,在创建之初就已经分配了内存空间。而类指针,如果未经过对象初始化,则不需要delete释放。
最后注意:普通方式创建的类对象在出了对象定义的范围后自动析构
new创建的类对象除非进程结束或显示调用delete释放
六、理解C++中new背后的行为
1:C++标准说:An allocation function shall be a class member function or a global function; a program is ill-formed if an allocation function is declared in a namespace scope other than global scope or declared static in global scope.
必须是全局函数或类成员函数,而不能是全局之外的名字空间或static全局函数。
2:new operator的行为
Foo* p = new Foo;
...
delete p;
我们知道,上面的代码,也就是C++中的new操作符(new operator)大致会做下面的事情:
a.调用operator new, 给对象分配内存
b.调用Foo的构造函数
c.返回指针
...
d. 调用Foo的析构函数~Foo()
e. 调用operator delete释放内存
更具体的, new operator的行为如下:
对于如下代码:
Foo* p = new(arg1,arg2,…… ) Foo(para1, para2, ...);
...
delete p;
编译器将生成如下代码:
调用 p = operator new( size_t 需要的大小,arg1,arg2,…… ); // 分配内存,这里有可能抛出std::bad_alloc,但无须在new operator中捕捉
如果构造Foo没有抛出异常 // 即Foo的构造函数后面显式的声明了 throw()
在p指向处构造foo(para1,para2,……); // 调用Foo的构造函数
return p;
否则
try
{
在p指向处构造Foo(para1,para2,……);
return p;
}
catch(...)
{
调用 operator delete( void* p, arg1,arg2,…… );
throw;
}
...
调用 Foo的析构函数~Foo();
调用 operator delete( void* p );
从上面的步骤可以看出:
(1)对于operator new, 我们只要确保第一参数是表示申请内存的大小, 其他参数可以自己随意重载
(2)只有Foo构造失败(构造函数内抛出异常),我们的
operator delete( void* p, arg1,arg2,…… )才会被调用,否则只会调用
operator delete( void* p )
3:全局形式的operator new伪代码:
void* operator new( size_t size ) // 包括其他形式
{
if( 0 == size ) // 须要注意
size = 1;
while(1)
{
分配size字节内存;
if(分配成功)
return 指向内存的指针;
new_handler globalhandler = set_new_handler(0);
set_new_handler(globalhandler);
if( globalhandler )
(*globalhandler)();
else
throw std::bad_alloc();
}
}
void operator delete( void* raw )
{
if( 0 == raw ) // 须要注意
return;
...
}
须要说明的是,编译器本身就隐含着一个 void* operator new( size_t ),所以重载全局operator new必须加其他参数以示区别。
一般重载分配函数时都会重载三个,分别是 void* operator new( size_t, …… ),void operator delete( void*, …… ),以及一般形式的 void operator delete( void* )。
4. set_new_handler的作用
set_new_handler设置一个函数,此函数将在分配内存失败时被调用,见3中的代码。
从3中的代码还能看得出,new_handler必须有主动退出的功能,否则就会导致operator new内部死循环。因此newhandler的一般形式是:
void mynewhandler()
{
if( 有可能使得operator new成功(比如释放部分内存) )
{
做有可能使得operator new成功的事
return;
}
// 主动退出
或 abort/exit 直接退出程序
或 set_new_handler(其他newhandler);
或 set_new_handler(0)
或 throw bad_alloc()或派生类 // 这一种比较好,不粗鲁的关闭程序,也不更改其他设置
}
须要说明的是,没有类形式的set_new_handler,但这也无所谓,你可以自己写。(见《Effective C++ 2e》条款7)
5. 类形式的operator new伪代码:
struct base
{
...
static void* operator new( size_t size );
static void operator delete( void* raw );
};
void* base::operator new( size_t size )
{
if( sizeof(base) != size ) // 须要注意
return ::operator new(size);
类似于3 // 注意“没有类形式的set_new_handler”
}
void base::operator delete( void* raw )
{
if( sizeof(base) != size ) // 须要注意
{
::operator delete(raw);
return;
}
同3
}
6. operator new的函数类型:
对我们来说一般有3种是语言要求的标准operator new(plain new, nothrow new, placement new):
void *operator new(std::size_t count)
throw(std::bad_alloc); //一般的版本(plain new)
void *operator new(std::size_t count,
const std::nothrow_t&) throw(); //
兼容早版本, new
内存分配失败不会抛出异常(
nothrow new
)
void *operator new(std::size_t count, void *ptr) throw();
//placement版本(
placement new
)
上面的方法我们可以这样调用:
Foo* p = new Foo;
delete p;
Foo* p1 = new(std::nothrow) Foo;
delete p1;
Foo f;
Foo* p2 = new(&f) Foo;
p2->~Foo();
针对数组则是:
void *operator new[](std::size_t count)
throw(std::bad_alloc);
void *operator new[](std::size_t count,
const std::nothrow_t&) throw();
void *operator new[](std::size_t count, void *ptr) throw();
可以看到上面函数第一个都是对象空间大小,除了重载C++中operator new的标准类型,另外我们也可以重载其他类型的operator new, 比如
void *operator new(std::size_t count, const string& s)
throw(std::bad_alloc);
void *operator new[](std::size_t count, const string& s) throw(std::bad_alloc);
然后就可以这样调用了: string str("abc"); Foo* p = new(str) Foo;
当然,如果我们自己重写了operator new, 最好我们也重写operator delete,这样如果我们的构造函数里抛出异常,我们自己重写的operator delete会被调用。
(当然,如果构造对象成功,最后delete时只会调用
operator delete( void* p )
)
比如针对上面新加的operator new函数,新加operator delete如下:
void operator delete(void* p, const string& s) throw();
void operator delete[](void* p, const string& s) throw();
可以看到,自己新加的operator delete, 只需确保第一个参数内存指针。
7. new operator和operator new的区别
new operator就象sizeof一样是语言内置的,我们不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new operator总是做这两件事情,你不能以任何方式改变它的行为。
我们所能改变的是如何为对象分配内存。new operator调用一个函数来完成必需的内存分配,你能够重写或重载这个函数来改变它的行为。new operator为分配内存所调用函数的名字是operator new。
如果想在堆上建立一个对象,应该用new operator。它既分配内存又为对象调用构造函数。如果你仅仅想分配内存,就应该调用operator new函数;它不会调用构造函数。如果你想定制自己的在堆对象被建立时的内存分配过程,你应该写你自己的operator new函数,然后使用new operator,new operator会调用你定制的operator new。如果你想在一块已经获得指针的内存里建立一个对象,应该用placement new。
与new operator/operator new相对应的是delete operator/operator delete, 当我们调用delete operator时,实际上包含析构函数调用和通过operator delete释放内存2个阶段。
我们可以单纯的通过operator new 和 operator delete来分配和释放内存:
void *buffer = operator new(50*sizeof(char)); // 内存以容纳50个char, 没有调用构造函数
...
operator delete(buffer); // 释放内存, 没有调用析构函数
8. operator new的一些原则:
a. 一般不要重写全局的operator new, 具体可以参考 不要重载全局 ::operator new
b. 如果重载了operator new, 同时提供所有版本(plain new, nothrow new, placement new)
c. 成对的提供new和delete, 即如果重载了operator new, 同时重载operator delete
七、从汇编角度看new和delete的背后行为
我们的代码很简单, 如下:
#include <iostream>
class A
{
public:
virtual void print()
{
std::cout << 10;
}
virtual ~A()
{
std::cout << "~A()";
}
};
class B: public A
{
public:
virtual void print()
{
std::cout << 100;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* p = new B();
p->print();
delete p;
return 0;
}
我用WinDbg可以看到main函数生成的汇编代码如下:
NewTest!wmain:
00aa1020 56 push esi
00aa1021 6a04 push 4
00aa1023 e8b4030000 call NewTest!operator new (00aa13dc) //调用operator new分配大小为4字节的空间
00aa1028 83c404 add esp,4
00aa102b 85c0 test eax,eax
00aa102d 740a je NewTest!wmain+0x19 (00aa1039)
00aa102f c7005421aa00 mov dword ptr [eax],offset NewTest!B::`vftable' (00aa2154) //将虚表地址写入对象地址的头4个字节(虚表指针)
00aa1035 8bf0 mov esi,eax
00aa1037 eb02 jmp NewTest!wmain+0x1b (00aa103b)
00aa1039 33f6 xor esi,esi
00aa103b 8b06 mov eax,dword ptr [esi]
00aa103d 8b10 mov edx,dword ptr [eax]
00aa103f 8bce mov ecx,esi
00aa1041 ffd2 call edx //调用虚表内的第一个函数print
00aa1043 8b06 mov eax,dword ptr [esi]
00aa1045 8b5004 mov edx,dword ptr [eax+4]
00aa1048 6a01 push 1
00aa104a 8bce mov ecx,esi
00aa104c ffd2
call edx //调用虚表内的第二个函数(析构函数)
00aa104e 33c0 xor eax,eax
00aa1050 5e pop esi
00aa1051 c3 ret
00aa1052 cc int 3
从上面代码中我们可以看到我们构造的B对象一共只有4个字节,而这四个字节包含的就是对象的虚表指针,对于C++对象内存布局, 对于C++对象的内存布局,可以
看我这篇
探索C++对象模型。同时我们可以看到, C++里确实是通过虚表来实现多态的。
上面的代码也告诉了我们为什么不能在构造函数里通过调用虚函数实现多态? 因为虚表是在最终派生类的构造函数中生成的的, 执行基类构造函数时虚表都还没有生成。
接下来我们看看operator new背后的行为:
0:000> u 00aa13dc
NewTest!operator new:
00aa13dc ff25cc20aa00 jmp dword ptr [NewTest!_imp_??2YAPAXIZ (00aa20cc)]
里面是一个直接跳转:
0:000> u poi(00aa20cc) L10
MSVCR90!operator new:
74603e99 8bff mov edi,edi
74603e9b 55 push ebp
74603e9c 8bec mov ebp,esp
74603e9e 83ec0c sub esp,0Ch
74603ea1 eb0d jmp MSVCR90!operator new+0x17 (74603eb0)
74603ea3 ff7508 push dword ptr [ebp+8]
74603ea6 e859dcfbff call MSVCR90!_callnewh (745c1b04)
74603eab 59 pop ecx
74603eac 85c0 test eax,eax
74603eae 740f je MSVCR90!operator new+0x26 (74603ebf)
74603eb0 ff7508 push dword ptr [ebp+8]
74603eb3 e887feffff call MSVCR90!malloc (74603d3f)
74603eb8 59 pop ecx
74603eb9 85c0 test eax,eax
74603ebb 74e6 je MSVCR90!operator new+0xa (74603ea3)
74603ebd c9 leave
我们可以看到operator new最终调用的是malloc, 如果再深入下去, 会发现malloc调用的是Kernel32!HeapAlloc, 而HeapAlloc调用的又是ntdll!
RtlAllocateHeap, 关于heap的布局和分配算法,可以看
张银奎的 软件调试
上面论证了new操作符背后的行为:
首先调用operator new分配空间, 我们可以重载operator new, 定义自己的内存分配算法
然后在分配的空间上调用构造函数创建对象, 构造函数内部可能会赋值虚表指针。
接下来我们看下delete背后的行为。
我们看到delete调用的是虚表里的第二个函数, 我们先看虚表内容:
0:000> dps 00aa2154
00aa2154 00aa1010 NewTest!B::print [f:\test\newtest\newtest\newtest.cpp @ 26]
00aa2158 00aa1060 NewTest!B::`scalar deleting destructor'
00aa215c 00000000
00aa2160 00000048
00aa2164 00000000
上面看到虚表里有2个函数, 一个是print, 还有一个是destructor, 我们看下第二个函数的内容:
0:000> u 00aa1060 L10
NewTest!B::`scalar deleting destructor':
00aa1060 56 push esi
00aa1061 8bf1 mov esi,ecx
00aa1063 c7064821aa00 mov dword ptr [esi],offset NewTest!A::`vftable' (00aa2148)
00aa1069 a15820aa00 mov eax,dword ptr [NewTest!_imp_?coutstd (00aa2058)]
00aa106e 50 push eax
00aa106f e84c010000 call NewTest!std::operator<<<std::char_traits<char> > (00aa11c0)
00aa1074 83c404 add esp,4
00aa1077 f644240801 test byte ptr [esp+8],1
00aa107c 7409 je NewTest!B::`scalar deleting destructor'+0x27 (00aa1087)
00aa107e 56 push esi
00aa107f e806030000 call NewTest!operator delete (00aa138a)
00aa1084 83c404 add esp,4
00aa1087 8bc6 mov eax,esi
00aa1089 5e pop esi
00aa108a c20400 ret 4
我们可以看到虚表里放的是 B 的
scalar deleting destructor
, 它里面包含两部分代码, 一个是我们真正定义的析构函数的代码,还有一部分就是operator delete ( operator delete又会去调用free, free调用kernel32!HeapFree)。这里的
scalar deleting destructor显然不是B的析构函数~B(), 这是编译器帮我产生的一个函数,它就是给delete B类型对象用的。
接下来我们看看对于数组类型的指针, C++编译器背后是如何处理的, 把代码改成如下:
int _tmain(int argc, _TCHAR* argv[])
{
A* p = new A[10];
delete []p;
return 0;
}
下面是生成的汇编代码:
NewTest!wmain:
01181030 6a2c push 2Ch
01181032 e8c4030000 call NewTest!operator new[] (011813fb) //通过operator new分配44自己
01181037 83c404 add esp,4
0118103a 85c0 test eax,eax
0118103c 7444 je NewTest!wmain+0x52 (01181082)
0118103e 56 push esi
0118103f 6810101801 push offset NewTest!A::~A (01181010) //A的析构函数
01181044 6800111801 push offset NewTest!A::A (01181100) //A的构造函数
01181049 6a0a push 0Ah //10
0118104b 8d7004 lea esi,[eax+4] //跨过了头四个字节
0118104e 6a04 push 4 //对象大小
01181050 56 push esi //esi里放的是对象列表的起始地址(跨过了头四个字节)
01181051 c7000a000000 mov dword ptr [eax],0Ah //头四个字节写入对象列表数量(10)
01181057 e812040000 call NewTest!`eh vector constructor iterator' (0118146e)
0118105c 85f6 test esi,esi
0118105e 7421 je NewTest!wmain+0x51 (01181081)
01181060 837efc00 cmp dword ptr [esi-4],0 //判断对象数量是否 为 0
01181064 8d46fc lea eax,[esi-4] //包含对象数量的地址保存到 eax
01181067 740f je NewTest!wmain+0x48 (01181078)
01181069 8b06 mov eax,dword ptr [esi] //取A的虚表地址
0118106b 8b5004 mov edx,dword ptr [eax+4] //虚表里的第二个函数
0118106e 6a03 push 3
01181070 8bce mov ecx,esi
01181072 ffd2 call edx
01181074 5e pop esi
01181075 33c0 xor eax,eax
01181077 c3 ret
重点看上面
红色
的代码, 我们可以看到, 在new一个数组时,编译器帮我们做了下面一些事情:
(1)调用数组的operator new[] 分配内存, 大小为 4 + sizeof(object) * count, 其中头四个字节为对象数量
(2)调用NewTest!`eh vector constructor iterator(pArrayAddress, sizeof(object), object_count, pFunConstructor, pFunDestructor),
其中 pFunDestructor为析构函数, pFunConstructor为构造函数, object_count为对象数量, sizeof(object)为对象大小,pArrayAddress为起始地址。,
下面我们反汇编 NewTest!`eh vector constructor iterator:
0:000> u 0118146e L50
NewTest!`eh vector constructor iterator':
0118146e 6a10 push 10h
01181470 6890221801 push offset NewTest!__rtc_tzz+0x8 (01182290)
01181475 e8d2040000 call NewTest!__SEH_prolog4 (0118194c)
0118147a 33c0 xor eax,eax
0118147c 8945e0 mov dword ptr [ebp-20h],eax
0118147f 8945fc mov dword ptr [ebp-4],eax
01181482 8945e4 mov dword ptr [ebp-1Ch],eax
01181485 8b45e4 mov eax,dword ptr [ebp-1Ch] //临时计数,初始为0
01181488 3b4510 cmp eax,dword ptr [ebp+10h] //将临时计数和对象数量比较
0118148b 7d13 jge NewTest!`eh vector constructor iterator'+0x32 (011814a0) //如果临时计数大于对象数量则退出循环
0118148d 8b7508 mov esi,dword ptr [ebp+8] //保存第一个参数(起始地址)到 esi
01181490 8bce mov ecx,esi //赋this指针到ecx
01181492 ff5514
call dword ptr [ebp+14h] //调用构造函数
01181495 03750c add esi,dword ptr [ebp+0Ch] //移动指针, 加上对象大小
01181498 897508 mov dword ptr [ebp+8],esi //保存新对象地址到第一个参数
0118149b ff45e4 inc dword ptr [ebp-1Ch] //增加临时计数
0118149e ebe5 jmp NewTest!`eh vector constructor iterator'+0x17 (01181485)
011814a0 c745e001000000 mov dword ptr [ebp-20h],1
011814a7 c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh
011814ae e808000000 call NewTest!`eh vector constructor iterator'+0x4d (011814bb)
011814b3 e8d9040000 call NewTest!__SEH_epilog4 (01181991)
011814b8 c21400 ret 14h
我们可以看到NewTest!`eh vector constructor iterator是编译器帮我们生成的函数, 它的作用就是为数组中的每个对象都调用构造函数。
接下我们再看看数组形式的delete []在背后究竟干了什么?
重点看上面
紫色
的代码:
NewTest!wmain:
....
01181060 837efc00 cmp dword ptr [esi-4],0 //判断对象数量是否 为 0
01181064 8d46fc lea eax,[esi-4] //包含对象数量的地址保存到 eax
01181067 740f je NewTest!wmain+0x48 (01181078)
01181069 8b06 mov eax,dword ptr [esi] //取A的虚表地址
0118106b 8b5004 mov edx,dword ptr [eax+4] //虚表里的第二个函数
0118106e 6a03 push 3
01181070 8bce mov ecx,esi
01181072 ffd2 call edx
....
可以看到它将对象列表起始地址保存到ecx, 然后调用对象虚表里的第二个函数, 并且传入参数是3, 我们先看对象虚表内容:
0:000> dps 01182148
01182148 01181000 NewTest!A::print [f:\test\newtest\newtest\newtest.cpp @ 11]
0118214c 01181090 NewTest!A::`vector deleting destructor'
我们看看该函数究竟干了什么:
0:000> u 01181090 L40
NewTest!A::`vector deleting destructor':
01181090 53 push ebx
01181091 8a5c2408 mov bl,byte ptr [esp+8]
01181095 56 push esi
01181096 8bf1 mov esi,ecx
01181098 f6c302 test bl,2 //是否需要调用析构函数
0118109b 742b je NewTest!A::`vector deleting destructor'+0x38 (011810c8)
0118109d 8b46fc mov eax,dword ptr [esi-4]
011810a0 57 push edi
011810a1 6810101801 push offset NewTest!A::~A (01181010)
011810a6 8d7efc lea edi,[esi-4]
011810a9 50 push eax
011810aa 6a04 push 4
011810ac 56 push esi
011810ad e87f040000
call NewTest!`eh vector destructor iterator'
(01181531)
011810b2 f6c301 test bl,1 //是否需要释放内存
011810b5 7409 je NewTest!A::`vector deleting destructor'+0x30 (011810c0)
011810b7 57 push edi
011810b8 e85f030000
call NewTest!operator delete[] (0118141c)
011810bd 83c404 add esp,4
011810c0 8bc7 mov eax,edi
011810c2 5f pop edi
011810c3 5e pop esi
011810c4 5b pop ebx
011810c5 c20400 ret 4
可以看到它内部调用的是NewTest!`eh vector destructor iterator, 而如果再跟踪NewTest!`eh vector destructor iterator,
会看所有数组里的对象调用析构函数, 最后调用operator delete[]释放所有内存。
我们可以看到数组new[]和delete[]的关键是, C++编译器在数组起始地址之前的4个字节保存了对象的数量N,后面会根据这个数量值进行N次的构造和析构 。
最后申明下, 上面的分析仅限于VS2008, 实际上在符合C++标准的前提下, 各个C++编译器有各自不同的实现。
我们可以看到C++ 编译器在背后干了很多事情,可能会内联我们的函数, 也可以修改和产生其他一些函数, 而这是很多C开发者受不了的事情, 所以在内核级别, 很多人宁愿用C来减少编译器背后的干扰。
最后思考一下, 如果我们代码这样写,会怎么样?
int _tmain(int argc, _TCHAR* argv[])
{
A* p = new
B
[10];
delete []p;
return 0;
}
八、
C++ new运算符和operator new, placement new之间的种种关联,new的底层实现,以及operator new的重载和一些在内存池,STL中的应用
1.new运算符和operator new():
new:指我们在C++里通常用到的运算符,比如A* a = new A; 对于new来说,有new和::new之分,前者位于std
operator new():指对new的重载形式,它是一个函数,并不是运算符。对于operator new来说,分为全局重载和类重载,全局重载是void* ::operator new(size_t size),在类中重载形式 void* A::operator new(size_t size)。还要注意的是这里的operator new()完成的操作一般只是分配内存,事实上系统默认的全局::operator new(size_t size)也只是调用malloc分配内存,并且返回一个void*指针。而构造函数的调用(如果需要)是在new运算符中完成的。
先简单解释一下new和operator new之间的关系:
关于这两者的关系,我找到一段比较经典的描述(来自于www.cplusplus.com 见参考文献):
operator new
can be called explicitly as a regular function, but in C++, new
is an operator with a very specific behavior: An expression with the new
operator, first calls function operator new
(i.e., this function) with the size of its type specifier as first argument, and if this is successful, it then automatically initializes or constructs the object (if needed). Finally, the expression evaluates as a pointer to the appropriate type.
比如我们写如下代码:
A* a = new A;
我们知道这里分为两步:1.分配内存,2.调用A()构造对象。事实上,分配内存这一操作就是由operator new(size_t)来完成的,如果类A重载了operator new,那么将调用A::operator new(size_t ),如果没有重载,就调用::operator new(size_t ),全局new操作符由C++默认提供。因此前面的两步也就是:1.调用operator new 2.调用构造函数。这里再一次提出来是因为后面关于这两步会有一些变形,在关于placement new那里会讲到。先举个简单例子
-
- #include<iostream>
- class A
- {
- public:
- A()
- {
- std::cout<<"call A constructor"<<std::endl;
- }
-
- ~A()
- {
- std::cout<<"call A destructor"<<std::endl;
- }
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
-
- A* a = new A;
- delete a;
-
- system("pause");
- return 0;
- }
下面我们跟踪一下A反汇编代码,由于Debug版本反汇编跳转太多,因此此处通过Release版本在A* a = new A;处设断点反汇编:
在Release版本中,构造函数和析构函数都是直接展开的。
- A* a = new A;
- 01301022 push 1 ;不含数据成员的类占用一字节空间,此处压入sizeof(A)
- 01301024 call operator new (13013C2h) ;调用operator new(size_t size)
- 01301029 mov esi,eax ;返回值保存到esi
- 0130102B add esp,4 ;平衡栈
- 0130102E mov dword ptr [esp+8],esi ;
- 01301032 mov dword ptr [esp+14h],0
- 0130103A test esi,esi ;在operator new之后,检查其返回值,如果为空(分配失败),则不调用A()构造函数
- 0130103C je wmain+62h (1301062h) ;为空 跳过构造函数部分
- 0130103E mov eax,dword ptr [__imp_std::endl (1302038h)] ;构造函数内部,输出字符串
- 01301043 mov ecx,dword ptr [__imp_std::cout (1302050h)]
- 01301049 push eax
- 0130104A push offset string "call A constructor" (1302134h)
- 0130104F push ecx
- 01301050 call std::operator<<<std::char_traits<char> > (13011F0h)
- 01301055 add esp,8
- 01301058 mov ecx,eax
- 0130105A call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]
- 01301060 jmp wmain+64h (1301064h) ;构造完成,跳过下一句
- 01301062 xor esi,esi ;将esi置空,这里的esi即为new A的返回值
- 01301064 mov dword ptr [esp+14h],0FFFFFFFFh
- delete a;
- 0130106C test esi,esi ;检查a是否为空
- 0130106E je wmain+9Bh (130109Bh) ;如果为空,跳过析构函数和operator delete
- 01301070 mov edx,dword ptr [__imp_std::endl (1302038h)] ;析构函数 输出字符串
- 01301076 mov eax,dword ptr [__imp_std::cout (1302050h)]
- 0130107B push edx
- 0130107C push offset string "call A destructor" (1302148h)
- 01301081 push eax
- 01301082 call std::operator<<<std::char_traits<char> > (13011F0h)
- 01301087 add esp,8
- 0130108A mov ecx,eax
- 0130108C call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]
- 01301092 push esi ;压入a
- 01301093 call operator delete (13013BCh) ;调用operator delete
- 01301098 add esp,4
- 通过反汇编可以看出A* = new A包含了operator new(sizeof(A))和A()两个步骤(当然,最后还要将值返回到a)
- delete a包含了~A()和operator delete(a)两个步骤。
2.operator new的三种形式:
operator new有三种形式:
throwing (1) | void* operator new (std::size_t size) throw (std::bad_alloc);
|
---|
nothrow (2) | void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
|
---|
placement (3) | void* operator new (std::size_t size, void* ptr) throw(); |
---|
(1)(2)的区别仅是是否抛出异常,当分配失败时,前者会抛出bad_alloc异常,后者返回null,不会抛出异常。它们都分配一个固定大小的连续内存。
用法示例:
A* a = new A; //调用throwing(1)
A* a = new(std::nothrow) A; //调用nothrow(2)
(3)是placement new,它也是对operator new的一个重载,定义于<new>中,它多接收一个ptr参数,但它只是简单地返回ptr。其在new.h下的源代码如下:
- #ifndef __PLACEMENT_NEW_INLINE
- #define __PLACEMENT_NEW_INLINE
- inline void *__cdecl operator new(size_t, void *_P)
- {return (_P); }
- #if _MSC_VER >= 1200
- inline void __cdecl operator delete(void *, void *)
- {return; }
- #endif
- #endif
那么它究竟有什么用呢?事实上,它可以实现在ptr所指地址上构建一个对象(通过调用其构造函数),这在内存池技术上有广泛应用。
它的调用形式为:
new(p) A(); //也可用A(5)等有参构造函数。
前面说到,new运算符都会调用operator new,而这里的operator new(size_t, void*)并没有什么作用,真正起作用的是new运算符的第二个步骤:在p处调用A构造函数。这里的p可以是动态分配的内存,也可以是栈中缓冲,如char buf[100]; new(buf) A();
我们仍然可以通过一个例子来验证:
- #include <iostream>
- class A
- {
- public:
- A()
- {
- std::cout<<"call A constructor"<<std::endl;
- }
-
- ~A()
- {
- std::cout<<"call A destructor"<<std::endl;
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
-
- A* p = (A*)::operator new(sizeof(A));
-
- new(p) A();
-
- p->~A();
-
- ::operator delete(p);
-
- system("pause");
- return 0;
- }
上面的代码将对象的分配,构造,析构和释放分离开来,这也是new和delete运算符两句就能完成的操作。
先直接运行可以看到程序输出:
再分别注释掉new(a) A();和a->~A();两句,可以看到对应的构造和析构函数将不会被调用。
然后查看反汇编:
平台: Visual Studio 2008 Debug版
- A* a = (A*)::operator new(sizeof(A)); //分配
- 00F9151D push 1
- 00F9151F call operator new (0F91208h) ;调用::operator new(size_t size)也就是throwing(1)版本
- 00F91524 add esp,4
- 00F91527 mov dword ptr [ebp-14h],eax ;返回地址放入[ebp-14h] 即为p
-
- new(a) A(); //构造
- 00F9152A mov eax,dword ptr [ebp-14h]
- 00F9152D push eax
- 00F9152E push 1 ;压入p
- 00F91530 call operator new (0F91280h);调用operator new(size_t, void* p)即placement(3)版本 只是简单返回p
- 00F91535 add esp,8
- 00F91538 mov dword ptr [ebp-0E0h],eax ;将p放入[ebp-0E0h]
- 00F9153E mov dword ptr [ebp-4],0
- 00F91545 cmp dword ptr [ebp-0E0h],0 ;判断p是否为空
- 00F9154C je wmain+81h (0F91561h) ;如果为空 跳过构造函数
- 00F9154E mov ecx,dword ptr [ebp-0E0h] ;取出p到ecx
- 00F91554 call A::A (0F91285h) ;调用构造函数 根据_thiscall调用约定 this指针通过ecx寄存器传递
- 00F91559 mov dword ptr [ebp-0F4h],eax ;将返回值(this指针)放入[ebp-0F4h]中
- 00F9155F jmp wmain+8Bh (0F9156Bh) ;跳过下一句
- 00F91561 mov dword ptr [ebp-0F4h],0 ;将[ebp-0F4h]置空 当前面判断p为空时执行此语句
- 00F9156B mov ecx,dword ptr [ebp-0F4h] ;[ebp-0F4h]为最终构造完成后的this指针(或者为空) 放入ecx
- 00F91571 mov dword ptr [ebp-0ECh],ecx ;又将this放入[ebp-0ECh] 这些都是调试所用
- 00F91577 mov dword ptr [ebp-4],0FFFFFFFFh
-
- a->~A(); //析构
- 00F9157E push 0
- 00F91580 mov ecx,dword ptr [ebp-14h] ;从[ebp-14h]中取出p
- 00F91583 call A::`scalar deleting destructor' (0F91041h) ;调用析构函数(跟踪进去比较复杂 如果在Release下,构造析构函数都是直接展开的)
-
- ::operator delete(a); //释放
- 00F91588 mov eax,dword ptr [ebp-14h] ;将p放入eax
- 00F9158B push eax ;压入p
- 00F9158C call operator delete (0F910B9h);调用operator delete(void* )
- 00F91591 add esp,4 </span>
从反汇编中可以看出,其实operator new调用了两次,只不过每一次调用不同的重载函数,并且placement new的主要作用只是将p放入ecx,并且调用其构造函数。
事实上,在指定地址上构造对象还有另一种方法,即手动调用构造函数:p->A::A(); 这里要加上A::作用域,否则编译器会报错:
error C2273: “函数样式转换”: 位于“->”运算符右边时非法
用p->A::A();替换掉new(p) A();仍然能达到同样的效果,反汇编:
- A* a = (A*)::operator new(sizeof(A)); //分配
- 010614FE push 1
- 01061500 call operator new (1061208h)
- 01061505 add esp,4
- 01061508 mov dword ptr [a],eax
-
- //new(a) A(); //构造
- a->A::A();
- 0106150B mov ecx,dword ptr [a]
- 0106150E call operator new (1061285h)
-
- a->~A(); //析构
- 01061513 push 0
- 01061515 mov ecx,dword ptr [a]
- 01061518 call A::`scalar deleting destructor' (1061041h)
-
- ::operator delete(a); //释放
- 0106151D mov eax,dword ptr [a]
- 01061520 push eax
- 01061521 call operator delete (10610B9h)
- 01061526 add esp,4
比之前的方法更加简洁高效(不需要调用placement new)。不知道手动调用构造函数是否有违C++标准或有什么隐晦,我在其他很多有名的内存池(包括SGI STL alloc)实现上看到都是用的placement new,而不是手动调用构造函数。
3. operator new重载:
前面简单提到过 A* p = new A;所发生的事情:先调用operator new,如果类A重载了operator new,那么就使用该重载版本,否则使用全局版本::operatro new(size_t size)。那么类中可以重载operator new的哪些版本?全局operator new可以重载吗?全局和类中重载分别会在什么时机调用?
1.在类中重载operator new
上面提到的throwing(1)和nothrow(2)的operator new是可以被重载的,比如:
- #include <iostream>
- class A
- {
- public:
- A()
- {
- std::cout<<"call A constructor"<<std::endl;
- }
-
- ~A()
- {
- std::cout<<"call A destructor"<<std::endl;
- }
- void* operator new(size_t size)
- {
- std::cout<<"call A::operator new"<<std::endl;
- return malloc(size);
- }
-
- void* operator new(size_t size, const std::nothrow_t& nothrow_value)
- {
- std::cout<<"call A::operator new nothrow"<<std::endl;
- return malloc(size);
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- A* p1 = new A;
- delete p1;
-
- A* p2 = new(std::nothrow) A;
- delete p2;
-
- system("pause");
- return 0;
- }
![](https://img-blog.csdn.net/20130710083057671?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvV1VEQUlKVU4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
如果类A中没有对operator new的重载,那么new A和new(std::nothrow) A;都将会使用全局operator new(size_t size)。可将A中两个operator new注释掉,并且在A外添加一个全局operator new重载:
- void* ::operator new(size_t size)
- {
- std::cout<<"call global operator new"<<std::endl;
- return malloc(size);
- }
程序输出:
注意,这里的重载遵循作用域覆盖原则,即在里向外寻找operator new的重载时,只要找到operator new()函数就不再向外查找,如果参数符合则通过,如果参数不符合则报错,而不管全局是否还有相匹配的函数原型。比如如果这里只将A中operator new(size_t, const std::nothrow_t&)删除掉,就会报错:
error C2660: “A::operator new”: 函数不接受 2 个参数。
至于placement new,它本身就是operator new的一个重载,不需也尽量不要对它进行改写,因为它一般是搭配 new(p) A(); 工作的,它的职责只需简单返回指针。
对operator new的重载还可以添加自定义参数,如在类A中添加
- void* operator new(size_t size, int x, int y, int z)
- {
- std::cout<<"X="<<x<<" Y="<<y<<" Z="<<z<<std::endl;
- return malloc(size);
- }
这种重载看起来没有什么大作用,因为它operator new需要完成的任务只是分配内存,但是通过对这类重载的巧妙应用,可以让它在动态分配内存调试和检测中大展身手。这将在后面operator new重载运用技巧中,展现。
2.重载全局operator new
全局operator new的重载和在类中重载并无太大区别,当new A;时,如果类A中没有重载operator new,那么将调用全局operator new函数,如果没有重载全局operator new,最后会调用默认的全局operator new。
3.类中operator new和全局operator new的调用时机
前面已经提到了在new时的调用顺序,但是这里提出来的原因是还存在一个全局的new运算符,也就是::new,这个运算符会直接调用全局operator new,并且也会调用构造函数。这可能让人很犯迷糊,只做了解即可。这里提到的调用时机都是指通过new运算符调用,没有讨论其他情况,比如主动调用。
4.operator new运用技巧和一些实例探索
1.operator new重载运用于调试:
前面提到如何operator new的重载是可以有自定义参数的,那么我们如何利用自定义参数获取更多的信息呢,这里一个很有用的做法就是给operator new添加两个参数:char* file, int line,这两个参数记录new运算符的位置,然后再在new时将文件名和行号传入,这样我们就能在分配内存失败时给出提示:输出文件名和行号。
那么如何获取当前语句所在文件名和行号呢,windows提供两个宏:__FILE__和__LINE__。利用它们可以直接获取到文件名和行号,也就是 new(__FILE__, __LINE__) 由于这些都是不变的,因此可以再定义一个宏:#define new new(__FILE__, __LINE__)。这样我们就只需要定义这个宏,然后重载operator new即可。
源代码如下,这里只是简单输出new的文件名和行号。
-
- class A
- {
- public:
- A()
- {
- std::cout<<"call A constructor"<<std::endl;
- }
-
- ~A()
- {
- std::cout<<"call A destructor"<<std::endl;
- }
-
- void* operator new(size_t size, const char* file, int line)
- {
- std::cout<<"call A::operator new on file:"<<file<<" line:"<<line<<std::endl;
- return malloc(size);
- return NULL;
- }
-
- };
-
- #include <iostream>
- #include "A.h"
- #define new new(__FILE__, __LINE__)
-
- int _tmain(int argc, _TCHAR* argv[])
- {
- A* p1 = new A;
- delete p1;
-
- A* p2 = new A;
- delete p2;
-
- system("pause");
- return 0;
- }
输出:
注意:需要将类的声明实现与new的使用隔离开来。并且将类头文件放在宏定义之前。否则在类A中的operator new重载中的new会被宏替换,整个函数就变成了: void* operator new(__FILE__, __LINE__)(size_t size, char* file, int line)
编译器自然会报错。
2.内存池优化
operator new的另一个大用处就是内存池优化,内存池的一个常见策略就是分配一次性分配一块大的内存作为内存池(buffer或pool),然后重复利用该内存块,每次分配都从内存池中取出,释放则将内存块放回内存池。在我们客户端调用的是new运算符,我们可以改写operator new函数,让它从内存池中取出(当内存池不够时,再从系统堆中一次性分配一块大的),至于构造和析构则在取出的内存上进行,然后再重载operator delete,它将内存块放回内存池。关于内存池和operator new在参考文献中有一篇很好的文章。这里就不累述了。
3.STL中的new
在SGI STL源码中,defalloc.h和stl_construct.h中提供了最简单的空间配置器(allocator)封装,见《STL源码剖析》P48。它将对象的空间分配和构造分离开来,虽然在defalloc.h中仅仅是对::operator new和::operator delete的一层封装,但是它仍然给STL容器提供了更加灵活的接口。SGI STL真正使用的并不是defalloc.h中的分配器,而是stl_alloc.h中的SGI精心打造的"双层级配置器",它将内存池技术演绎得淋漓尽致,值得细细琢磨。顺便提一下,在stl_alloc.h中并没有使用::operator new/delete 而直接使用malloc和free。具体缘由均可参见《STL源码剖析》。
5. delete的使用
delete的使用基本和new一致,包括operator delete的重载方式这些都相似,只不过它的参数是void*,返回值为void。但是有一点需要注意,operator delete的自定义参数重载并不能手动调用。比如
- void* operator new(size_t size, int x)
- {
- cout<<" x = "<<x<<endl;
- return malloc(size);
- }
- void operator delete(void* p, int x)
- {
- cout<<" x = "<<x<<endl;
- free(p);
- }
如下调用是无法通过的:
A* p = new(3) A;//Ok
delete(3) p;//error C2541: “delete”: 不能删除不是指针的对象
那么重载operator delete有什么作用?如何调用?事实上以上自定义参数operator delete 只在一种情况下被调用:当new运算符抛出异常时。
可以这样理解,只有在new运算符中,编译器才知道你调用的operator new形式,然后它会调用对应的operator delete。一旦出了new运算符,编译器对于你自定义的new将一无所知,因此它只会按照你指定的delete运算符形式来调用operator delete,而至于为什么不能指定调用自定义delete(也就是只能老老实实delete p),这个就不知道了。
细心观察的话,上面operator new用于调试的例子代码中,由于我们没有给出operator new对应的operator delete。在VS2008下会有如下警告:
warning C4291: “void *A::operator new(size_t,const char *,int)”: 未找到匹配的删除运算符;如果初始化引发异常,则不会释放内存
6. 关于new和内存分配的其他
1.set_new_handler
还有一些零散的东西没有介绍到,比如set_new_handler可以在malloc(需要调用set_new_mode(1))或operator new内存分配失败时指定一个入口函数new_handler,这个函数完成自定义处理(继续尝试分配,抛出异常,或终止程序),如果new_handler返回,那么系统将继续尝试分配内存,如果失败,将继续重复调用它,直到内存分配完毕或new_handler不再返回(抛出异常,终止)。下面这段程序完成这个测试:
- #include <iostream>
- #include <new.h>// 使用_set_new_mode和set_new_handler
- void nomem_handler()
- {
- std::cout<<"call nomem_handler"<<std::endl;
- }
- int main()
- {
- _set_new_mode(1);
- set_new_handler(nomem_handler);
- std::cout<<"try to alloc 2GB memory...."<<std::endl;
- char* a = (char*)malloc(2*1024*1024*1024);
- if(a)
- std::cout<<"ok...I got it"<<std::endl;
- free(a);
- system("pause");
- }
程序运行后会一直输出call nomem_handler 因为函数里面只是简单输出,返回,系统尝试分配失败后,调用nomem_handler函数,由于该函数并没有起到实际作用(让可分配内存增大),因此返回后系统再次尝试分配失败,再调用nomem_handler,循环下去。
在SGI STL中的也有个仿new_handler函数:oom_malloc
2.new分配数组
A* p = new A[3];中,会直接调用全局的operator new[](size_t size),而不管A中是否有operator new[]的重载。而delete[]p却会优先调用A::operator delete[](void*)(如果A中有重载)。另外还要注意的是,在operator new[](size_t size)中传入的并不是sizeof(A)*3。而要在对象数组的大小上加上一个额外数据,用于编译器区分对象数组指针和对象指针以及对象数组大小。在VS2008下这个额外数据占4个字节,一个int大小。测试代码如下
-
- class A
- {
- public:
- A()
- {
- std::cout<<"call A constructor"<<std::endl;
- }
-
- ~A()
- {
- std::cout<<"call A destructor"<<std::endl;
- }
-
- void* operator new(size_t size)
- {
- std::cout<<"call A::operator new[] size:"<<size<<std::endl;
- return malloc(size);
- }
- void operator delete[](void* p)
- {
- std::cout<<"call A::operator delete[]"<<std::endl;
- free(p);
- }
- void operator delete(void* p)
- {
- free(p);
- }
- };
-
- #include <iostream>
- #include "A.h"
-
- void* operator new[](size_t size)
- {
- std::cout<<"call global new[] size: "<<size<<std::endl;
- return malloc(size);
- }
-
- void operator delete[](void* p)
- {
- std::cout<<"call global delete[] "<<std::endl;
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
- std::cout<<"sizeof A "<<sizeof(A)<<std::endl;
- A* p1 = new A[3];
- delete []p1;
-
- system("pause");
- return 0;
- }
输出:
简单跟踪了一下:
operator new[]返回的是0x005b668 而最后new运算符返回给p的是0x005b66c。也就是说p就是数组的起始地址,这样程序看到的内存就是线性的,不包括前面的额外数据。
在内存中,可以看到前面的四个字节额外数据是0x00000003 也就是3,代表数组元素个数。后面三个cd是堆在Debug中的默认值(中文的cdcd就是"屯",栈的初始值为cc,0xcccc中文"烫")。再后面的0xfdfdfdfd应该是堆块的结束标志,前面我有博客专门跟踪过。
注:其实在malloc源码中也有内存池的运用,而且也比较复杂。最近在参考dlmalloc版本和STL空间适配器,真没有想到一个内存分配能涉及这么多的东西。
参考文献:
3.《STL源码剖析》 空间配置器
4.http://blog.csdn.net/wudaijun/article/details/927333