您的位置:首页 > 编程语言 > C语言/C++

C++ new/delete运算符

2016-05-18 14:31 543 查看

一、new/delete 简介

new 和 delete 是 C++ 用于管理 堆内存 的两个运算符,对应于 C 语言中的 malloc 和 free,但是 malloc 和 free 是函数,new 和 delete 是运算符。除此之外,new 在申请内存的同时,还会调用对象的构造函数,而 malloc 只会申请内存;同样,delete 在释放内存之前,会调用对象的析构函数,而 free 只会释放内存。

new 运算符的内部实现分为两步:

内存分配

调用相应的
operator new(size_t)
函数,动态分配内存。如果
operator new(size_t)
不能成功获得内存,则调用
new_handler()
函数用于处理new失败问题。如果没有设置
new_handler()
函数或者
new_handler()
未能分配足够内存,则抛出
std::bad_alloc
异常。“new运算符”所调用的
operator new(size_t)
函数,按照C++的名字查找规则,首先做依赖于实参的名字查找(即ADL规则),在要申请内存的数据类型T的 内部(成员函数)、数据类型T定义处的命名空间查找;如果没有查找到,则直接调用全局的
::operator new(size_t)
函数。

构造函数

在分配到的动态内存块上 初始化 相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用
operator delete(void*, void*)
函数释放已经分配到的内存。

delete 运算符的内部实现分为两步:

析构函数

调用相应类型的析构函数,处理类内部可能涉及的资源释放。

内存释放

调用相应的
operator delete(void *)
函数。调用顺序参考上述
operator new(size_t)
函数(ADL规则)。

关于 new/delete 的内部实现,参考如下代码。

class T{
public:
T(){
cout << "构造函数。" << endl;
}

~T(){
cout << "析构函数。" << endl;
}

void * operator new(size_t sz){

T * t = (T*)malloc(sizeof(T));
cout << "内存分配。" << endl;

return t;
}

void operator delete(void *p){

free(p);
cout << "内存释放。" << endl;

return;
}
};

int main()
{
T * t = new T(); // 先 内存分配 ,再 构造函数

delete t; // 先 析构函数, 再 内存释放

return 0;
}


结果如下:



每个 new 获取的对象,必须用 delete 析构并释放内存,以免 内存泄漏。

举例说明:

class Test{
public:
Test(){
str = new char[2];
}
~Test(){
delete [] str;
}
private:
char * str;
};

int main(){

// ①
Test * t = new Test;
free(t);

// ②
Test * t2 = (Test*)malloc(sizeof(Test));
delete t2;

return 0;
}


对于 ①,
new Test
的时候将会产生两方面的内存:

Test 对象本身的内存( Win32环境,4 Bytes 存储 char * 指针);

str 所指向的 2 bytes 堆内存。

如果调用 free 释放内存,那么由于 free 并不会调用 Test 的析构函数,所以 free 只能释放 Test 对象的内存(4 bytes),而 str 所指向的 2-bytes 堆内存并不能得到释放,因此而造成 内存泄漏

对于 ②,malloc 并不会调用类的构造函数,所以只分配了 Test 对象的内存,str 并未初始化为指向一块堆内存。所以当调用 delete 释放内存的时候,将调用类的析构函数 (
delete [] str
),此时 delete 一块没有使用权的内存,程序崩溃

总之,编写C++程序时,在进行动态内存分配的时候,最好使用 new 和 delete。并且记住,new 出来的对象用 delete “消灭”它。

二、new/delete 表达式语法

2.1 new 表达式语法

2.1.1 内存分配

1)普通的 new 运算符表达式

new 的基本语法 :

type  * p_var = new type;   // int * a = new int; // 分配内存,但未初始化,垃圾值


通过new初始化对象,使用下述语法:

type  * p_var = new type(init); // int * a = new int(8); //分配内存时,将 *a 初始化为 8


其中 init 是传递给构造函数的实参表或初值。

2)动态生成对象数组的 new 运算符表达式

new 也可创建一个对象数组:

type  p_var = new type [size]; // int * a = new int[3] ; // 分配了 3个 int 大小的连续内存块, 但未初始化


