分析new 与 malloc的不同,以及如何使用free()、delete、delete[]释放资源。

在阅读本篇文章之前,我们先带着几个问题思考一下:

  1. free可以对new的空间进行释放吗?
  2. delete可以释放new出的数组空间吗?
  3. 什么情况下可以使用free或delete释放,却不能使用delete[]释放?
  4. 什么情况下可以free、delete、delete[] 三种方法进行资源的释放?

让我们带着疑问阅读了下面的内容,希望在您阅读了以下内容后,能够有所收获,而以上几个问题的答案就出现在本篇文章中。


一、malloc函数只申请资源,不调用构造

1.1 空指针访问类成员方法

示例一:试问以下代码会不会产生错误?如果产生了错误,请指明是哪几行,并说明错误原因。

class Object
{
private:
	int value;
public:
	Object(int x = 0) :value(x) { cout << " construct object :" << this << endl; }
	~Object() { cout << "deconstruct object : " << this << endl; }
	static void show() { cout << "0bject : : show" << endl; }
	void print() { cout << "Object : : print" << endl; }
	void print_value() { cout << value << endl; }
};
int main()
{
	Object* op = nullptr;
	op->show();
	op->print();
	op->print_value(); 
	return 0;
}

分析
在以上代码中出现了三个类成员函数函数,依次对齐分析如下:

  • show():该函数是静态成员方法,可以不依赖对象调用,因此可以说与对象无关。那么op->show();不会产生错误。
  • print():该函数是类成员方法,但该方法的实现中并没有访问类对象的成员,我们都知道类成员方法的存储是不占用类对象的存储空间的,因此’op->print();` 也不会缠身错误。
  • print_value():该函数与print()一样同属类成员方法,而在该方法的实现中访问了类对象的成员value。具体行为是,通过this指针解引用访问value。而我们可以看到在main函数中,Object* op = nullptr;,那么当函数执行到op->print_value();时就会访问 0x00000000位置(NULL),该位置属于系统保留区不提供访问,因此会造成程序崩溃。
1.2 使用malloc函数申请资源

示例二:使用malloc申请资源,可以看到在类的构造函数中存在默认值,而malloc申请资源是并不能进行赋值。

那么试问以下的代码会不会出现错误?如果会出现错误请说明原因,如果不会出现错误请分析输出结果。

// 注:此处类的设计,与实例一相同
class Object
{
private:
	int value;
public:
	Object(int x = 0) :value(x) { cout << " construct object :" << this << endl; }
	~Object() { cout << "deconstruct object : " << this << endl; }
	static void show() { cout << "0bject : : show" << endl; }
	void print() { cout << "Object : : print" << endl; }
	void print_value() { cout << value << endl; }
};
int main()
{
	Object* op = nullptr;
	op = (Object*)malloc(sizeof(Object));	// malloc
	op->show();
	op->print();
	op->print_value(); 
	return 0;
}

分析:以上代码不会出现错误,其中op->print_value()输出结果为 -842150451
解释:这里的 print_value() 函数输出了 “CDCDCDCD”(-842150451) ,因为 malloc 只分配空间,不调用对象的构造函数。而堆区申请的空间默认填充的值为 0xCD,由于没有调用构造函数的关系,value的值为申请空间是的默认值,因此输出了0xCDCDCDCD

1.3 空间与对象间的赋值

由于malloc申请的资源没有调用构造函数,没有生成完整的对象,因此通过malloc申请的资源只能是一片空间而非一个对象。

示例三:对象给空间赋值。由于该空间是由一个 Object 类型指针所指向的,因此在通过->成员访问符访问类成员是可以的,而类中存在有隐藏的赋值函数。因此,该空间是可以合法调用Object::operator=()函数的。

试问:在以下代码中,是否存在错误,如果存在错误请指明错误原因,如果不存在错误请分析输出结果。

// 注:此处类的设计,与实例一相同
class Object
{
private:
	int value;
public:
	Object(int x = 0) :value(x) { cout << " construct object :" << this << endl; }
	~Object() { cout << "deconstruct object : " << this << endl; }
	static void show() { cout << "0bject : : show" << endl; }
	void print() { cout << "Object : : print" << endl; }
	void print_value() { cout << value << endl; }
};
int main()
{
	Object obj(10);
	Object* op = (Object*)malloc(sizeof(Object));	// malloc
	*op = obj;
	op->show();
	op->print();
	op->print_value(); 
	return 0;
}

分析:程序运行并无明显错误,输出结果中op->print_value();输出了与 obj对象 中的value值相同的结果。
解释:无明显错误是指,至少在当前的程序中没有出现错误,但是任然存在隐患。而输出结果也不难分析,因为 op 指向的空间调用了Object的赋值函数,将 obj 对象的value值赋值到 op 所指向的空间中,而在输出时将该值输出。

1.4 避免空间与对象间进行赋值

在1.3中提到,空间与对象间赋值存在隐患,下面将演示这中场景。

示例四:将类中的某方法设置为虚函数。我们都知道一旦类中某个成员方法设计为虚函数,那么该类就要生成对应的虚表,存储虚函数的地址信息。

试问:观察以下代码,是否存在错误,如果存在请指出错误原因,如果不存在请分析输出结果。

// 注:此类的设计中,把print设置为虚函数
class Object
{
private:
	int value;
public:
	Object(int x = 0) :value(x) { cout << " construct object :" << this << endl; }
	~Object() { cout << "deconstruct object : " << this << endl; }
	static void show() { cout << "0bject : : show" << endl; }
	virtual void print() { cout << "Object : : print" << endl; }
	void print_value() { cout << value << endl; }
};
int main()
{
	Object obj(10);
	Object* op = (Object*)malloc(sizeof(Object));	// malloc
	*op = obj;
	op->show();
	op->print();
	op->print_value(); 
	return 0;
}

分析:程序运行到op->print();语句时崩溃。
解释:由于op指针指向的空间不是完整的对象,因此该空间只是与Object对象的空间大小相等,在调用赋值函数时也只会对成员变量进行赋值。而op中不存在虚表指针,或者说该虚表指针所表示的区域从为被初始化过。因此,在调用op->print(); 时由于无法通过虚表指针找到 print 函数的地址而引发程序的崩溃。

在这里插入图片描述
需要注意的是,虚表的设计和虚表指针的初始化都发生在调用构造函数的过程中,而malloc函数在申请资源后并不会调用对象的构造函数,因此使用malloc申请的空间是对象不完整的。

1.5 使用重定位new,在已有空间的基础上构建对象

在1.4中由于没有调用构造函数这一步操作,对象并不完整,因此只能当做一片空间来使用。而只有在空间的基础上调用了构造函数后才能称之为对象。

因此我们可以这样做,使用operator new进行重定位构建对象。

示例五:代码如下

class Object
{
private:
	int value;
public:
	Object(int x = 0) :value(x) { cout << " construct object :" << this << endl; }
	~Object() { cout << "deconstruct object : " << this << endl; }
	static void show() { cout << "0bject : : show" << endl; }
	virtual void print() { cout << "Object : : print" << endl; }
	void print_value() { cout << value << endl; }
};
int main()
{
	Object obj(10);
	Object* op = (Object*)malloc(sizeof(Object));	// malloc
	new(op) Object(20);	// 重定位new,在已有空间的基础上构建对象
	op->show();
	op->print();
	op->print_value(); 
	return 0;
}

输出结果:
在这里插入图片描述

二、new关键字先申请空间,后调用构造

有关new与delete的使用,相信大家都很熟悉了,这里就不再赘述。关于new的使用方法可以参考《C++ | 关键字new 与 delete ——动态申请空间》这篇文章。

对于这对关键字,主要有以下两种用法:

  1. 对于单个对象
    • new:申请单个对象
    • delete:销毁单个对象
    • 初始化:可以通过new type(val)的形式初始化。
  2. 对于多个对象
    • new[] :申请对象数组
    • delete[] :销毁对象数组
    • 初始化:可以使用 new type[cnt]() 的方式令所有对象初始化为0,或使用 new type[cnt]{v1, v2, v3 ...}的方式对每个对象初始化。
2.1 对于内置类型,使用new[] 申请的资源可以使用 delete 或 delete[] 释放资源

一般来说,使用new[] 创建的数组对象都需要使用delete[] 的方式进行释放。而对于内置类型来说,使用 delete 或 delete[] 的释放方式都可以。

int main()
{
	int* pi = new int();
	int* pis = new int[5]{1,2,3,4,5};
	
	delete pi;
	delete pis;
	return 0;
}

甚至,使用 free() 的方式释放也是可以的。

free(pi);
free(pis);

需要注意的是,对于内置类型来说,他们不存在构造函数和析构函数,在使用new时也只是申请了空间,而后忽略其调用构造函数这一步,而delete在释放资源后,也不会调用析构函数。

而对于使用 new[] 的方式申请的数组为什么可以使用 delete 或 free() 来释放资源这件事,请继续往下看。

2.2 类中没有析构函数,就可以使用 delete 释放 new[] 申请的对象数组

例如:此代码不会产生任何问题。

class Object
{
public:
	Object() {}
	//~Object();
};

int main()
{
	Object* pobj = new Object[10]();
	delete pobj;
	return 0;
}

可以看到我们使用new[] 创建了堆区数组,不过由于我们的类中不存在析构函数,因此调用delete关键字析构数组不会产生任何问题。这就等同于int* p = new int[10]; delete p;不会产生任何问题一样。

我们都知道,类中有隐藏的几个函数,我们不实现时编译器会自动帮我们实现。而实际上这样的说法是不严谨的,就如上面的例子而言,编译器的确实现了一个Object类的析构函数,但并没有进行实际的调用这步操作。

而如果该类继承的基类中有析构函数,或者自身的成员对象有析构函数,最终都会有实际的析构函数的调用这一步操作。那么此时使用delete 释放数组资源就会出错。

class Base
{
public:
	~Base() {}
};
class Object :public Base	// 基类中有析构
{
};
int main()
{
	Object* pobj = new Object[10]();
	delete pobj;	// errror

	return 0;
}
class Base
{
public:
	~Base() {}
};
class Object
{
private:Base bs;		// 成员对象有析构函数
};
int main()
{
	Object* pobj = new Object[10]();
	delete pobj;	// errror

	return 0;
}
问题4:什么情况下可以free、delete、delete[] 三种方法进行资源的释放?

也就是说,只要我们的类最终不调用析构函数,delete 就可以实现对单个堆对象或多个堆对象使用,甚至是使用 free() 释放也是可以的。

class Object
{
public:
	Object() { cout << "c" << endl; }
	//~Object() { cout << "d" << endl; }
	virtual void show() { cout << this << endl; }
};


int main()
{
	Object* pobj = new Object[10]();
	// 以下任选其一皆可
	delete pobj;
	free(pobj);
	delete []pobj;

	return 0;
}
问题3:什么情况下可以使用free或delete释放,却不能使用delete[]释放?

而当我们类中存在析构时,free 与 delete 均可对单个的堆对象释放资源,只是free不会调用对象的析构。而我们此时使用 delete[] 释放数组资源就会出错。

class Object
{
public:
	Object() { cout << "c" << endl; }
	~Object() { cout << "d" << endl; }
	virtual void show() { cout << this << endl; }
};


int main()
{
	Object* pobj = new Object();
	// 前两个皆可
	delete pobj;
	free(pobj);		// 可以释放资源,但不会调用析构函数
	delete []pobj;	// errror

	return 0;
}
2.3 new[] 资源的申请与 类的析构函数 之间的关系

而究其原因是因为在类中存在析构函数时,申请的资源中有额外的4个字节空间,代表此次申请的对象个数。 delete[] 会依据这对象个数对空间进行逐个的释放。
在这里插入图片描述
而我们使用new创建单个对象时,就不会产生记录对象个数的标志位,因此delete和free都可以对该资源释放。但是delete[] 释放时,会检查当前对象空间的前四个字节,以确定释放的对象个数,因此这里不能用delete[] 释放单个资源,否则delete会把边界标志0xFDFDFDFD当做对象个数而进行多次的释放,从而引发错误。
在这里插入图片描述

问题2:delete可以释放new出的数组空间吗?

答案当然是可以的,根据上面的分析可知,只要类中不存在析构函数,delete在释放资源时就不会发生错误。即当申请的对象空间中,没有额外的空间用于统计对象个数的标志位,此时便可以使用 delete 释放该篇连续的数组空间。
在这里插入图片描述

问题1:free可以对new的空间进行释放吗?

解答:一般来说不可以,参考《https://docs.microsoft.com/zh-cn/cpp/cpp/new-and-delete-operators?view=msvc-160》中提到new运算符为来自称为“自由存储”的池中的对象分配内存,而malloc明确定义是从“堆”上申请空间。

例如,new可以从已有变量的空间上分配资源。

int main()
{
	int a = 0x00010001;
	short* psh = new(&a) short;
	cout << a << " "		// 65537
		<< *psh << endl;	// 1

	*psh = 0;
	cout << a << " "		// 65536
		<< *psh << endl;	// 0
}

但是在某些情况下,free()是可以释放new的资源的。比如new在堆区申请的不带标志位的空间可以使用free()释放,参考前文提到的示例。

因为尽管使用不同的内存分配算法来实现new和malloc是合法的,但是在大多数系统上,new的内部是使用malloc实现的,不会产生系统级的差异。


附:new/delete与malloc/free的异同(参考StackOverflow)

new/delete是C++式的写法,malloc/free是C式写法,并且相对于malloc来说new大致具有以下的优点:

  1. 更安全,主要表现在:
    1. new内存分配失败,则会引发异常,并非返回一个NULL指针。
    2. new是类型安全的,返回确切的类型,而非(void*)指针。
    3. new通过调用该对象的构造函数来构造对象,而并非通过(type*)进行类型转换。
  2. 更方便:主要表现在:
    1. 申请对象时自动调用构造函数,释放对象时自动调用析构函数
    2. new是运算符,可以重载,而malloc是函数,不能重载
    3. new可以在申请时做初始化,而malloc只能先申请空间后进行赋值。
    4. 使用new[] 分配数组,比malloc更直观。

除此之外,new还能做到很多malloc无法完成的事,具体可以参考一下几篇文章。
1. https://docs.microsoft.com/zh-cn/cpp/cpp/new-and-delete-operators?view=msvc-160
2. What is the difference between new/delete and malloc/free?
3. In what cases do I use malloc and/or new?

以下引用自上述连接,有关new/delete与malloc/free的相关资料。
在这里插入图片描述
new/delete

  • Allocate/release memory
    1. Memory allocated from ‘Free Store’
    2. Returns a fully typed pointer.
    3. new (standard version) never returns a NULL (will throw on failure)
    4. Are called with Type-ID (compiler calculates the size)
    5. Has a version explicitly to handle arrays.
    6. Reallocating (to get more space) not handled intuitively (because of copy constructor).
    7. Whether they call malloc/free is implementation defined.
    8. Can add a new memory allocator to deal with low memory (set_new_handler)
    9. operator new/delete can be overridden legally
    10. constructor/destructor used to initialize/destroy the object

malloc/free

  • Allocates/release memory
    1. Memory allocated from ‘Heap’
    2. Returns a void*
    3. Returns NULL on failure
    4. Must specify the size required in bytes.
    5. Allocating array requires manual calculation of space.
    6. Reallocating larger chunk of memory simple (No copy constructor to worry about)
    7. They will NOT call new/delete
    8. No way to splice user code into the allocation sequence to help with low memory.
    9. malloc/free can NOT be overridden legally

Table comparison of the features:

 Feature                  | new/delete                     | malloc/free                   
--------------------------+--------------------------------+-------------------------------
 Memory allocated from    | 'Free Store'                   | 'Heap'                        
 Returns                  | Fully typed pointer            | void*                         
 On failure               | Throws (never returns NULL)    | Returns NULL                  
 Required size            | Calculated by compiler         | Must be specified in bytes    
 Handling arrays          | Has an explicit version        | Requires manual calculations  
 Reallocating             | Not handled intuitively        | Simple (no copy constructor)  
 Call of reverse          | Implementation defined         | No                            
 Low memory cases         | Can add a new memory allocator | Not handled by user code      
 Overridable              | Yes                            | No                            
 Use of (con-)/destructor | Yes                            | No  

Technically memory allocated by new comes from the ‘Free Store’ while memory allocated by malloc comes from the ‘Heap’. Whether these two areas are the same is an implementation detail, which is another reason that malloc and new can not be mixed.


2021.5.14更新…

1、以下内存申请与释放配套使用:
  • malloc/free : 只申请/释放空间
  • new/delete : 先申请空间,再调用一次构造/先调用析构,再释放空间
  • new[]/delete[] : 先申请空间,在调用多次构造/先调用多次析构,在释放空间
2、delete[] 函数的特点

而我们可以认为,free/delete/delete[] 中释放空间的功能效果是等同的区别在于他们对于对象析构函数的调用

其中,delete[] 在 对象存在析构函数与不存在析构函数时,分别采用不同的措施。

  • 对象存在析构函数,delete[] 会首先检查所申请空间首部的标志位用以确定需要调用的析构函数的次数
  • 对象不存在析构函数,delete[] 不会检查标志位。因为不需要调用析构,只用释放空间。所以free/delete/delete[] 的都可以使用。

需要明确的是,对象有析构函数时,用new申请数组时,才会产生标志位用以明确析构调用次数。而对象无析构(如内置类型 int ),申请数组时,不会产生标志位(因为,不需要调用析构)。

3、模拟 new[] 与 delete [] 的执行过程

使用,malloc与placement new 模拟实现 new[] 与 delete [] 的过程。首先我们需要知道一下几点:

  1. 对象销毁的步骤有两步:1. 析构对象 2. 释放空间
  2. delete[] 的工作原理是:先检查分配的数组空间的标志位的值,明确对象的个数从而明确析构的调用次数。当所有对象的析构都调用完成的时候,再释放空间。
  3. placement new:在预先已经分配了内存的空间上,创建对象

基于以上三点,我们可以模仿整个对象的申请与销毁的过程。

#include <iostream>

using namespace std;

class Object
{
public:
    Object() { cout << "construct Object" << endl; }
    ~Object() { cout << "desstruct Object" << endl; }
};

int main()
{
    int cnt = 2;    // 模拟标志位,标记有多少个对象
    // 以下两步相当于 Object *pObj = new Object[2]
    Object* pObj = (Object*)malloc(sizeof(Object) * cnt);   // 只申请空间
    new(pObj) Object[cnt];          // 在已申请的空间上,构造对象(调用构造)

    // ...

	// 以下两部相当于 delete[] pObj
    for (int i = 0; i < cnt; i++)   // 调用多次析构
        (pObj + i)->~Object();      // 
    free(pObj);                     // 释放空间

    return 0;
}

其中,我们使用cnt标志对象的个数。而当cnt为1时,即可表示 new/delete 的使用情况。

通过以上这个模拟的过程,便可较为方便的理解 free/new/new[] 的差别。例如,

  • free可以对new的空间进行释放吗?
    答:一般不可以。在对象存在析构函数时,需要调用析构函数,但free并不具备调用析构的功能。
    除非,对象不存在析构析构函数(内置类型,或类成员不存在析构函数)
  • delete可以释放new出的数组空间吗?
    答:一般不可以。因为new出的数组,需要进行多次的析构,而delete只能析构一次。
    除非,对象不存在析构,此时delete与delete[] 都只进行释放空间的操作,与free等价。
  • 什么情况下可以使用free或delete释放,却不能使用delete[]释放?
    答:delete[]与delete的区别在于前者会检查标志位确定调用析构的次数,而如果对象存在析构函数,且只申请了一个对象时,实际申请的空间中不会产生标志位标记对象个数。
    而delete[]却会把上越界标志误认为类对象个数,进行析构函数次数的调用,从而产生错误。
  • 什么情况下可以free、delete、delete[] 三种方法进行资源的释放?
    答:free与delete的区别为,前者不能调用析构函数,那么当对象不存析构函数时,free与delete等价。
    而在以上条件成立的条件下,delete与delete[]等价,即当对象不存析构函数时,三种资源释放方式都可以使用

2021.8.5更新…

1、使用CRT检测内存泄漏,使用示例:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

int main()
{
    int* p = (int*)malloc(sizeof(int));
    // free(p)
    _CrtDumpMemoryLeaks();	// 在程序退出前使用此函数检测内存泄漏
    return 0;
}

参考:https://docs.microsoft.com/zh-cn/visualstudio/debugger/finding-memory-leaks-using-the-crt-library?view=vs-2019

使用内存泄漏检测工具检测 new [ ] 申请的空间个数

对于复杂数据结构(含有析构函数的对象),使用 new Obj[n] 在分配内存时,会在这片地址之前多申请四个字节写入数组大小n,然后调用n次构造函数。因此,对与此种类型,申请的空间大小为 sizeof(Obj) * n + 4

我们可以使用内存泄漏检测的工具,来检测实际申请的空间个数。

1.1无析构函数的 new [ ] 占用的空间

示例1:无析构函数

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>

using namespace std;

class Test
{
public:
    Test() {}
    //~Test() {}
};

int main()
{
    Test* p = new Test[10];
    //delete p[];
    _CrtDumpMemoryLeaks();
    return 0;
}

如图:检测结果显示,无析构函数的时候,有10个字节的内存申请后没有被释放,造成了内存泄漏。
在这里插入图片描述

1.2有析构函数的 new [ ] 占用的空间

示例2:有析构函数

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>

using namespace std;

class Test
{
public:
    Test() {}
    ~Test() {}
};

int main()
{
    Test* p = new Test[10];
    //delete p[];
    _CrtDumpMemoryLeaks();
    return 0;
}

如图:检测结果显示,无析构函数的时候,有14个字节的内存申请后没有被释放,造成了内存泄漏。这4个字节就是用于保存对象个数的。
在这里插入图片描述
我们输出该空间, p-4 位置的值,可以看到这里的确保存着对象的个数。
在这里插入图片描述
同时,如果我们使用 delete[] 释放空间之后,该空间的值被回收。
在这里插入图片描述

2、使用 delete 释放
1.1 释放有析构函数的对象数组

出错
在这里插入图片描述

1.2 释放无析构函数的对象数组

执行正常,无内存泄漏产生。

因为无析构函数,在申请空间 的时候只申请了 n 个空间,而使用 delete 释放的时候,直接将这10个空间当做一整块空间进行释放。
在这里插入图片描述

1.3 使用delete[] 释放的时候也需要注意

delete[] 会检查待释放空间的前4个字节的值n,然后循环n次调用析构并释放空间

因此在用delete[] 释放单个new的对象会出错的原因在于,调用了n次析构函数导致的程序崩溃。
在这里插入图片描述

注意上图中,打印了多次“析构…”,实际上这里循环调用了 n 次的析构函数。而这里 n 的取值实际上是堆区的防越界标记值。因此这里会反复的调用 4261281277(0xfdfdfdfd)次析构。
在这里插入图片描述

而如果我们将析构函数去掉,就会发现程序执行正常
在这里插入图片描述

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫RT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值