1 C/C++内存分配方式
- 在C/C++中,内存分成5个区,他们分别是堆、栈、内存映射段、数据段和代码段。
如图所示
堆
:用于满足程序动态内存分配的空间,简而言之就说new分配的内存块,其分配和释放都由程序员控制。如果不主动释放,容易产生内存泄漏;程序结束后,操作系统也会自动回收程序动态申请的内存。
栈
:程序内局部变量存放的区域,包括非静态局部变量、函数参数和返回值等等,生命周期只在其声明和定义的那个代码段,当函数执行结束后,其空间会被自动释放。
内存映射段
:是一种高效的I/O映射方式,用于装载一个共享的动态内存库。在该区域用户可通过系统调用创建共享内存,进行进程通讯。
数据段
:也称全局/静态存储区,故全局变量和静态变量存放在该区域。
代码段
:也称常量存储区,故存放可执行代码和只读常量。
2 C++内存管理方式
- C语言中主要是通过以下四个函数malloc/calloc/realloc/free对内存进行动态管理,C++则使用new和delete操作符进行动态内存管理,观察如下代码。
void Test() {
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
int* p3 = new int[5]; // 5个int数组
int* p4 = new int(5); // 初始化位5
int* p5 = new int[5]{ 1,2,3,4 };
free(p1);
delete p2;
delete[] p3;
delete p4;
delete[] p5;
}
- 对于内置类型的动态内存管理 new/delete 与 malloc/free 只用法不同。需要注意的是在申请和释放单个元素的空间时候,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。
- 而对于自定义类型,new/delete 和 malloc/free最大区别是 new/delete目的是为[自定义类型]准备的,因为与malloc/free相比,new/delete除了申请和释放堆上的空间还会调用构造函数/析构函数,观察如下代码。
class A{
public:
A(int a = 0)
: _a(a){
cout << "A():" << this << endl;
}
~A(){
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main(){
// 1在堆上申请空间
A* p1 = (A*)malloc(sizeof(A)); // C
if (p1 == NULL) {
perror("malloc fail\n");
return 0;
}
cout << "malloc完成#########" << endl;
// 1在堆上申请空间 2调用构造函数初始化
A* p2 = new A(1);
cout << "new完成############" << endl;
// 1释放空间
free(p1);
cout << "free完成###########" << endl;
// 1调用析构函数清理对象中的资源 2释放空间
delete p2;
cout << "delete完成#########" << endl;
return 0;
}
运行结果如下:
-
malloc/free和new/delete的异同点总结
-
共同点:
从堆上动态申请空间,并且需要程序员手动释放,以避免内存泄漏。
- 不同点:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[] 中指定对象个数即可
- malloc的返回值为 void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
3 new与delete实现与重载
- new和delete作为进行动态内存申请和释放的操作符,其底层是通过调用两个全局函数(operator new 和operator delete)进行空间的申请和释放的。
- 通过查看两个全局函数的实现时,可发现operator new实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete最终是通过free来释放空间的,具体使用场景如下。
int main() {
// malloc要检查返回值,如果malloc失败返回NULL
char* p1 = (char*)malloc(10);
// char* p1 = (char*)malloc(1024u * 1024u * 1024u * 2 - 1);
printf("%p\n", p1);
// new不需要检查返回值,如果new失败抛出异常
try {
char* p2 = new char[10];
// char* p2 = new char[1024u * 1024u * 1024u * 2 - 1];
printf("%p\n", p2);
delete[] p2;
}
catch (const exception& e) {
cout << e.what() << endl;
}
return 0;
}
- 一般情况下,我们无需对该两个全局函数进行重载,只有当我们在申请和释放空间有特殊需求时才进行重载,如需要打印一些日志信息等信息进行检查时。
重载一个类专属的operator new/delete
struct ListNode {
int _val;
ListNode* _next;
// 类内声明
// 内存池
static allocator<ListNode> alloc;
void* operator new(size_t n) {
//
cout << "void* operator new -> STL内存池allocate申请" << endl;
void* obj = alloc.allocate(1);
return obj;
}
void operator delete(void* ptr) {
//
cout << "void* operator delete -> STL内存池allocate释放" << endl;
alloc.deallocate((ListNode*)ptr, 1);
}
struct ListNode(int val)
:_val(val)
,_next(nullptr)
{}
};
// 类外定义
allocator<ListNode> ListNode::alloc;
int main() {
// new -> operator new + 构造函数
// 默认情况下operator new使用全局库函数,会使用malloc
// 频繁申请ListNode,为提高效率,不使用malloc,而使用自己定制的内存池
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
delete node1;
delete node2;
delete node3;
return 0;
}
运行结果如下:
4 定位new表达式
- 定位new表达式的作用的是在已经分配的动态内存空间中,调用构造函数进行初始化一个对象,具体例子如下:
class A{
public:
A(int a = 0)
: _a(a){
cout << "A():" << this << endl;
}
~A(){
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main(){
A* p1 = (A*)malloc(sizeof(A));
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
new(p1)A;
// 定位new表达式,注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
// 手动调用析构函数
free(p1);
// 释放malloc申请的空间
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
delete p2;
return 0;
}
- 定位new表达式在实际中一般是配合内存池使用,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定位表达式进行显示调构造函数进行初始化。