C++98 标准规定,new 创建的对象数组不能被显式初始化, 数组所有元素被缺省初始化。如果数组元素类型没有缺省初始化(默认构造函数),则编译报错。但 C++11 已经允许显式初始化,例如:

int *p_int = new int[3] {1,2,3};


如此生成的对象数组,在释放时必须调用
delete []
表达式。

2.1.2 placement new 运算符表达式

placement new 运算符表达式 就是 在用户指定的内存位置上构建新的对象 ,这个构建过程并不需要额外分配内存,只需要调用对象的构造函数即可。

placement new 的语法是:

new ( expression-list ) new-type-id ( optional-initializer-expression-list );


使用这种 placement new 运算符表达式,原因之一是 用户的程序不能在一块内存上自行调用其构造函数,必须由编译系统生成的代码调用构造函数。原因之二是可能需要把对象放在特定硬件的内存地址上,或者放在多处理器内核的共享的内存地址上。(PS:构造函数没办法直接这么调用 p->A(),而析构函数可以直接这么调用 p->~A()。)

释放这种 placement new 运算符对象时,不能调用 placement delete,应直接调用析构函数,如:pObj->~ClassType() ; 然后再自行释放内存。

注意: C++ 中并没用与 placement new 运算符 功能相对应 的 placement delete 运算符(没有placement delete 运算符的概念,但是有 placement delete 函数)。^_^

解释:

首先看看 C++ 设计者,大牛 - Bjarne Stroustrup 的说法 Is there a “placement delete”?

class Arena {
public:
void * allocate(size_t);
void deallocate(void\*);
....
};
void * operator new(size_t sz, Arena& a)
{
return a.allocate(sz);
}
Arena a1(some arguments);
Arena a2(some arguments);

X* p1 = new(a1) X;
Y* p2 = new(a1) Y;
Z* p3 = new(a2) Z;


对于上述代码,C++的类型机制并不能推断 p1 指向的对象是否位于 a1 之上。那么直接调用
delete(a1) p1;
就容易出错。所以为了安全,C++不提供 placement delete 运算符。

placement new 运算符不另外分配内存,换句话说,不是new运算符。它完成的功能是在给定地址上调用构造函数。如果提供p->T(),那么 placement new 运算符就不需要了。如果存在功能对应的 placement delete 运算符,那么功能就应该是在给定地址上调用析构函数。但因为 C++已经提供了p->~T(),就没必要有 placement delete 运算符。

如果存在对应的 placement delete 运算符,其实就是调用析构函数。而本身析构函数就可以自行主动调用,那么自己调用就好了,但是对象本身所占用这块内存还可以继续使用。如果想 placement delete 运算符像打洞一样,连对象内存一起回收,那
operator new(size_t )
的大块蜂窝煤内存如何 delete 。这不科学,既然整块内存是
operator new(size_t)
的,就应该由
operator delete(void *)
回收,而不能用 placement delete 运算符部分回收。

总之,没有与 placement new 运算符功能相对应的 placement delete 运算符。而且需要注意的是,运算符和函数是两个不同的概念,C++有 placement new 运算符和函数的概念,但是没有 placement delete 运算符的概念,有 placement delete 函数的概念 。

所以,对于 placement new 运算符,我们需要主动调用对象的析构函数。如下示例:

#include <iostream>

using namespace std;

class Test{
public:
Test(){
cout << "Test 构造" << endl;
str = new char[2];
}
~Test(){
cout << "Test 析构" << endl;
delete [] str;
}
private:
char * str;
};

int main(int argc, char* argv[])
{
char buf[100];  // 栈变量
Test *p = new(buf) Test(); // Test()产生的临时变量用于初始化 指定内存地址

p->~Test(); // 一定要主动调用析构函数,避免内存泄漏。 而且调用必须在 buf 生命周期内调用才有效。

// buf 指向的栈内存并不需要程序员主动释放。
// 栈变量过了生命周期会自动释放内存
// 其实栈内存的释放也不叫内存释放,只是栈顶指针移动,如果该块栈内存没有被其他程序刷新,那么该栈内存的值依然不变。

char * buf2 = new char[100];
Test * p2 = new(buf2) Test();

p2->~Test(); // 切记,主动调用析构函数

delete [] buf2; // 堆内存需要主动释放

return 0;
}


