详解C/C++内存管理
1. 内存布局的分布
在讲解内存管理前,我们先了解一下C/C++中内存布局的分布,一个典型的 C++ 程序内存通常分为以下几个区域:
(1)栈 :用于存储函数的局部变量、函数参数、返回地址等
管理方式:由编译器自动分配和释放。当函数被调用时,其变量在栈上创建;当函数返回时,这些内存被自动回收。
特点:速度快,但大小有限(通常几 MB)。生命周期由作用域决定。
#include<iostream>
using namespace std;
int Add(int x, int y)//这里的函数调用就是栈的创建
{return x + y;
}
int main()
{std::cout << Add(10, 20) << endl;//调用结束后,空间会被收回return 0;
}
(2)堆 :用于动态分配的内存。这是手动管理内存的主要战场。
管理方式:由程序员显式地分配(new
, malloc
)和释放(delete
, free
)。如果忘记释放,会导致内存泄漏。
特点:容量大(只受系统可用内存限制),但分配和释放速度较慢,需要手动管理生命周期。
int main()
{int* ptr= (int*)malloc(10*sizeof(int));//malloc在堆上开辟空间int* ptr2 = new int(1);//用new来开辟空间,还可以进行初始化,给ptr2初始化为1;return 0;
}
(3)全局/静态存储区 :用于存储全局变量、静态变量(static
关键字)。
管理方式:在程序开始时分配,程序结束时释放。
特点:生命周期贯穿整个程序。
int x=10;//全局变量
int Add(int x,int y)
{static int=k//静态变量,相当于全局变量k=x+y;return k;
}
int main()
{int y=0;//局部变量。return 0;
}
(4)常量存储区 :用于存储字符串常量和其他常量。
特点:里面的数据是只读的,修改它会导致未定义行为(通常程序崩溃)
const char* str = "Hello, World"; //"Hello, World" 本身在这个区域
(5)代码区:用于存储程序的二进制代码(计算机指令)
知道了以上的区域划分后,我们可以来看一个练习
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在哪里?____
答案为: CCCAAAAADAB
2.手动内存管理类
在C语言当中,我们通常用malloc/realloc/free等内存函数来管理内存。那么在C++当中我们管理内存通常用new/delete。
2.1 new/delete操作内置类型
基础语法如下:
int main()
{int* ptr1 = new int;//动态申请一个int类型空间int* ptr2 = new int(10);//动态申请一个int类型空间并初始化为10;int* ptr3 = new int[3];//动态申请三个int类型空间delete ptr1;delete ptr2;delete []ptr3;//特别主义,释放第三种的空间的时候,在delete后面加[];return 0;
}
2.2 new和delete操作自定义类型
new和delete当然也可以操作自定义类型。
我们看以下代码即可理解:
#include<iostream>
using namespace std;
class A
{
private:int _a;
public:A(int a = 0):_a(a){cout << "A()" << this << endl;}~A(){cout << "~A()" << this << endl;}
};
int main()
{//new/delete和malloc/free的最大区别是,malloc/free只负责开空间,//new/delete在开空间的基础上还会调用自定义类型里面的构造函数和析构函数A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1);free(p1);delete p2;return 0;
}
在内置类型里面new/delete和malloc/free的区别就不大,可以理解为几乎是一样的。
int main()
{int* ptr1 = (int*)malloc(sizeof(int));int* ptr2 = new int(0);free(ptr1);delete ptr2;return 0;
}
3.operator new与operator delete函数
尽管我们已经了解了new/delete的用法,但是实际上我们真正的想掌握重载new delete的方法,首先就要对new/delete表达式的工作机理有更多了解。
例如我们刚才的例子:
int main()
{int* ptr1 = new int;//动态申请一个int类型空间int* ptr2 = new int(10);//动态申请一个int类型空间并初始化为10;int* ptr3 = new int[3];//动态申请三个int类型空间delete ptr1;delete ptr2;delete []ptr3;//特别主义,释放第三种的空间的时候,在delete后面加[];return 0;
}
以上的代码实际上执行了三步操作。第一步,new表达式调用一个名为operator new(operator new[ ])的标准库函数。也就是说new/delete只是一个定义,它要调用内层的标准库函数。这个operator new/operator delete的库函数会分配一块足够大,原始的,未命名的内存空间以便存储特定类型的对象(或者对象的数组)。第二步,编译器运行相应的构造函数以构造函数这些对象,并为其传入初始值。第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针。
当我们使用一条delete表达式删除一个动态分配的对象时
int main()
{int* ptr2 = new int(0);int* ptr3 = new int[3];delete ptr2;//销毁*ptr2,让后释放sp指向的内存空间delete []ptr3;//销毁数组中的元素,让后释放对应的内存空间。return 0;
}
这里的delete实际上执行了两步操作。第一步,对ptr2所指的对象或者ptr3所指向的数组中的元素执行对应的析构函数。第二步。编译器调用名为operator delete(operator delete[ ])的标准库函数来释放内存空间
如果应用程序希望控制内存分配的过程,则他们需要定义自己的operator new函数和operator delete函数。即使在标准库已经存在这两个函数的定义,我们仍旧可以定义自己的版本。编译器不会对这种重复的定义提出异议,相反,编译器将使用我们自定义的版本替换标准库定义的版本。
应用程序可以在全局作用域中定义operator new和operator delete函数,也可以将它们定义为成员函数。当编译发现一条new表达式或delete表达式后,将在程序中查找可供调用的operator函数。
如果被分配(释放)的对象是类类型时,则编译器首先在类及其基类的作用域中查找。此时如果该类含有operator new/delete成员。则相应的表达式将调用这些成员。否则,编译器在全局作用域查找匹配的函数。此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表示会;如果没找到,就用标准库函数。
3.1 operator new/delete 接口
标准库中定义了operator new/delete的八个重载版本。其中前4个版本可能抛出bad_alloc异常,后四个则不会抛出异常。
void* operator new(size_t);//分配一个对象
void* operator new[](size_t);//分配一个数组
void* operator delete(void*) noexcept;//释放一个对象
void* operator delete[](void*) noexcept;//释放一个数组
//这些版本承诺不会抛出异常
void* operator new(size_t,nothrow_t&) noexcept;
void* operator new[](size_t,nothrow_t&) noexcept;
void* operator delete(void*,nothrow_t&) noexcept;
void* operator delete[](void*,nothrow_t&) noexcept;
我们来逐个语句解释:
(1)void* operator new(std::size_t); //分配一个对象
作用:请求分配至少size字节的连续内存空间。
参数: size 需要分配的字节数。对于单个对象,这个值通常等于sizeof(myclass) ,但编译器可能传入更大的值。
返回值:成功时,返回指向分配的内存块起始位置的指针。
失败行为:如果分配失败(如内存不足),默认情况下会抛出 std::bad_alloc
异常。
(2)void* operator new[](std::size_t); //分配一个数组
作用:与单个对象版本完全相同,但它用于分配数组。
参数:size e
- 需要分配的字节数。对于 new myclass[N],这个值通常至少是 N *
sizeof(myclass),但编译器可能会分配更多空间(例如用于存储数组长度等元信息)。
(3) void operator delete(void*) noexcept; //释放一个对象
作用:释放之前由operator new分配的单个对象的内存。
参数:ptr - 一个指向待释放内存块的指针,必须是之前从 operator new 返回的值或者是空指针。
行为:如果 ptr
是 nullptr
,则该函数不执行任何操作。该函数不调用析构函数(析构函数由 delete
表达式在调用 operator delete
之前调用)。
异常规范:noexcept
表示该函数承诺不会抛出任何异常。这对于析构路径中的安全性至关重要
(4) void operator delete[](void*) noexcept; //释放一个数组
作用:释放之前由 operator new[]
分配的数组的内存
调用时机:当你使用 delete[] arr
时,底层就会调用这个函数。
(5)//这些版本承诺不会抛出异常
void* operator new(std::size_t, std::nothrow_t&) noexcept;
void* operator new[](std::size_t, std::nothrow_t&) noexcept;
作用:与基础版本功能相同,都是分配内存。
关键区别:如果分配失败,它们不会抛出 std::bad_alloc
异常,而是直接返回一个空指针 (nullptr
)。
(6)void operator delete(void*, std::nothrow_t&) noexcept;
void operator delete[](void*, std::nothrow_t&) noexcept;
作用:用于释放由对应的 Nothrow operator new
分配的内存。
提示:在实践中,标准的 operator delete(void*)
版本完全可以释放任何 operator new
版本(包括nothrow)分配的内存。释放机制是通用的。这些版本的存在主要是为了语法上的对称性和完整性,但很少需要显式使用。
总结:
分离性:内存分配 (operator new
) / 释放 (operator delete
) 与对象构造(构造函数) / 析构(析构函数)是分离的。
异常安全:基础版本通过异常报告失败,而 nothrow
版本通过返回 nullptr
报告失败。
通常你不会直接调用它们:你使用 new
和 delete
表达式,编译器会自动为你生成调用这些底层函数的代码。
重载:你可以为你自定义的类重载这些函数,以实现自定义的内存管理策略(例如内存池)。
4.malloc/free和new/delete的区别
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在释放空间前会调用析构函数完成
空间中资源的清理释放