new、delete和malloc、free详解与混用问题

如有错漏,还望指摘!


又更新一下:
这几天工作涉及到了重载new、new[]、delete、delete[]操作符的问题,突然发现之前研究的这个还挺有用的,因此重新编辑了一下。


更新一下:
最近回过头来重新看这篇文章,发现这里有一点小问题:
C++标准规定,这些是不可以混用的,会导致未定义行为。而这篇文章所讨论的行为,只是当前的编译器(或者说我使用的编译器)为了实现功能而自己定义的行为。随着版本的更新,有可能会有改动(不过我觉得可能性不大)。因此,这篇文章的内容只是帮助理解编译器如何实现的new[]和delete[],并不作为混用的解决方案。
不建议混用!
不建议混用!
不建议混用!

另外,我当时使用的VS2017做的测试,不确定linux平台下是否有同样的行为,回头我测试一下再看是否需要更新一下。


正文:

在C中,不存在class的概念,分配内存只需要简单的malloc即可。
而到了C++中,出现了class以及class的初始化,因此相应的出现了新的操作符new和delete。
那么新的操作符new、new[]、delete、delete[]究竟和C中的malloc和free有什么区别呢?又是怎样实现的呢?各种教材都明确说明,这些分配和析构必须对应使用,不可混用,这又是为什么呢?

malloc和free

其实malloc和free非常简单,malloc就是向系统申请一块指定大小的内存,free则是向系统归还指定地址开始的一块内存。当然更底层的实现就比较复杂了,在这里先挖个坑,将来有机会写一篇malloc和free的底层原理。
不过可以理解为去向图书馆借书一样,管理员会记录下借给你的内存起始地址。归还时则拿着起始地址归还,管理员找到对应的记录,然后标注为已归还,这样就可以继续借给他人。

new和malloc的比较

C++中的new操作符其实分为两个步骤:

  1. operator new
  2. placement new

这其实很好理解,因为我们从功能上来说也可以明白,c++的new相比C中的malloc而言,其实是做了两件不同的事情,分别是:

  1. 分配内存;
  2. 调用构造函数。

这其实也就是上面两个步骤分别做的事情。

而其中第一步operator new允许重载,其默认实现,其实就是调用了malloc来实现内存分配的。因此C++中的new和C的malloc的区别其实就是,new在malloc的基础上多了一步调用构造函数

delete和free的比较

我们知道new和malloc的区别之后,其实应该就比较好理解delete和free的区别:
delete比free多了调用析构函数的过程
和new一样,delete的第一步operator delete同样可以重载。而相较于new的第二步有一个特殊的placement new不同,delete在第二部直接调用了析构函数(因为析构函数是可以被直接调用的)。

new[]和delete[]

理解了普通的new和delete之后,我们再来看new[]和delete[]有多出了什么样的功能呢?
从功能上来说,new[]是说分配一个连续的数组并调用相应次数的构造函数。从这里来看和new的区别只有调用构造函数的次数不同而已。
但是delete[]的功能是,调用相应次数的析构函数,并释放整体的空间。这里就出现了一个问题,delete[]如何知道需要调用多少次析构函数呢?因此就必须仰赖new[]在构建时,就需要在内存空间中存储一个次数的信息,以便delete[]在析构时,可以正确调用对应次数的析构函数。

一般编译器是这么做的:
new[]在分配时,如果类中显式定义了析构函数,new会在分配的时候,根据系统的位数额外分配对应的空间(32位系统分配32位空间,也就是4字节,64位系统分配64位空间,也就是8字节)。如对于32位系统,new[2]分配的空间应该如下:
new[]地址分配示意图
在返回的指针之前,还有一个4字节的header,其中存储了分配的个数,如这里就应该是2。

总结

  1. new相较于malloc多了调用构造函数的步骤,称为placement new;
  2. delete相较于free多了调用析构函数的步骤;
  3. new[]相较于new,额外分配了一段空间用来存储数组的长度,而delete[]则根据这个长度来正确地调用对应次数的析构函数;
  4. 对于构造函数没有作用的类,因为不需要调用构造函数,new和malloc可以混用。
  5. 对于构造函数有作用的类,如果想混用,需要显式调用构造函数的逻辑实现。
  6. 对于没有显式定义析构函数的类,delete、delete[]和free可以混用。
  7. 对于显式定义析构函数的类,delete[]和new[]必须配套使用,delete和free如果想混用,free需要显式调用析构函数。
  8. 如果不按规则混用了,程序会崩溃。因为显然,比如使用delete来析构new[]申请的空间,那么delete析构时使用的地址,并不是向操作系统申请内存时获取的地址,而是向后挪动过的地址,操作系统将无法找到对应的内存块。其他混用同理。

补充