如上代码,如果把
p->~Test();
注释掉,上述代码的结果将为:

Test 构造


显然是只调用构造函数。所以对于placement new,我们需要主动调用对象的析构函数
pObj->~ClassType()


2.1.3 如何在栈上new?

我们知道,new 是用于管理堆内存,那又怎么可能在栈上 new 出一个对象呢?

通过上面的讨论,我们发现,new 除了能用于动态分配内存,还能够使用 placement new 在特定内存位置进行初始化。所以,如何在栈上 new 呢?上述代码(2.1.2)就是一个很好的例子。

2.1.4 不抛出异常的new运算符

在分配内存失败时,new运算符的标准行为是抛出
std::bad_alloc
异常。也可以让new运算符在分配内存失败时不抛出异常而是返回空指针。

new (nothrow) Type ( optional-initializer-expression-list );




new (nothrow) Type[size]; // new (std::nothrow_t) Type[size];


其中 nothrow 是
std::nothrow_t
的一个实例.

2.2 delete 表达式语法

2.2.1 内存释放

1)普通的 delete 运算符

delete 的基本语法是:

delete val_ptr;


2)释放对象数组的 delete 运算符

delete [] val_ptr


2.2.2 没有 placement delete 运算符表达式

通过上面的讨论,我们可以知道 C++ 中并没有提供与 placement new 运算符功能相对应 placement delete 运算符。但是仍然有placement delete函数的概念,功能在后面有介绍。

C++ 不能使用 placement delete 运算符表达式直接析构一个对象但不释放其内存。因此,对于placement new表达式构建的对象,析构释放时有两种办法:

是直接写一个函数,完成析构对象、释放内存的操作:

void destroy (T * p, A & arena)
{
// *p 是在 arena 之上构建的对象,即 T * a = new(&arena) T;
p->~T() ;   // 先析构 *p 对象
arena.deallocate(p) ;  // 再释放 arena 整个内存,而不是位于arena中的部分内存(*p)
}
A arena ;
T * p = new (arena) T ;
....
destroy(p, arena) ;


分两步显式 调用析构函数 与 带位置的 operator delete 函数:

A arena ;
T * p = new (arena) T ;
/* ... */
p->~T() ;    // 先析构
operator delete(p, arena) ;   // 调用 placement delete 函数(非运算符)

// Then call the deallocator function indirectly via
operator delete(void *, A &) .


带位置的
operator delete(void *,void *)
函数,可以被 placement new 运算符表达式自动调用。这是在对象的构造函数抛出异常的时候,用来释放掉 placement new 函数获取的内存(类内部可能涉及的内存分配)。以避免内存泄露。

#include <cstdlib>
#include <iostream>

char buf[100];
struct A {} ;
struct E {} ;

class T {
public:
T()
{
std::cout << "T 构造函数。" << std::endl;
throw E(); //抛出异常
}

void * operator new(std::size_t,const A &)
{
std::cout << "Placement new called for class T." << std::endl;
return buf;
}

void operator delete(void*, const A &)
{
std::cout << "Placement delete called for class T." << std::endl;
}
} ;

void * operator new ( std::size_t, const A & )
{
std::cout << "Placement new called." << std::endl;
return buf;
}

void operator delete ( void *, const A & )
{
std::cout << "Placement delete called." << std::endl;
}

int main ()
{
A a ;
try {
T * p = new (a) T ;
/* do something */
}
catch (E exp)
{
std::cout << "Exception caught." << std::endl;
}

return 0 ;
}


结果如下:



C++ 有 placement delete 函数,但是没有 placement delete 运算符的概念。

2.2.3 delete 类对象时该注意的问题

问题 1

如下一段代码,是否是产生内存泄漏? 此题的讨论详见 csdn 论坛

class A
{
public:
A(){}
virtual void f(){}

private:
int m_a;
};

class B : public A
{
public:
virtual void f(){}
private:
int m_b;
};

int main()
{
A *pa = new B;
delete pa;

pa = NULL;

return 0;
}


答案:不会产生内存泄漏。

delete 释放内存时,会调用类的析构函数。 但是需要明确的是 析构函数并不会释放 对象本身 的内存 。

