new/delete 详解

2021年6月17日 2点热度 0条评论 来源: 皮小猪的时光

一、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 函数)。^_^

解释:

  1. 首先看看 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 运算符。

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

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

  4. 总之,没有与 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表达式构建的对象,析构释放时有两种办法:

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

    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) ;
  2. 分两步显式 调用析构函数 与 带位置的 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 上的提问,可以参考

    原文作者:皮小猪的时光
    原文地址: https://blog.csdn.net/hihozoo/article/details/51441521
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。