new/delete 详解

转载from:   https://blog.csdn.net/hihozoo/article/details/51441521

一、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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
结果如下: 
这里写图片描述




每个 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
对于 ①,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; // 分配内存,但未初始化,垃圾值
1
通过new初始化对象,使用下述语法:


type  * p_var = new type(init); // int * a = new int(8); //分配内存时,将 *a 初始化为 8
1
其中 init 是传递给构造函数的实参表或初值。


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


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


type  p_var = new type [size]; // int * a = new int[3] ; // 分配了 3个 int 大小的连续内存块, 但未初始化
1
C++98 标准规定,new 创建的对象数组不能被显式初始化, 数组所有元素被缺省初始化。如果数组元素类型没有缺省初始化(默认构造函数),则编译报错。但 C++11 已经允许显式初始化,例如:


int *p_int = new int[3] {1,2,3};
1
如此生成的对象数组,在释放时必须调用 delete [] 表达式。


2.1.2 placement new 运算符表达式


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


placement new 的语法是:


new ( expression-list ) new-type-id ( optional-initializer-expression-list );
1
使用这种 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
对于上述代码,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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
如上代码,如果把 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 );
1



new (nothrow) Type[size]; // new (std::nothrow_t) Type[size];
1
其中 nothrow 是 std::nothrow_t 的一个实例.






2.2 delete 表达式语法


2.2.1 内存释放


1)普通的 delete 运算符


delete 的基本语法是:


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


delete [] val_ptr
1
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) ;
1
2
3
4
5
6
7
8
9
10
分两步显式 调用析构函数 与 带位置的 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 &) .
1
2
3
4
5
6
7
8
带位置的 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 ;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
结果如下: 
这里写图片描述


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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
答案:不会产生内存泄漏。


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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
答案:会造成内存泄漏。


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();
1
2
内存需要回收的话,调用对应的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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
结果: 
这里写图片描述


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();
1
2
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() { }
1
2
3
4
5
6
7
禁止重定义这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 &*/);
1
2
3
4
5
但是,基本没有什么意义 ^_^。


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();
1
2
3
4
5
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这段代码很简单,神秘的 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;
}
1
2
3
4




四、小结


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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值