delete 运算符分为2个阶段。 第一个阶段是调用类的析构函数,第二阶段才是释放对象内存(但是这个工作不是析构函数在做)。

析构函数是free()之前的调用,而真正释放内存的操作是
free(void *ptr)
,注意只有指针一个参数,没有长度参数,这说明了什么?说明了
A *pa = new B;
时带着长度sizeof(B)最终调用了
malloc(sizeof(B))
;申请的内存及长度已经被记录,当free(pa)是就会释放掉自pa开始长度为sizeof(B)的内存。析构函数仅仅是应用逻辑层次的释放资源,不是物理层次的释放资源。(PS:关于new/delete运算符的具体实现后面还会涉及。)

问题 2

修改一下上面的题目,如下是否会造成内存泄漏呢?

class A
{
public:
A(){
m_a = new int(1);
}
~A(){  // 声明为virtual, 防止内存泄漏
delete m_a;
}

private:
int * m_a;
};

class B : public A
{
public:
B() : A(){
m_b = new int(2);
}

~B(){
delete m_b;
}

private:
int * m_b;
};

int main()
{
A * pa = new B;
delete pa;

pa = NULL;

return 0;
}


答案:会造成内存泄漏。

delete pa 的时候,只会调用基类的析构函数。所以 m_b 指向的内存块没得到释放。造成内存泄漏。

通过这个例子,应该深刻理解 析构函数的作用: 程序员处理类内部可能涉及的内存分配、资源释放。而不是释放类本身的内存。

三、operator new/delete() 的函数重载

平时使用 new 动态生成一个对象,实际上是调用了 new 运算符

该运算符首先调用了
operator new(std::size_t )
函数动态分配内存,然后调用类型的构造函数初始化这块内存。 new / delete 运算符是不能被重载的,但是下述各种
operator new/delete()
函数既可以作为 1. 全局函数重载,也可以作为 2. 类成员函数或 3. 作用域内的函数重载,即由编程者指定如何获取内存。

3.1 普通的operator new/delete(size_t size)函数

new 运算符 首先调用
operator new(std::size_t )
函数动态分配内存。首先查找 类内 是否有
operator new(std::size_t)
函数可供使用(即依赖于实参的名字查找)。

operator new(size_t )
函数的参数是一个 size_t 类型,指明了需要分配内存的规模。

operator new(size_t )
函数可以被每个 C++ 类作为成员函数重载。也可以作为全局函数重载:

