记录C++内存管理(malloc和free中的cookie)

new和delete

在说array new和array delete之前,先说一下new和delete。在C++中,使用new和delete来进行动态内存的创建和销毁,而new和delete的底层其实是调用的malloc和free函数,并且会为其调用构造函数和析构函数。

new会被编译器编译成下面两个步骤:

  • 调用operator new函数申请内存,operator new内部其实就是调用的malloc函数。
  • 调用类的构造函数,在申请的内存处构造对象。

delete也会被编译器编译成下面两个步骤:

  • 首先调用对象的析构函数。
  • 调用operator delete释放内存,operator内部调用的就是free函数。

array new和array delete

array new和array delete就是动态申请多个对象的内存,用法如下:

// 代码示例一
class Demo
{
private:
    int a, b, c;

public:
    Demo() : a(0), b(0), c(0) {}
    ~Demo() 
    {
        cout << "destructor" << endl;
    }
};
Demo *p = new Demo[3];
delete[] p;

上述语句中,new部分:

  • 调用了operator new[]函数
  • 调用3次构造函数

而delete部分:

  • 调用三次析构函数
  • 调用了operator delete[]函数。

上述提到的operator new和operator delete函数都是可以进行重载的,但是new和delete是不支持重载的。

malloc的cookie机制

在malloc为用户分配内存的时候,除了分配用户本身的内存,还会在内存前后加上两个cookie,来记录分配了多少内存,这样在调用free函数的时候才能准确的回收内存。因此每次调用malloc函数都会产生cookie消耗。

而new操作符中的array new,为了记录需要调用多少次析构函数,会在分配的内存前记录分配了多少个对象。下面的图更好理解一点:

malloc和new的内存分布布局

上图展示了我们在运行代码示例一之后分配的内存,其中61H是malloc设置的cookie,表示malloc总共分配给用户的内存大小;而00481c30地址所指的3,则是new设置的其分配对象的个数。也就是说,每次调用new操作符的时候,都会分配一些额外的内存来存放所分配内存信息。

下面使用代码来展示一下operator new[]和operator delete[]的具体操作。

operator new[]和operator delete[]

首先来看下面这段代码:

// 代码示例二
#include <iostream>

using namespace std;

class Demo
{
private:
    int a, b, c;

public:
    Demo() : a(0), b(0), c(0) 
    {
        cout << "constructor" << endl;
    }
    ~Demo() 
    {
        cout << "destructor" << endl;
    }
};

int main()
{
    Demo *p = new Demo[3];
    delete[] p;
    return 0;
}
/*
输出:
constructor
constructor
constructor
destructor
destructor
destructor
*/

上面的代码示例二展示了array new和array delete的用法,可以看到,其中调用了3次构造函数和3次析构函数。如果我们将delete[] p改为delete p,结果会是什么样呢?

将delete[]改为delete

delete[] p改为delete p之后,会出现上图的错误,invalid pointer。为什么会出现这种情况呢,前面我们提到过,在使用new操作符分配一个数组时,会在分配的数组前面多分配几个字节(视环境而定侯捷老师说是4个字节,但是在我的环境下面是8个字节),再来看一下前面那个图:

我们在运行了代码示例2之后,实际分配的内存如上图所示(仅仅是为了说明,地址并不准确)。在运行Demo *p =new Demo[3]之后,返回的指针p是0x00481c34,在00481c34之前还有用来存放对象数量的内存,4个字节或者8个字节,图中展示为8个字节。

在调用delete[] p的时候,会调用operator delete[]函数,而传入operator delete[]函数的指针其实是从0x00481c30开始的,并不是从对象真正的地址开始,因为new[]申请的内存是从0x00481c30开始的。我们用下面的程序验证:

#include <iostream>

using namespace std;

class Demo
{
private:
    int a, b, c;

public:
    Demo() : a(0), b(0), c(0) 
    {
        cout << "constructor" << endl;
    }
    ~Demo() 
    {
        cout << "destructor" << endl;
    }
};

inline void operator delete(void *p)
{
    cout << "address " << p << endl;
}

inline void operator delete[](void *p)
{
    cout << "address " << p << endl;
}

int main()
{
    Demo *p = new Demo[3];
    cout << p << endl;
    delete[] p;
    return 0;
}

上面的程序中,我们重载了全局的operator delete和operator delete[]函数。从上面的输出我们可以看出,new Demo[3]返回的指针地址0x20f4c28,但是传入operator delete[]中的指针地址时0x20f4c20,刚好相差8个字节,并且那8个字节中的值是3,刚好的我们创建的对象的数量。因此这个就可以解释前面的那张图。

那为什么直接调用delete p会出错呢,因为调用delete的时候,编译器将其作为一个普通的指针进行处理,即没有考虑其前面所占的用于记录对象数量的内存空间,我们再次使用上面的代码,将delete[] p改为delete p,输出会变成下图:

从上图中可以看到,此时只调用了一次析构函数,我们传入operator delete的指针地址和指针p相同,也就是说,指针p前面的用于记录对象个数的那片内存被忽略了,因此如果此时在operator delete里面调用free函数的话,会出现前面invalid pointer的错误。并且如果在创建类的时候,构造函数动态申请了内存,如果只调用delete的话会造成内存泄漏,因为只释放了一个对象申请的内存。因此我们在调用了new []之后一定要使用delete []

特殊情况

上面所说的情况只针对有自定义析构函数的情况,如果你定义的类的析构函数是trivially的,即不重要的(编译器默认),或者数据类型是POD类型,那么此时调用delete或者delete[]是不会有什么差别的。因此此时并不会像之前一样用额外的字节数来存储分配对象的个数,因为其析构函数调用或者不调用是没有区别的。我们再用一个程序来展示:

#include <iostream>

using namespace std;

class Demo
{
private:
    int a, b, c;

public:
    Demo() : a(0), b(0), c(0) 
    {
        cout << "constructor" << endl;
    }
    /*
    ~Demo() 
    {
        cout << "destructor" << endl;
    }
    */
};

inline void operator delete(void *p)
{
    cout << "address " << p << endl;
}

inline void operator delete[](void *p)
{
    cout << "address " << p << endl;
    // cout << *(int*)p << endl;
}

int main()
{
    Demo *p = new Demo[3];
    cout << p << endl;
    delete[] p;
    return 0;
}

上述代码中,我们将类Demo的析构函数注释掉,此时类Demo的析构函数就是trivially的,因此在我们调用delete []的时候,其传入的指针和p是相等的,因为operator new[]并没有申请多余的内存来存储对象的数量,因此此时如果调用delete,是可以直接释放内存,不会出错。

总结

在调用new来分配数组时(new Deno[n]),如果类的析构函数是重要的,则会多申请4个字节或者8个字节来存储申请对象的数量,以供编译器知悉在调用delete[] p的时候需要调用几次析构函数之后再释放内存,避免内存泄漏,并且此时必须调用delete [],否则会出错。如果类的析构函数是不重要的,则不会申请额外的内存存储申请对象的数量,此时调用几次析构函数并不会有什么影响,因此此时也可以调用delete直接释放内存,不会出错。

猜你喜欢

转载自blog.csdn.net/qq18218628646/article/details/132475843