C++ 中的 new/delete 和 new[]/delete[]深入理解
来源:互联网 发布:海马电动车数据 编辑:程序博客网 时间:2024/06/07 06:23
在 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 objectvoid *operator delete(void *); //free an objectvoid *operator new[](size_t); //allocate an arrayvoid *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 以及打开一个文件,析构函数关闭打开的文件。
我们使用
class A *pA = new A(10);
来创建一个类的对象,返回其指针 pA。如下图所示 new 背后完成的工作:
简单总结一下:
- 首先需要调用上面提到的 operator new 标准库函数,传入的参数为 class A 的大小,这里为 8 个字节,至于为什么是 8 个字节,你可以看看《深入 C++ 对象模型》一书,这里不做多解释。这样函数返回的是分配内存的起始地址,这里假设是 0x007da290。
- 上面分配的内存是未初始化的,也是未类型化的,第二步就在这一块原始的内存上对类对象进行初始化,调用的是相应的构造函数,这里是调用
A:A(10);
这个函数,从图中也可以看到对这块申请的内存进行了初始化,var=10, file 指向打开的文件
。 - 最后一步就是返回新分配并构造好的对象的指针,这里 pA 就指向 0x007da290 这块内存,pA 的类型为类 A 对象的指针。
所有这三步,你都可以通过反汇编找到相应的汇编代码,在这里我就不列出了。
好了,那么 delete 都干了什么呢?还是接着上面的例子,如果这时想释放掉申请的类的对象怎么办?当然我们可以使用下面的语句来完成:
delete pA;
delete 所做的事情如下图所示:
delete 就做了两件事情:
- 调用 pA 指向对象的析构函数,对打开的文件进行关闭。
- 通过上面提到的标准库函数 operator delete 来释放该对象的内存,传入函数的参数为 pA 的值,也就是 0x007d290。
好了,解释完了 new 和 delete 背后所做的事情了,是不是觉得也很简单?不就多了一个构造函数和析构函数的调用嘛。
如何申请和释放一个数组?
我们经常要用到动态分配一个数组,也许是这样的:
string *psa = new string[10]; //array of 10 empty stringsint *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,但不具体描述类的内容,这个类中有显示的构造函数、析构函数等。那么 当我们调用
class A *pAa = new A[3];
时需要做的事情如下:
从这个图中我们可以看到申请时在数组对象的上面还多分配了 4 个字节用来保存数组的大小,但是最终返回的是对象数组的指针,而不是所有分配空间的起始地址。
这样的话,释放就很简单了:
delete []pAa;
这里要注意的两点是:
- 调用析构函数的次数是从数组对象指针前面的 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
}
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;
针对数组则是:
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, 比如
然后就可以这样调用了: 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();
new operator就象sizeof一样是语言内置的,我们不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new operator总是做这两件事情,你不能以任何方式改变它的行为。
我们所能改变的是如何为对象分配内存。new operator调用一个函数来完成必需的内存分配,你能够重写或重载这个函数来改变它的行为。new operator为分配内存所调用函数的名字是operator 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); // 释放内存, 没有调用析构函数
七、从汇编角度看new和delete的背后行为
1.new运算符和operator new():
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.2.operator new的三种形式:
void* operator new (std::size_t size) throw (std::bad_alloc);
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
void* operator new (std::size_t size, void* ptr) throw();
3. operator new重载:
1.在类中重载operator new
error C2660: “A::operator new”: 函数不接受 2 个参数。
2.重载全局operator new
3.类中operator new和全局operator new的调用时机
4.operator new运用技巧和一些实例探索
1.operator new重载运用于调试:
2.内存池优化
3.STL中的new
5. delete的使用
6. 关于new和内存分配的其他
1.set_new_handler
2.new分配数组
简单跟踪了一下:
参考文献:
- C++ 中的 new/delete 和 new[]/delete[]深入理解
- 理解new和delete
- C++ 中的 new/delete 和 new[]/delete[]
- C++中的new/delete和new[]/delete[]
- C++ 中的 new/delete 和 new[]/delete[]
- C++中的new delete和new[] delete []
- C++中的new/delete和new[]/delete[]
- C++ 中的 new/delete 和 new[]/delete[]
- 【C++】 深入探究 new 和 delete
- C++:new和delete
- new和delete【C++】
- More Effective C :理解new和delete
- More Effective C++:理解new和delete
- 【C++】new delete & new[] delete[]
- 深入new和delete小结
- new, delete 和 new[] delete[]
- new、delete和new[]、delete[]
- C++中的new和delete
- 游戏中内嵌视频播放功能
- bzoj3585(线段树)
- PXE多机无人值守安装+批量用户创建并限定登录地点
- Activity学习笔记
- Theano学习笔记(一)——代数
- C++ 中的 new/delete 和 new[]/delete[]深入理解
- 进程间通信总述
- 水平滑动头布局
- AdaBoost 人脸检测介绍(7) : Haar特征CvHaarClassifierCascade等结构分析
- DevExpress Grid 列标题多行设置
- 2016Hrbust软件学院ACM新生选拔赛(一)
- java中I/O流中的随机流
- 不用变量交换2个值
- 大数据Spark “蘑菇云”行动第52课: Spark大型项目广告点击项目数据建模 项目!!!大项目!!!超大型大数据项目!!!