目录
(2)new关键字的原理之 operator new() 函数
(3)delete关键字的原理之 operator delete() 函数
1.了解进程空间
如下是程序运行时的进程空间:
我们本文谈到的动态内存管理就是和堆相关的。C语言中的malloc、calloc、realloc所申请的空间就是在堆中。
2.回忆C语言动态内存管理
C语言中的动态内存管理中,我们经常使用malloc,calloc,realloc来在堆上申请空间,使用free释放在堆上申请的空间。下面来回忆一下malloc的使用:
代码一:
//代码一
#include <stdio.h>
#include <malloc.h>
int main() {
int* p = (int*)malloc(sizeof(int) * 10);
free(p);
}
3.认识C++动态内存管理
(1)new 与 delete 操作内置类型
<1>单个内置类型空间的申请与释放
new使用方法:1.new 类型。(不初始化) 2. new 类型(初始值)。
delete使用方法:delepte 变量名。
代码二:申请单个内置类型的空间。(本文代码均在win10系统下的vs2019下运行)
//代码二
#include "iostream"
using namespace std;
int main() {
//单个类型空间的申请
int* p1 = new int;
//单个类型空间的申请并初始化
int* p2 = new int(10);
//单个类型空间的释放
delete p1;
delete p2;
}
<2>连续多个内置类型空间的申请与释放
首先来了解一个知识点,int arr[10]的类型是什么?类型是:int [10]。之所以形式与其他内置类型不一样是因为这是语法规定。
new []使用方法:1.new 类型。(此处的类型是指上述中的数组类型,并且这种形式没有赋初始值) 2.new 类型{初始值}。(此处类型也是上述中的数组类型)
delete[]使用方法:delete[] 变量名。
代码三:
//代码三
#include "iostream"
using namespace std;
int main() {
//一段连续内置类型空间的申请
int* p3 = new int[10];
//申请一段连续的内置类型空间并初始化
int* p4 = new int[10]{ 1,2,3,4 };
//释放一段连续的内置类型空间
delete[] p3;
delete[] p4;
}
(2)new 与 delete 操作自定义类型
注意:new会调用构造函数,delete会调用析构函数。(现在知道这一点对于理解它的用法很有帮助)
<1>单个自定义类型空间的申请与释放
[1].只有默认构造函数的自定义类型。(注意:如果没有显式定义构造函数,就不可以在new的括号后面赋初值,因为new需要调用构造函数,没有实现,怎么可以赋初值?)
代码四:
//代码四
#include "iostream"
using namespace std;
class A {
public:
int _m = 0;
};
int main() {
A* pa = new A;
delete pa;
}
[2].显式定义有参构造函数后,就不可以使用上述方式赋值。(因为,new会调用构造函数,除非你显示实现了无参的构造函数,否则那样必然是会报错的。)
代码五:
//代码五
#include "iostream"
using namespace std;
class A {
public:
int _m = 0;
A(int m)
:_m(m)
{}
};
int main() {
A* pa = new A(5);
delete pa;
}
<2>连续多个自定义类型空间的申请与释放
[1].只有默认构造函数的自定义类型。(注意事项和单个自定义类型相同)
代码六:
//代码六
#include "iostream"
using namespace std;
class A {
public:
int _m = 0;
};
int main() {
A* pa = new A[10];
delete pa;
}
[2].显式定义构造有参函数后。(注意事项和单个自定义类型相同)
代码七:
注意:[]内的数字代表数组中对象个数,{}中的是为每个对象赋的初值。在实际使用中我发现,初值的个数与对象个数要完全相同,否则报错。这一点和内置类型中的不同。
//代码七
#include "iostream"
using namespace std;
class A {
public:
int _m = 0;
A(int m)
:_m(m)
{}
};
int main() {
A* pa = new A[2]{3,3};
delete pa;
}
4.new和delete使用的注意事项
(1)new与delete特点
<1>new会调用构造函数,delete会调用析构函数。
代码八:
//代码八
#include "iostream"
using namespace std;
class A {
private:
int _m;
int _n;
public:
A(int m, int n)
:_m(m)
, _n(n)
{
cout << "带参构造调用" << endl;
}
A() {
cout << "无参构造调用" << endl;
}
~A() {
cout << "析构调用" << endl;
}
};
int main() {
A* pa1 = new A;
delete pa1;
cout << "=============" << endl;
A* pa2 = new A(12, 13);
delete pa2;
}
上述代码的打印结果:从中可以验证,上述结论。
<2> new 和 delete应该配套使用,否则可能会有内存泄漏或者程序崩溃。
此处的配套使用是指:用new申请的空间要用delete释放。用malloc、calloc、realloc申请的空间要用free释放。
(2)malloc与free特点
malloc不会调用构造函数,free不会调用析构函数。
代码九:
//代码九
#include "iostream"
using namespace std;
class A {
private:
int _m;
int _n;
public:
A(int m, int n)
:_m(m)
, _n(n)
{
cout << "带参构造调用" << endl;
}
A() {
cout << "无参构造调用" << endl;
}
~A() {
cout << "析构调用" << endl;
}
};
int main() {
A* pa1 = (A*)malloc(sizeof(A));
free(pa1);
cout << "=============" << endl;
A* pa2 = (A*)malloc(sizeof(A));
free(pa2);
}
上述代码的结果打印:
(3)注意事项
<1>操作内置类型时,如果不配套使用,一般不会出现内存泄漏。我们用内置的_CrtDumpMemoryLeaks() 函数来检测是否存在内存泄漏。
代码十:如下代码可以在输出窗口中看到检测结果,并没有内存泄漏。
//代码十
#include "iostream"
using namespace std;
int main() {
int* p1 = new int(10);
free(p1);
_CrtDumpMemoryLeaks();
}
<2>操作自定义类型,如果不配套使用,会内存泄漏 或 程序崩溃。
[1]验证程序崩溃
代码十一:这段代码会报错,并且是在析构函数内报错,原因如下。
首先,使用malloc会申请一段空间,但因为没有初始化,所以空间中全是随机值,作为类中成员变量的指针p中自然也就是保存的随机值。
可是释放资源是使用的是delete,这个已经验证过了,会调用析构函数。可是p中保存的是随机值,也就是说p是一个野指针,释放一个野指针怎么会不报错呢?
//代码十一
#include "iostream"
using namespace std;
class A {
int* p;
public:
A() {
p = (int*)malloc(100);
cout << "构造调用" << endl;
}
~A() {
free(p);
p = nullptr;
cout << "析构调用" << endl;
}
};
int main() {
A* pa = (A*)malloc(sizeof(A));
delete pa;
_CrtDumpMemoryLeaks();
}
[2]验证内存泄漏
代码十二:
//代码十二
#include "iostream"
using namespace std;
class A {
int* p;
public:
A() {
p = (int*)malloc(100);
cout << "构造调用" << endl;
}
~A() {
free(p);
p = nullptr;
cout << "析构调用" << endl;
}
};
int main() {
A* pa = new A;
free(pa);
_CrtDumpMemoryLeaks();
}
如下是这段代码的输出窗口图和打印窗口图:
如下图是打印窗口图:可以看到,只调用了构造函数,没有调用析构函数。
如图是经过处理后的输出窗口图:可以看检测结果有100字节的内存泄漏,就是因为构造函数中申请的空间没有释放。
5.new和delete的实现原理
(1)相应汇编
首先看一下如下代码的汇编:
代码十三:
//代码十三
#include "iostream"
using namespace std;
class A {
public:
int _m = 0;
A(int m)
:_m(m)
{}
};
int main() {
A* pa = new A[2]{3,3};
delete pa;
}
以下是经过处理后的汇编语句,看一下重点即可:
可以看到,new关键字申请空间时,底层会先调用一个operator new()函数,然后再调用构造函数初始化。delete关键字释放空间时,底层会调用一个operator delete()函数。
注意:new和delete是关键字,operator new 和 operator delete是函数。
(2)new关键字的原理之 operator new() 函数
new T:T是自定义类型
1.申请sizeof(T)大小的空间。
[1]此时operator new()函数通过调用 malloc 循环申请空间。若成功,直接返回空间的首地址。若失败,执行[2]。
[2]检测用户是否提供应对空间不足的函数。若提供,继续执行[1]。若未提供,抛出异常。
2.调用T的构造函数对空间进行初始化形成对象。
这一步就不需要解释了,直接调用构造函数,在申请好的空间上进行初始化。
(3)delete关键字的原理之 operator delete() 函数
1.先调用析构函数,将对象中的资源清理干净
2.调用operator delete释放对象的空间。
operator delete内部调用free来释放对象的空间
6.总结
<1>malloc(size):只是从堆上申请size个字节的空间,并不会对空间中的内容进行初始化。即:不会调用构造函数。
<2>free(p):只负责将p指向的堆空间还给系统,并不会调用析构函数对空间中的内容进行清理。即:不会调用析构函数。
<3>new:会申请空间,其次会调用构造函数对空间中的内容进行初始化。
<4>delete:会释放空间,并且会调用析构函数对空间中的资源进行清理。