C++new/delete与malloc/free的区别

一.new与delete

1. new与delete函数原型:

void *operator new(size_t); void *operator delete(void *)

2. new与delete的运行机制

class A{
public:
    A(int v) : var(v){
        fopen_s(&file, "test", "r");
  }
    ~A(){
        fclose(file);
  }
private:
    int var;
    FILE *file;
};
  • 当我们用new创建一个A类型的指针数组,new返回的是一个指向内存空间的A类型指针class * pA = new A(10);
  • 背后实则发生了这些事:
    C++new/delete与malloc/free的区别C++new/delete与malloc/free的区别
  • new的工作分为以下几点:
    (1)由于A类大小为8Bytes(参考《深入C++对象模型》),那么new函数会开辟一个8Bytes的地址空间,但内存空间没有初始化和类型化。
    (2)对内存空间进行类对象的初始化,调用构造函数,给private基本变量赋值,指针变量指向对应位置。
    (3)返回对象指针。
  • 当我们用delete释放掉类对象的时候delete pA,背后完成的工作:
    C++new/delete与malloc/free的区别
  • delete的工作分为以下几点:
    (1)调用指针所指向对象的析构函数,对打开的文件进行关闭
    (2)通过调用delete库函数来释放掉该对象的内存空间,传入的参数是指针pA,也就是对象的地址。

3.用new typep[]和delete []申请和释放数组

(1)申请和释放基本数据类型的数组空间

string *psa = new string[10];//array of 10 empty strings
int *pia = new int[10];//array of 10 uninitialized ints
delete []psa;
delete []pia;
  • 在上面的例子中,释放string类型数组空间时实际上先为10个string对象分别调用析构函数,再释放掉为10个string对象所分配的所有内存空间;而当释放int类型数组空间时,因int为内置类型不存在析构函数所以直接释放掉了为10个int类型变量分配的所有空间。
  • 这就造成了一个问题,非内置类型数据用new type[]来动态分配内存时,必须保存数据的维度,以确定在析构时需要调用对象析构函数的次数。而C++的解决方案为在分配数组空间时前面多分配了4Bytes大小的空间,专门保存数组的维度,在delete时根据数据维度调用析构函数,最后再释放所有内存空间。
  • 用一个例子来说明他们的运行机制,首先为对象数组分配内存空间:class A * pAa = new A[3];
    C++new/delete与malloc/free的区别
  • 释放对象数组内存空间时:delete []pAa;
    C++new/delete与malloc/free的区别
  • 调用析构函数的总次数是由调用new库函数时开辟的内存空间的前4Bytes中保存的数据决定的;调用delete []name库函数时,其参数不是指针name的值(也就是第一个数组元素的地址),而是这个地址值减去4Bytes。

4.内存泄漏

int *pia = new int[10];
delete []pia;
  • 这样写是没有问题的,但一旦把delete []pia;换成delete pia;,有问题吗?
    其实这也是没问题的,但不建议这样做,原因在于上面提到了在new[]时多分配4个字节的缘由,因为析构时需要知道数组的大小,但如果不调用析构函数呢(就例如这里的内置类型:int数组)?我们在new[]时就没必要多分配那四个字节,delete [] 时直接到第二步释放为int数组分配的空间。如果这里使用delete pia; 那么将会调用operator delete 函数,传入的参数是分配给数组的起始地址,所做的事情就是释放掉这块内存空间,不存在问题。
  • 但是,上述写法不存在问题有一个大前提是:对象的类型是内置类型或者是无自定义的析构函数的类类型!

  • 我们再看看带有自定义析构函数的类类型,用上述写法写时会发生什么?