void * operator new (std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();


内存需要回收的话,调用对应的
operator delete(void *)
函数。

例如,在 new 运算符表达式的第二步,调用构造函数初始化内存时如果抛出异常,异常处理机制在栈展开(stack unwinding)时,要回收在new运算符表达式的第一步已经动态分配到的内存,这时就会 自动调用 对应
operator delete(void*)
函数。(注意:此处调用的是非位置delete函数)

struct E{};

class T{
public:
T(){
cout << "构造函数。" << endl;
throw E();
}

~T(){
cout << "析构函数。" << endl;
}

void * operator new(size_t sz){

T * t = (T*)malloc(sizeof(T));
cout << "内存分配。" << endl;

return t;
}

void operator delete(void *p){

free(p);
cout << "内存释放。" << endl;

return;
}
};

int main()
{

try {
T * p = new T;
/* do something */
}
catch (E exp){
std::cout << "Exception caught." << std::endl;
}

return 0;
}


结果:



3.2 数组形式的operator new/delete[](size_t size)函数

new type[] 运算符,用来动态创建一个对象数组。这需要调用数组元素类型内部定义的
void* operator new[](size_t)
函数来分配内存。如果数组元素类型没有定义该函数,则调用全局的
void* operator new[](size_t)
函数来分配内存。

#include <new>
中声明了
void* operator new[](size_t)
全局函数:

void * operator new [] (std::size_t) throw(std::bad_alloc);
void operator delete [](void*) throw();


3.3 placement new/delete 函数

void * operator new(size_t,void*)
函数用于带位置的 new 运算符调用。C++标准库已经提供了
operator new(size_t,void*)
函数的实现,包含
<new>
头文件即可。这个实现只是简单的把参数的指定的地址返回,带位置的new运算符就会在该地址上调用构造函数来初始化对象:

// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) throw() { return __p; }
inline void* operator new[](std::size_t, void* __p) throw() { return __p; }

// Default placement versions of operator delete.
inline void  operator delete  (void*, void*) throw() { }
inline void  operator delete[](void*, void*) throw() { }


禁止重定义这4个函数。因为都已经作为
<new>
的内联函数了。在使用时,实际上不需要
#include <new>


虽然上面的4个 placement new/delete 函数不能重载,但是仍然可以写一个自己的 placement new/delete 函数,例如 :

inline void* operator new(std::size_t, A * /* 或者 const A &*/);
inline void* operator new[](std::size_t, A * /* 或者 const A &*/);

inline void  operator delete  (void*, A* /* 或者 const A &*/);
inline void  operator delete[](void*, A* /* 或者 const A &*/);


但是,基本没有什么意义 ^_^。

3.4 保证不抛出异常的operator new/delete函数

C++标准库的
<new>
中还提供了一个nothrow的实现,用户可写自己的函数替代:

void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();

void operator delete(void*, const std::nothrow_t&) throw();
void operator delete[](void*, const std::nothrow_t&) throw();


3.5 Clang关于operator new/delete 的实现

以下这段代码是Clang编译器关于
operator new(std::size_t)
operator delete (void *)
的实现:

void * operator new(std::size_t size) throw(std::bad_alloc) {
if (size == 0)
size = 1;
void* p;
while ((p = ::malloc(size)) == 0) {
std::new_handler nh = std::get_new_handler();
if (nh)
nh();
else
throw std::bad_alloc();
}
return p;
}

void operator delete(void* ptr) {
if (ptr)
::free(ptr);
}


这段代码很简单,神秘的 operator new/delete 在背后也不过是在偷偷地调用C函数库的 malloc / free !当然,这跟具体实现有关,Clang libcxx 是这样实现,不代表其它实现也是如此。

需要意识到的是, operator new 和 operator + () 一样,只不过是普通的函数,是可以重载的,所谓的 placement new ,也是一个全局 operator new 的重载版本,在Clang libcxx 中定义如下:

inline _LIBCPP_INLINE_VISIBILITY void* operator new (std::size_t, void* __p) _NOEXCEPT
{
return __p;
}


四、小结

new 和 delete 是 C++ 用于管理 堆内存 的两个运算符。

new 运算符 进行动态内存申请的时候,包含 2 个阶段:

内存申请 new。

根据 Clang 的实现,我们可以猜测 内存new 基本就是通过 malloc 进行动态内存申请,但是本步骤并不初始化内存。本步骤对应
operator new(size_t )
函数。

构造函数。

delete 运算符 进行内存释放的时候,也包含 2 个阶段:

析构对象。

内存释放 delete。

本步骤对应
operator delete(void*)
函数。

除了用于内存管理的 new/delete 运算符,还有带位置的 placement new 运算符,但是没有带位置的 placement delete 运算符。

placement new 运算符

解决不能主动调用构造函数的“矛盾”。

对应的函数是
operator new(size_t , void *)


placement delete 运算符

没有此类运算符。

但有带位置的 placement delete 函数,如全局的
operator delete(void *,void*)


五、扩展 : free/delete 怎么知道有多少内存要释放 ?

参考 matthewgao github page

在使用c或者c++的时候我们经常用到malloc/free和new/delete,在使用malloc申请内存的时候我们给定了需要申请的内存大小,但是在free或者delete的时候并不需要提供这个大小,那么程序是怎么实现准确无误的释放内存的呢?

实际上,在申请内存的时候,申请到的地址会比你实际的地址大一点点,他包含了一个存有申请空间大小的结构体。

比如你申请了20byte的空间,实际上系统申请了48bytes的block

16-byte header containing size, special marker, checksum, pointers to next/previous block and so on.

32 bytes data area (your 20 bytes padded out to a multiple of 16))

这样在 free的时候就不需要提供任何其他的信息,可以正确的释放内存

这里有个在 stackoverflow.com 上的提问,可以参考

http://stackoverflow.com/questions/1518711/c-programming-how-does-free-know-how-much-to-free
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: