C++内存管理:new与delete的深层解析
1. 引言
在C++的世界里,动态内存管理是一个核心话题。对于从C语言过渡到C++的开发者来说,一个常见的困惑是:既然C语言的
malloc
和free
依然可以在C++中使用,为什么C++还要引入new
和delete
这两个操作符?本文将深入探讨这两对内存管理机制的根本区别,帮助你理解为何在C++中更推荐使用
new
和delete
。
2、本质区别:运算符vs函数
这就最根本的区别,决定了他们的行为与能力。
new和delete:是c++内置的运算符(operator),编译器直接理解它们的含义,并腐恶转换为底层的内存分配和对象构造调用。
malloc和free:是c语言标准库中定义的库函数,位于<stdlib>,<stdlib.h>头文件中,它们的功能由运行时库提供。
特性 new/delete malloc/free 本质 c++操作符 c库函数 内存分配 在堆上分配指定类型的内存 在堆上分配指定字节数的内存 析构函数/构造函数 会调用 不会调用 返回值 返回明确类型的指针 返回void*,需手动强制转换 分配失败 抛异常 返回NULL 重载 可以进行类成员重载或全局重载 不可以重载 计算大小 编译器自动计算所需内存大小 需手动使用sizeof计算字节数 初始化 支持显式初始化 只能分配未初始化的内存
3、类型安全与返回值:
malloc的返回值是void*,所以在使用的时候必须使用强制转换,如果忘记进行强制类型转换,那么就会发生错误,这也是malloc函数的弊端或者说是缺点之一。
// C style (类型不安全)
int *p = (int*)malloc(sizeof(int)); // 必须进行强制类型转换
*p = 10;
free(p);
new
直接返回所需类型的指针,是类型安全的。
// C++ style (类型安全)
int *p = new int; // 直接返回 int* 类型
*p = 10;
delete p;
接着往下看,这也是malloc与free函数与new和delete运算符的本质区别:
#include<iostream>
#include<stdlib.h>
using namespace std;
class Myclass
{
public:Myclass(){cout << "成功调用构造函数:Myclass()函数" << endl;}~Myclass(){cout << "成功调用析构函数:~Myclass()函数" << endl;}
};
int main()
{cout << "using malloc/free:" << endl;Myclass* obj1 = (Myclass*)malloc(sizeof(Myclass));free(obj1);//析构函数并不会被调用cout << "using new/delete:" << endl;Myclass* obj2 = new Myclass;delete obj2;//1、调用Myclass的析构函数,对于自定义类型,会调用它的析构函数//2、释放内存,将对象本身的内存还给操作系统。
}
从输出结构可以清晰地看到,malloc仅仅分配了足够大的内存空间,而new在分配内存后,还调用了类的构造函数来对实例化出的对象进行初始化操作,delete在释放内存前也会调用析构函数来清理资源(如关闭文件、释放内存),而free则直接释放内存,有可能导致内存泄漏,所以在c++里,十分建议写new和delete,不仅仅安全还很方便,malloc与free有的,new和delete都有,malloc与free没有的,new和delete也有。
4 、对于数组的处理:
使用free不需要关心是否是数组,但是如果你使用new[ ]进行内存分配,那么必须使用delete[ ]来释放对应的内存,去掉这个[ ]可能会报错!
接着看下面的代码:
#include<iostream>
#include<stdlib.h>
using namespace std;
class Myclass
{
public:Myclass(){cout << "成功调用构造函数:Myclass()函数" << endl;}~Myclass(){cout << "成功调用析构函数:~Myclass()函数" << endl;}
};
int main()
{Myclass* ptr = new Myclass[10];delete [] ptr;
}
在c++中,使用new[ ]动态分配数组时,对于内置类型,比如int,char,float,等等,new int[10]不会调用其构造函数,因为内置类型没有用户定义的构造函数,但是对于自定义类型而言,使用new[ ]来定义数组,必须我这里自定义的类型Myclass,这里的new Myclass[10]会调用10次Myclass类的构造函数和析构函数。
下面介绍的才是这篇博客的核心内容,也是本人从浅至深的一个过程。
5、operator new和operator delete:
提到new和delete,那么就不得不谈到operator new和operator delete,那么operator new和operator delete有什么联系或者说是区别呢?
为了方便我把代码直接截图下来,当我们写出一句 Myclass* ptr=new Myclass[10]的时候,编译器会将其分解为三个步骤:
1、分配内存:调用operator new(sizeof(Myclass)函数(这一点我等下会通过观察汇编来进行验证),申请一块足够大的,并且未初始化的原始内存。
2、构造对象:
在上述内存地址上调用Myclass::myclass()构造函数,初始化这块内存,使其成为一个真正的对象。
3、返回指针:返回构造好的对象的地址,所以用指向这个类的指针来接收也就是Myclass*。
那么同理,delete obj也做了两件事情:
1、析构对象:调用obj->~Myclass()析构函数,清理对象占用的内存
2、释放资源:调用operator delete(obj)函数,释放对象所占用的原始内存块。
不仅仅如此,operator new与operator delete与new和delete:
new和delete是操作符,而operator new和operator delete是函数,换句说,operator new和operator delete就是C语言中malloc函数与free函数的加强版,它们不仅仅会开辟空间,还会在开辟空间的基础上进行抛异常。而opertaor new和operator delete底层上也是调用了malloc函数与free函数。
下面我将通过汇编来验证,new会通过调用operator new来开辟空间。
operator new
和operator delete
做了什么?(单一职责)这两个函数只负责第一步和最后一步,即原始内存的分配与释放。它们和
malloc
/free
是同一级别的概念,但属于C++的体系。
void* operator new(size_t size)
:它的唯一任务就是接受一个字节数
size
,找到一块足够大的连续内存空间,并返回指向这块内存的void*
指针。如果失败,它默认抛出std::bad_alloc
异常。
void operator delete(void* ptr)
:它的唯一任务是接受一个由
operator new
返回的void*
指针,释放这块内存。标准库已经提供了默认的全局
operator new
和operator delete
,它们通常就是基于malloc
和free
实现的。你可以把它们想象成是C++世界里“高级的”、“会抛异常的”
malloc
和free
。
6、 重载 (Overloading)
这才是重载的意义所在。我们可以提供我们自己版本的 operator new
和 operator delete
函数,来接管内存分配和释放的过程。
第一种方式:
全局重载:
这种方式非常不推荐,因为程序中所有的new和delete都会调用我们自己写的版本,这么做可以说是不安全的一种行为。
#include <iostream>
#include <stdlib.h> // for malloc, free
using namespace std;// 全局重载 operator new
void* operator new(size_t size) {cout << "Global new called, size: " << size << endl;void* p = malloc(size);if (!p) throw bad_alloc(); // 遵循规范,分配失败抛异常return p;
}// 全局重载 operator delete
void operator delete(void* p) noexcept {cout << "Global delete called" <<endl;free(p);
}class MyClass { int data;
};
int main() {int* p1 = new int(42); // 会调用我们重载的全局 operator newdelete p1; // 会调用我们重载的全局 operator deleteMyClass* obj = new MyClass; // 同样会调用我们的版本delete obj;return 0;
}
第二种方式:
类特定重载 (非常有用且推荐):
#include <iostream> #include <stdlib.h> using namespace std;class MyClass { public:int _data;// 类特定的 operator newstatic void* operator new(size_t size) {cout << "MyClass::new called, size: " << size <<endl;void* p = malloc(size);if (!p) throw bad_alloc();return p;}// 类特定的 operator deletestatic void operator delete(void* p) noexcept {cout << "MyClass::delete called" <<endl;free(p);} };int main() {MyClass* obj = new MyClass; // 调用 MyClass::operator newdelete obj; // 调用 MyClass::operator deleteint* p = new int; // 仍然使用全局的 ::operator new,不受影响delete p;return 0; }
我们只为我们特定的类重载
operator new
和operator delete
。这样,只有分配和释放这个类的对象时,才会使用我们自定义的版本,不会影响程序的其他部分。
下面将使用一表来把operator new与operator delete和new和delete之间的区别做个总结:
总结如下:
特性 | new/delete | operator new/operator delete |
身份 | 操作符 | 函数 |
职责 | 完成的对象生命周期管理(分配内存加上构造或者析构) | 仅负责原始内存的分配和释放 |
可重载性 | 不可重载 | 可以重载 |
调用关系 | 调用operator new和构造函数 | 被new所调用(已通过观察汇编进行了对应的证明) |
7、new
和 delete
表达式的编译期魔法
最后一个部分:对上面的内容做一个更深层次的理解:
当编译器看到 MyClass *obj = new MyClass;
这行代码时,它会进行一个固定的分解动作。这个过程是理解重载底层原理的关键。
// 我们自己写的代码:
MyClass *obj = new MyClass(arg1, arg2);// 编译器在背后实际生成的代码:
void* __memory = nullptr; // 1. 先申请原始内存
try {__memory = MyClass::operator new(sizeof(MyClass)); // 寻找分配函数obj = static_cast<MyClass*>(__memory);obj->MyClass::MyClass(arg1, arg2); // 2. 在内存上构造对象(调用构造函数)
} catch (...) {if (__memory)MyClass::operator delete(__memory); // 如果构造失败,释放申请的内存throw; // 重新抛出异常
}
同理,delete也是一样。
// 我们自己写的代码: delete obj;// 编译器在背后实际生成的代码: obj->~MyClass(); // 1. 先调用析构函数 MyClass::operator delete(obj); // 2. 再释放内存
重载
operator new
和operator delete
,本质上就是告诉编译器,在执行上述流程的第一步和最后一步时,不要用标准库提供的默认函数,而是用我自定义的函数。
底层做了什么?
1、编译时:编译器看到
new MyClass
,知道要去MyClass
的作用域内寻找operator new
函数。2、运行时:
new:调用我们自己重写的Myclass::operator new来获取内存,然后调用构造函数完成初始化。
delete:调用析构函数,然后调用我们自己重写的Myclass::operator delete来释放内存。
3、内存布局:重载函数本质是类的静态成员函数,他们不属于任何一个对象实例,因此没有this指针,通俗地讲,他们只是为对象的诞生(创建)和消亡(销毁)提供场地管理的后勤部门!
截止到这里,本文的所有内容就完成了,在写的时候难免有所不足之处,可在评论区指出,本人会及时进程更新,如对您有所帮助可以点赞加收藏,本作者持续更新c/c++或数据结构或linux有关的内容!