class A *pAa = new class A[3];
delete pAa;
  • delete pAa;做了两件事:
    (1)调用一次pAa指向的对象的析构函数;
    (2)调用operator delete(pAa);释放内存。
    显然,后面的两个对象均未调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你在销毁数组对象时少调用了析构函数,造成内存泄漏。
  • 如果说上面的问题还不足以引起你的重视,第二点就非常致命了:直接释放pAa指向的内存空间,这个总是会造成严重的段错误(Segmentation fault),程序必然会崩溃。因为分配的空间的起始地址是pAa指向的地址减去4字节的地方,应将传入参数设为那个地址!
  • 总的来说,谨记:new/delete, new[]/delete[]配套使用。

二.malloc与free

  1. 头文件为#include <stdlib.h>,函数声明为: void* malloc(size_t size);
    (1)参数size_t size表示动态内存分配空间的大小,以字节为单位。
    (2)malloc()函数的返回值为一个指针,或者说是分配后内存空间的首地址。
    (3)如果malloc()申请空间成功则返回一段内存空间的首地址,失败则返回NULL。
    (4)返回指针类型需要强制类型转换(存在安全隐患)

  1. int *p = (int *)malloc(sizeof(int));
    (1)在使用malloc()函数申请的空间之前,最好用memset()函数把这段内存空间清理一下。malloc只能保证内存空间的大小,无法保证是否有垃圾数据。
    (2)同new/delete一样,malloc与free要配对使用,否则会内存泄漏。
#include <stdlib.h>
    char *p = (char *)malloc(sizeof(char));//定义一个char* 指针变量p,并分配10字节内存空间(字符数组) 
    memset(p, 0, 10 * sizeof(char));//将字符数组中的内容全部修改为0
    strcpy(p, "hello");//复制一段字符串到p指向的数组空间(长度要比所分配内存小)
    p = (char*)realloc(p, 20 * sizeof(char));//用realloc扩充p指向的内存空间
    strcat(p, "world");//在原字符串后拼接一段字符
    free(p);//释放空间

三.new/delete和malloc/free的区别

1.申请的内存所在位置

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆为操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配。
那么自由存储区是否能够是(等价于new是否在堆上动态分配内存),取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

  • 特别的,new甚至可以不为对象分配内存!定位new的功能可办到这一点:
    new (place_address) type
    place_address为一个指针,代表一块内存的地址。当使用上面这种仅以一个地址调用new操作符时,new操作符调用特殊的operator new,也就是下面这个版本:
    void * operator new(size_t, void *)//这个版本的operator new不允许重定义
    此operator new 不分配任何内存,只是简单的返回指针实参,然后右new表达式负责在place_address指定的地址进行对象的初始化工作。

2.返回类型安全性

  • new操作符内存分配成功时,返回的时对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符;而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void* 指针转换成我们需要的类型。
    而类型安全在很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己未被授权的内存区域。

3.内存分配失败时的返回值

new内存分配失败时,会抛出bad_alloc异常,不会返回NULL;malloc分配内存失败返回NULL。
使用C语言时,我们习惯在malloc分配内存后判断是否成功:

int *a = (int *)malloc(sizeof(int ));
if (NULL == a){
    ...
  }else{
    ...
}

走入C++阵营的新手可能把这个习惯带入:

int *a = new int(); 
if (NULL == a){
    ...
  }else{
    ...
}

实际这样毫无意义,因为new根本不会返回NULL,而且若程序能够执行到if语句已经说明内存分配成功,失败早就抛异常了,正确的做法要使用异常机制:

try{
    int *a = new int();
}catch (bad_alloc)
{
    ...
}

4.是否需要指定内存大小

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

class A{...};
A *ptr = new A;
A *ptr = (A *)malloc(sizeof(A)); 

5.是否调用构造函数/析构函数

使用new操作符分配对象内存经历三个步骤:

  • 一:调用operator new 函数(对于数组为operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  • 二:编译器运行相应的构造函数以构造对象,并为其传入初值。
  • 三:对象构造完成后,返回一个指向该对象的指针。
    使用delete操作符来释放对象内存会经过两个步骤:
  • 一:调用对象的析构函数。
  • 二:编译器调用operator delete(或operator delete[])释放内存空间。
    总的来说,new/delete会调用对象的构造函数/析构函数来完成对象的构造/析构。malloc则不会,例如:
class A{
public:
    A() : a(1), b(1.11){}
private:
    int a;
    int double;
};

总结

特征 new/delete malloc/free
分配内存的位置 自由存储区
内存分配成功返回值 完整类型指针 void*
内存分配失败返回值 默认抛出异常 返回NULL
分配内存的大小 由编译器根据类型计算得出 必须显式指定字节数
处理数组 有处理数组的new版本new[] 需要用户计算数组的大小后进行内存分配
已分配内存的扩充 无法直观的处理 使用realloc完成
是否相互调用 可以,看具体operator new/delete实现 不可调用new
分配内存时内存不足 客户能指定处理函数或重新制定分配器 无法通过用户代码处理
函数重载 允许 不允许
构造函数与析构函数 调用 不调用

四.placement new/delete

1.什么是placement new/delete?

如果operator new接受的参数除了一定会有的那个size_t之外还有其他参数,这便是个所谓的placement new。例如众多placement new版本中特别有用的一个“接受一个指针指向对象该被构造之处”
void* operator new(std::size_t, void* pMemory) throw();
这个版本的new已经被纳入STL,只需#include <new>。它的用途之一就是负责vector的未使用空间上创建对象。

  • 一般性术语“placement new”意味带任意额外参数的new。

2.为什么要用到placement new/delete?

记得在前面说过,new/delete区别于malloc/free很重要的一点就是他们会额外调用构造/析构函数,完成对象的构造和销毁。

  • 那么问题就来了,如果operator new内存分配成功,而为其分配内存的对象的构造函数抛出异常,运行期系统有责任取消operator new的分配并恢复旧观。而在实际中运行期系统可能不会那么智能,它只会寻找参数个数和类型都与operator new相同的某个operator delete。所以一个placement new一定要对应一个与之参数个数类型都相同的placement delete。
  • 如果一个带额外参数的operator new没有带相同额外参数的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用,就会出现内存泄漏

3.避免内存泄漏要集齐全部版本

delete pw;
如果你像上面这么写,调用的是正常形式的operator delete,而非其placement版本 。placement delete只会在伴随placement new调用而触发的构造函数出现异常时才会被调用,对一个指针(pw)绝不会调用,所以我们必须提供一个正常的operator delete(用于构造期间无任何异常抛出)和一个placement delete。

4.使用placement new/delete的一些注意事项

(1)成员函数的名称会掩盖其外围作用域中的相同名称,要避免让自己class专属的news掩盖客户期望的其他news。例如:

class Base{
public:
    ···
    static void* operator new(std::size_t size, std::ostream& logStream)//这个new掩盖正常的global形式
    throw(std::bad_alloc);
};
Base* pb = new Base;//error,正常形式operator new被掩盖
Base* pb = new(std::cerr) Base;//正确,调用Base的placement new

同理,derived classes中的operator news会掩盖global版本和继承而得的operator new版本

(2)缺省情况下C++在global作用域提供三种形式的operator new:

void* operator new(std::size_t) throw(std::bad_alloc);//normal
void* operator new(std::size_t, void*) throw();//placement
void* operator new(std::size_t, const std::nothrow_t&) throw();//nothrow

nothrow形式的operator new负责供应传统的C++:分配失败便返回NULL的行为。此形式颇为局限,只适用于内存分配,后继的构造函数调用还是可能抛出异常。

class A{···};
A* p2 = new(std::nothrow) A;//如果分配失败,返回0
if (p2 == 0)···//古老的侦测NULL的写法

原文链接: https://www.cnblogs.com/forlqy/p/15784947.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    C++new/delete与malloc/free的区别

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/184766

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 上午10:48
下一篇 2023年2月12日 上午10:48

相关推荐