对于上面说的new,分配内存,即operator new这一步在c++中是允许全局重载和类内重载的,而第二步palcement new则不允许。operator delete同样允许重载。
这在很多时候非常有用,比如使用自定义的内存池时可以将new操作符中的分配内存替换成从内存池中获取空间;又比如分配内存时可以打印自己想要的信息。

这里重载更推荐类内重载,因为全局重载会污染整个项目,在操作不当或者疏忽的情况下可能导致不可预知的后果。
类内重载时,虽然operator new是静态的,但是子类依然可以继承父类的实现,也可以覆盖。
new运算符返回值要求必须是void*。参数只有申请空间的大小:

class DemoClass
{
public:
	DemoClass(int id) id_(id) {}
    void* operator new(size_t size);
    void operator delete(void* ptr);
private:
    int id_;
};

void* DemoClass::operator new(size_t size)
{
    //使用自定义的allocator进行内存分配
    std::cout << "Using my allocator" << std::endl;
    return my_allocator::allocate(size);
}

void* DemoClass::operator delete(void* p)
{
    // 使用自定义的allocator进行内存释放。
    std::cout << "Using my deletor" << std::endl;
    return my_allocator::free(p);
}

int main(){
    // 此时在构造和析构Demo类时会自动调用重载后的new和delete实现内存的分配与释放。
    auto ptr = new Demo();  // Using my allocator
    delete ptr;             // Using my deletor
}

调用placement new则需要引用new头文件并且调用方式为:
new (地址指针) 类名(参数);
比如:

#include <new>

class DemoClass
{
public:
	DemoClass(int id) id_(id) {}
private:
    int id_;
};

int main()
{
    char* buffer = new char[100];
    DemoClass* ptr = reinterpret_cast<DemoClass*>(buffer);
    for(int i = 0; i < 5; ++i, ++ptr)
    {
        new (ptr) DemoClass(i);
    }
}

混用代码:

//构造函数和析构函数都没有作用。可以随意混用。
class Test1{
public:
    Test1(){}
}

int main(void){
    /*
    这里用任意方式分配的内存都可以用任意方式释放
    */
    //Test1 *ptr = new Test1;
    Test1 *ptr = new Test1[3];
    //Test1 *ptr = (Test1*)malloc(sizeof(Test1));
    
    //delete ptr;
    //delete[] ptr;
    free(ptr);
}
//构造函数有作用。
//没有显示定义析构函数,因此delete、delete[]、free可以混用
#include <new>
class Test2{
public:
    Test2()
    {
        init();
    }
    void init()
    {
        cout << "constructor" << endl;
    }
}

int main(void){
    //Test2 *ptr = new Test2;
    //Test2 *ptr = new Test2[3];
    /*
    这里如果想用malloc,必须显示调用构造函数的逻辑。
    注:由于构造函数无法被显式调用,因此如果不想调用placement new的话必须另外实现一个inti函数。
    */
    Test2 *ptr = (Test2*)malloc(sizeof(Test2));
    new (ptr) Test2();
    // ptr->init();
    
    //delete ptr;
    delete[] ptr;
    //free(ptr);
}
//构造函数没有有作用。new、new[]、malloc可以混用
//显示定义析构函数
class Test3{
public:
    Test3(){}
    ~Test3(){
        cout << "destructor" << endl;
    }
}

int main(void){
    //Test3 *ptr = new Test3;
    Test3 *ptr = (Test3*)malloc(sizeof(Test3));
    
    //delete ptr;
    /*
    必须显示调用析构函数
    */
    ptr->~Test3();
    free(ptr);

    /*
    只要定义了析构函数,即使析构函数内是空的,delete[]和new[]也必须配套使用,否则会一直调用析构函数(因为将前面的几个字节乱码,当成了数组长度),并在最后释放空间时崩溃。
    */
    //Test3 *ptr = new Test3[3];
    //delete[] ptr;
}
//对于32位系统,new[]和delete[]的非配套使用。
//显示定义析构函数

#include <new>

class Test3{
public:
    Test3(){}
    ~Test3(){
        cout << "destructor" << endl;
    }
}

int main(void){
    int n = 3;
    /*
    new[]和free混用
    */
    Test3 *ptr1 = new Test3[n];
    //模拟delete[]
    for(int i = 0; i < n; i++){
        (ptr1 + i)->~test3();
    }
	free(((char*)ptr1) - sizeof(void*));

    /*
    malloc和delete[]
    */
    //模拟new[]
    void* buffer = malloc(sizeof(void*) + n * sizeof(Test3));
    Test3* ptr2 = (Test3*)((char*)buffer + sizeof(void*));
    if(sizeof(void*) == 4)
        *((int32_t*)buffer) = n;
    else if(sizeof(void*) == 8)
        *((int64_t*)buffer) = n;
   
	for(int i = 0; i < n; ++i){
        new (ptr2 + i) Test3();
    }
    
	delete[] ptr2;
}
  • 6
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值