目录
4.operator new和operator delete函数
一、引言
管理内存最重要的是要知道我们每段代码存储在内存的什么区上面,不同的区有不同的管理方式,例如:栈区的内存空间不需要我们去管理,这是编译器需要干的活,而堆区的使用就需要我们去管理,有需要就申请,使用完就释放。主要还有全局/静态区,常量区,代码区几个内存区域,内存划分区域一方面是不同的区有不同的管理方式,另一方面主要是不同的变量,函数需要完成的任务不同,生命周期也不相同。
栈区:又叫堆栈--由编译器自动分配和释放,存放函数的参数值、局部变量等。其操作方式类似于数据结构中的栈。栈是向下增长的。
堆区:用于程序运行时动态内存分配,一般由程序员分配和释放(C++使用
new、malloc等),若程序员不释放,程序结束时可能由操作系统回收。注意它与数据结构中的堆是不同的概念,分配方式类似于链表。堆是可以上增长的。全局/静态区:存储全局数据和静态数据。在程序编译时分配。程序结束后由系统释放,该区域又可细分为已初始化和未初始化两个部分。
常量区:存储常量字符串等不可修改的数据,程序结束后由系统释放。
代码区:存放函数体的二进制代码,由操作系统进行管理。
例如:
// 全局变量(全局/静态区)
int globalVar = 10;
// 静态全局变量(全局/静态区)
static int staticGlobalVar = 20;
// 常量全局变量(常量区)
const int constGlobalVar = 30;
// 函数(代码区)
void testFunction() {
// 静态局部变量(全局/静态区)
static int staticLocalVar = 40;
// 局部变量(栈区)
int localVar = 50;
// 数组(栈区)
int intArray[5] = { 1, 2, 3, 4, 5 };
// 字符数组(栈区),内容可修改
char charArray[] = "hello";
// 指向常量字符串的指针(常量区)
const char* constString = "world";
// 堆内存分配(堆区)
int* heapInt = new int(60);
int* heapArray = new int[5] {10, 20, 30, 40, 50};
// 释放堆内存
delete heapInt;
delete[] heapArray;
}
二、c语言中动态内存管理方式
在上面了解完不同的内存区域以及不同的代码所在的内存区域之后,我们也可以发现只有堆区的内存使用需要我们主动去申请和释放,这个也是我们需要去学习申请和释放方式的地方。在c语言中主要是三个内存分配函数去申请空间:malloc/calloc/realloc ,它们都需要强制类型转换。它们都是通过 free去释放。
1.malloc
分配指定大小的内存块,不初始化内容(内存中的值是未定义的)。
int* ptr = (int*)malloc(sizeof(int) * 10); // 分配10个int的空间
2.calloc
分配指定数量和大小的内存块,并初始化为 0。
int* ptr = (int*)calloc(10, sizeof(int)); // 分配10个int并初始化为0
3.realloc
调整已分配内存块的大小,可能会移动内存位置。这里可能移动内存位置的意思是:当在已经分配的内存空间上扩大分配空间的时候,先判断已经分配内存空间后面连续的内存空间是否已经分配出去。如果分配出去了就不能再使用,就需要在其他地方申请一块扩大后的内存空间,然后把原来的内存空间的内容拷贝过去,再释放原来的分配空间。如果没有分配出去就可以直接在原来的分配内存空间的后面扩大内存空间即可。
int* ptr = (int*)malloc(10 * sizeof(int));
ptr = (int*)realloc(ptr, 20 * sizeof(int)); // 扩大为20个int的空间
| 函数 | 初始化 | 内存布局 | 返回值 |
|---|---|---|---|
malloc | 不初始化 | 连续内存块 | 指向分配内存的指针 |
calloc | 初始化为 0 | 连续内存块(num*size) | 指向分配内存的指针 |
realloc | 保留原有数据 | 可能移动内存位置 | 新指针(可能与原指针不同) |
4.free
它与
malloc/calloc/realloc配合使用,用于回收不再需要的堆内存,防止内存泄漏。
void free(void* ptr);//ptr 是指向先前通过 malloc、calloc 或 realloc 分配的内存块的指针。
注意:
① 必须释放已分配的内存
◆ 只释放通过
malloc/calloc/realloc返回的指针,否则行为未定义(如释放栈变量、静态变量或未分配的指针)。② 避免悬空指针
◆ 释放内存后,指针
ptr变为悬空指针(指向已释放的内存)。建议立即将其置为NULL:③ 禁止重复释放
◆ 对同一块内存多次调用
free会导致程序崩溃。需确保每个内存块只释放一次。
三、c++中动态内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
1.new
用于动态内存分配的操作符,与 C 语言的
malloc相比,它具有类型安全、自动类型转换、支持构造函数等优势,它对内置类型也可以指定值进行初始化。当内存分配失败时,new默认抛出std::bad_alloc异常。
#include <iostream>
class Point
{
public:
int x, y;
Point()
: x(0)
, y(0)
{
std::cout << "Default constructor" << std::endl;
}
Point(int x, int y)
: x(x)
, y(y)
{
std::cout << "Param constructor" << std::endl;
}
};
int main()
{
// 分配单个对象
Point* p1 = new Point; // 调用默认构造函数
Point* p2 = new Point(1, 2); // 调用带参构造函数
// 分配数组
Point* arr1 = new Point[3]; // 创建3个Point对象,调用3次默认构造
Point* arr2 = new Point[2]{ {3,4}, {5,6} }; // C++11列表初始化
// 释放内存
delete p1;
delete p2;
delete[] arr1; // 数组必须用 delete[]
delete[] arr2;
return 0;
}
2.定位new
允许在已分配的内存块上构造对象,常用于内存池、高性能场景。也就是定位到已经分配空间的地方进行初始化。
#include <new> // 必须包含此头文件,iostream可能包含了但是不保证
// 预分配内存
char* buffer = new char[sizeof(Point)];
// 在预分配的内存上构造对象
Point* p = new (buffer) Point(10, 20);
// 手动调用析构函数(因为不能用 delete)空间不能释放只能将初始化给抹去
p->~Point();
delete[] buffer; // 释放预分配的内存
3.delete
delete是用于释放由new分配的动态内存的操作符。它与new配对使用,负责销毁对象并回收内存,是手动内存管理的核心机制。delete执行两个步骤,一是调用析构函数(如果对象是类类型),二是释放内存将其返回给系统。这里的使用是和new配套使用,例子也可以参考上面new的例子。
注意:
匹配问题:对于数组,必须使用
delete[],否则很可能导致内存泄漏,这里具体的下面会举例子详细介绍。悬空指针:
delete后,原指针变为悬空指针(指向已释放的内存)。建议立即置为nullptr。定位
new的释放规则:对于定位new创建的对象,不能直接用delete,需手动调用析构函数。重载
delete操作符:可自定义内存释放行为。
4.operator new和operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new和operatordelete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operatordelete全局函数来释放空间。也就是说对于自定义类型new在底层是先通过operator new来申请好内存空间,然后再通过构造函数进行初始化,而delete先通过析构函数进行析构,然后再通过operatordelete去释放空间。而operator new实际也是通过malloc来申请空间,operatordelete最终是通过free来释放空间的,不过它会抛异常。
四、malloc/free和new/delete的区别
共同点是:
都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
1.
malloc和free是函数,new和delete是操作符。2.
malloc申请的空间不会初始化,new可以初始化。3.
malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可。4.
malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型。5.
malloc申请空间失败时,返回的是null,因此使用时必须判空,new不需要,但是new需 要捕获异常。6. 申请自定义类型对象时,
malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理释放。
五、为什么一定让delete[]与new[]的匹配?
在使用时delete与new匹配,delete[]要与new[]匹配,举个例子就可以发现在某些情况下如果不匹配会出现什么问题。
#include<iostream>
using namespace std;
class A
{
public:
A(int a1 = 0, int a2 = 0)
:_a1(a1)
, _a2(a2)
{
cout << "A(int a1 = 0, int a2 = 0)" << endl;
}
~A()
{
//delete _ptr;
cout << "~A()" << endl;
}
private:
int _a1 = 1;
int _a2 = 1;
};
class B
{
private:
int _b1 = 2;
int _b2 = 2;
};
int main()
{
B* p2 = new B[10];
delete p2;
A* p3 = new A[10];
delete p3;//报错
return 0;
}
在上面的代码中,A和B是两个类,都通过new[]去申请空间,都通过delete释放空间。对于B对象没有报错,而A对象报错了,这是为什么呢?因为存在编译器优化问题,对于内置类型的变量释放的时候要调用析构函数,B类中我没有自己写析构函数,编译器会自己生成默认的析构函数,那么编译器认为这里默认的析构函数作用不大,就在new[]申请空间的时候不会多开4个字节空间去存放个数,而A类我写了析构函数,编译器就不会生成默认的析构函数,那么它就不能直接认为这里的析构函数作用不大,就会在new[]申请空间的时候前面多开4个字节空间去存放个数。

这就是它们所申请的空间,当然这里是举的例子比喻,A前面多开了4个字节空间,如果都使用delete释放空间。对于p2释放没影响,但是对于p3没有[]就不会向前去调整p3的指向,那么前面的多开的4个字节空间就没办法释放,就存在内存泄漏,同时释放空间不能在申请的内存中间去释放。使用delete[]就可以很好的避免这种问题出现。所以一定记住delete与new匹配使用,delete[]与new[]匹配使用。
六、总结
内存管理方式是很重要的一节,在很多时候我们都需要去动态在堆上申请空间,如何正确申请空间,怎样正确释放空间,防止内存泄漏是非常重要的。希望我的实现思路能够帮助到你,有些思路我想尽可能说清楚就会有一些冗余,希望大家不要介意,有错误的地方也请温柔的指出来谢谢。我们一起努力进步,加油!


被折叠的 条评论
为什么被折叠?



