动态内存分配
前言
C 语言的内存管理方式,C++也可以使用,这里放一篇👉🔗 C 语言关于动态内存分配的文章👈,需要巩固的同学可以先看看这篇。
查查 C 语言学的如何呀:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
1. 选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____ staticGlobalVar在哪里?____
staticVar在哪里?____ localVar在哪里?____
num1 在哪里?____
char2在哪里?____ *char2在哪里?___
pChar3在哪里?____ *pChar3在哪里?____
ptr1在哪里?____ *ptr1在哪里?____
2. 填空题:
sizeof(num1) = ____;
sizeof(char2) = ____; strlen(char2) = ____;
sizeof(pChar3) = ____; strlen(pChar3) = ____;
sizeof(ptr1) = ____;
3. sizeof 和 strlen 区别?
----------
C C
C A
A
A A
A D
A B
40
5 4
4/8 4
4/8
C 语言的内存管理方式有不足且比较麻烦,C++ 有自己的方式,就是 new 和 delete 操作符。
1. 初步认识 new 和 delete
new 和 delete 是用来在 堆上申请和释放空间的 ,是 C++ 定义的 关键字,和 sizeof 一样。
int main()
{
// 动态申请一个 int 类型的空间,不初始化
int* p1 = new int;
// 对比 malloc
/*int* p2 = (int*)malloc(sizeof(int));
if (p2 == NULL)
{
perror("malloc fail");
}*/
// 动态申请 1 个 int 类型的空间,并初始化为 10
int* p3 = new int(10);
// 动态申请 10 个 int 类型的空间(数组)
int* p4 = new int[10];
// 初始化动态数组
int* p5 = new int[10]{ 1,2,3,4 };
return 0;
}
🚩注意:
-
实际 new / delete 和 malloc / free 最大的区别是,前者对于 自定义类型 除了可以开辟空间,还会调用构造和析构函数。
-
这 一对操作符 和 一对函数 一定要匹配使用,切记 不可交叉使用,后文进行原因分析。
🌰我们做如下测试:
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
/************************ 内置类型 ************************/
// 交叉使用会报警告,但仍可以运行,差别不大
// 只有测试价值,实际使用并不推荐如此做法
void test1()
{
// 内置类型是几乎是一样的,后面分析原因
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
free(p2);
delete p1;
}
/*********************** 自定义类型 ************************/
// new / delete 调用了构造和析构函数,malloc / free 没有调用
void test2()
{
A* p3 = (A*)malloc(sizeof(A));
A* p4 = new A(1); // 构造
free(p3);
delete p4; // 析构
}
/********************* 自定义类型数组 ***********************/
void test3()
{
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10]; // 构造 10 次
free(p5);
delete[] p6; // 析构 10 次
}
得到测试结果:
2. operator new 和 operator delete
虽然有重载关键字 operator,但可不要被他的名字误导😧!!他们是 C++ 库里面的两个函数,本质上是对 malloc 和 free 的封装。
new 和 delete 是用户进行动态内存申请和释放的 操作符,
operator new 和 operator delete 是系统提供的 全局函数,他们之间是底层调用的关系。
-
⭕这里进行过程梳理:
-
new 在底层调用 operator new 全局函数来申请空间,
operator new 实际通过 malloc 来申请空间 -
delete 在底层通过 operator delete 全局函数来释放空间。
operator delete 实际通过 free 来释放空间。
画成视图便是如上:当我们使用 new 和 delete 操作符,会调用里面的 operator new 和 operator delete 函数,而这两个函数又是 malloc 和 free 的封装。
既然是封装,设计这个函数的大佬自然想到,要将我们平时必须手动进行的 malloc 申请检查进行优化。故 operator new 有如下特点。
operator new 的使用细节:申请失败抛异常
- malloc 申请空间成功,则直接返回。
- malloc 申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。
🐎失败测试:
/********************* malloc 失败 ***********************/
void testMalloc()
{
size_t size = 0;
// 失败了返回 nullptr
while (1)
{
int* p1 = (int*)malloc(1024 * 1024 * 4);
if (p1 == nullptr)
{
perror("malloc fail");
break;
}
size += 1024 * 1024 * 4;
cout << p1 << endl;
}
cout << size / 1024 / 1024 << "MB" << endl;
}
/*********************** new 失败 ************************/
void testNew()
{
size_t size = 0;
// 失败了抛异常
try
{
while (1)
{
int* p1 = new int[1024 * 1024 * 4]; // 失败则直接跳到 catch 处
size += 1024 * 1024 * 4;
cout << p1 << endl;
}
}
catch (const std::exception& e)
{
cout << e.what() << endl;
}
cout << size / 1024 / 1024 << "MB" << endl;
}
/**********************************************************/
int main()
{
testMalloc();
testNew();
return 0;
}
测试结果如下:
3. 🚩new / delete 和 new T[N] / delete[] 的实现原理
通过前一小节我们得出,new / delete 和 malloc / free 的最大区别在于,对自定义类型进行使用的时候,前者会调用构造和析构函数。现在我们补充了 operator new 和 operator delete 的知识后,new 和 delete 显得更忙碌了。
对于 内置类型:
new / delete 和 malloc / free 基本类似;
不同的地方是, new 在申请空间失败时会抛异常,malloc 会返回 NULL;
更有一点,new / delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续空间。
对于 自定义类型
-
new 的原理
- Ⅰ. 调用 operator new 函数申请空间
- Ⅱ. 在申请的空间上执行构造函数,完成对象的构造 delete 的原理
- Ⅰ. 在空间上执行析构函数,完成对象中资源的清理工作
- Ⅱ. 调用 operator delete 函数释放对象的空间 new T[N] 的原理
- Ⅰ. 调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请
- Ⅱ. 在申请的空间上执行 N 次构造函数 delete[] 的原理
- Ⅰ. 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理
- Ⅱ. 调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间
🌰做如下测试:
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
/************************ 自定义类型 ************************/
void test1()
{
A* p1 = new A;
// 1.new -> operator new -> 封装 malloc
// 2.再调用构造函数
delete p1;
// 3.先调用析构函数
// 4.再 operator delete p3 指向的空间
}
/********************* 自定义类型数组 ***********************/
void test2()
{
A* p2 = new A[10];
// 1.new -> operator new[] -> operator new -> 封装 malloc
// 2.先调用 10 次构造函数
delete[] p2;
// 3.先调用 10 次析构函数
// 4.再 operator delete[] p4 指向的空间
}
/*********************** 交叉测试 *************************/
void test3()
{
int* p3 = new int[10];
free(p3); // 这里不会报错,因为 p3 是内置类型,delete 实质也是调用的 free 函数,跟直接使用没有太大的区别
A* p4 = new A;
free(p4); // 这里也不会报错,只是相当于没有析构。但是如果有内存开辟,就可能会出现内存泄露了
A* p5 = new A[10];
//free(p5); // 都会报错 是 vs 编译器的机制设置所致
//delete p5;
delete[] p5;
}
🌰稍微复杂一点的例子(含图示):
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = new int[4];
_top = 0;
_capacity = 4;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
void test()
{
// 正常使用时,会调用构造和析构,不需要手动释放
// 两层:st 的成员开在栈上,其中的 _a 指向堆的一块空间
Stack st;
// 由于 Stack* 是内置类型,不会调用构造和析构,需要动释放
// 三层:pst 指向开在堆上的成员,其中的 _a 指向堆的一块空间
Stack* pst = new Stack;
delete pst;
}
图示:
定位 new (placement-new)
对一块已有的空间(特定位置的空间)进行初始化,不进行任何检查,需要程序员自己把控。
写法:
// 在指针指向的位置进行 单位类型 的空间开辟
new(指针)类型
// 在指针指向的位置对 单位类型 开辟空间并 初始化
new(指针)类型(初始化的值)
// 在指针指向的位置进行 多单位类型 的空间开辟
new(指针)类型[N]
可以看到 :
- p2 第一次输出地址就是 buffer[] 的地址,在给 p2 中的值初始化后,能正常读出;
- 接着用定位 new 将 p3 定位到 buffer[] 处,修改并覆盖了 p2 地址里面的值;
- p4 用定位 new 从 buffer[] 的起始位置偏移 40 并返回正确。
实际使用上,除了出现向内存池申请空间的情况,此外先 malloc 再定位 new 确实多此一举,建议直接 new。
4. VS 上 newT[N] 和 delete[] 的标记机制
-
编译器会在 new T[N] 的时候在数组前面紧挨着的位置多开 4 个字节,标记数组个数 N,指针变量指向后面的空间
-
delete[] 会读取标记,按照标记次数进行析构函数的调用。
例中的情况下,delete 时,明显指针位置不对,所以导致了程序的崩溃。
但这时候,如果我们把已设置的析构函数屏蔽掉,程序时可以正常运行的,为什么呢??
简单说来,当编译器识别到我们没有定义析构函数,会自动评估,如果这个程序不调用析构函数不影响内存的使用,便不再多开这 4 个字节。
5. 🚩结论:匹配使用!
new / malloc 整个系列,其底层实现机制都有交叉关联,不匹配使用可能有问题,也可能没问题,建议大家一定匹配使用
6. malloc / free 和 new / delete 的区别总结
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以对内置类型初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理