目录
一. C/C++内存分布
二. C/C++动态内存管理
2.1 C语言动态内存管理
2.2 C++动态内存管理
2.2.1 new/delete操作符
2.2.2 operator new与operator delete函数
2.3 new/delete的实现原理
2.4 定位new(placement - new)
2.5 new/delete和malloc/free的区别
三. 内存泄漏
3.1 什么是内存泄漏
3.2 内存泄漏的危害
一. C/C++内存分布
对于任何一个C/C++程序,其程序内存可以划分为下面几个区域:
- 内核空间:用户代码不能读写
- 栈区:存储非静态的局部变量、函数参数和返回值等。
- 内存映射段:用于装载一个共享的动态内存库,用户可使用系统接口创建共享内存,做进程间通信。
- 堆区:如果程序中需要用到较大的内存区域,则用户可以手动在堆区申请内存空间。
- 数据段:用于存储全局数据和静态数据。
- 代码段:用于存储可执行代码和只读常量。
图1.1以一段具体的程序为例,表述了每块内存区域存储的内容。
二. C/C++动态内存管理
2.1 C语言动态内存管理
涉及四个主要函数,分别为malloc、calloc、realloc、free
- malloc:动态申请一块内存空间,不进行初始化,函数原型为void* malloc(size_t size),在使用malloc函数的返回值是要进行强制类型转换。
- calloc:动态申请内存空间,并将申请的空间的内容初始化为0。函数原型为:void* calloc(size_t num, size_t size),num表示申请内存空间的块数,size表示每块空间的大小。
- realloc:调整一块已经申请的内存空间的大小,函数原型为void* realloc(void* ptr, size_t size),其中ptr指向要调整大小的内存空间、size表示调整后的内存空间大小。
- free:释放动态申请的内存空间。
2.2 C++动态内存管理
2.2.1 new/delete操作符
在C++中,可以使用new来申请堆区内存空间,采用delete释放堆区内存空间,new的使用语法为:
- 申请单块内存空间不初始化:数据类型* ptr = new 数据类型
- 申请数组空间不初始化:数据类型* ptr = new 数据类型[数据量]
- 申请单块内存空间并初始化:数据类型* ptr = new 数据类型(初始化值)
- 申请数组空间并初始化:数据类型* ptr = new 数据类型{初始化值1, 初始化值2, ...... }
delete的使用语法为:
- 释放单个内存空间:delete 指向动态开辟的内存区域的指针
- 释放数组空间:delete[] 指向动态开辟的内存区域的指针 -- 其中[]就表示数组
int main()
{
int* ptr1 = new int; //申请单个int型空间
int* ptr2 = new int(5); //申请单个int型空间并初始化为5
int* ptr3 = new int[3]; //申请3个int型空间
int* ptr4 = new int[3]{ 1,2,3 }; //申请3个int型空间并初始化为1、2、3
delete ptr1;
delete ptr2; //释放单个内存空间
delete[] ptr3;
delete[] ptr4; //释放数组内存空间
return 0;
}
当使用new/delete申请和释放内置数据类型的空间时,其与malloc/free没有除语法以外的太大区别。但是,对于自定义类型(类)空间,使用new申请空间会调用类的构造函数,使用delete释放空间会调用类的析构函数。如果使用malloc/free申请和释放内存空间,则不会调用类的构造函数和析构函数。
int main()
{
//动态开辟类A的空间
//使用new动态申请自定义类型的空间时,会调用自定义类型的构造函数
//malloc-free自定义类型内存空间不会调用其构造函数和析构函数
A* p1 = new A;
A* p2 = new A[3];
//delete自定义类型的内存空间是,会调用其析构函数
delete p1;
delete[] p2; //释放动态开辟的内存空间
A* p3 = new A(10, 20); //申请类A型的内存空间并初始化
//申请3个类A型的内存空间并初始化
//注意:(10,20)会被解读为逗号表达式20,相当于只有构造函数的第一个参数给了值,第二个参数使用默认值
//(30,40)、(50,60)同理
A* p4 = new A[3]{ (10,20), (30, 40), (50, 60) };
//给构造函数传两个参数初始化要使用花括号{}而非小括号()
A* p5 = new A[3]{ {10,20}, {30, 40}, {50, 60} };
delete p3;
delete[] p4;
delete[] p5;
return 0;
}
2.2.2 operator new与operator delete函数
对于一门语言,其处理异常的方式可能是下面两种:
- 面向过程的语言(C) :返回值 + 错误码errno
- 面向对象的语言(C++、JAVA等):抛异常 -- 可使用try catch来捕获异常
如C语言中的malloc函数,如果malloc函数申请动态内存失败,那么函数就返回空指针NULL,而operator new是C++提供的动态内存申请函数,其底层就是通过调用malloc函数来实现的。而为了满足面向对象的语言出错抛异常的要求,如果动态内存开辟失败,operator new会把malloc的返回值报错方式转换为抛出异常。
operator new的使用语法:数据类型* ptr = (数据类型*)operator new(空间大小)
operator delete与operator new配对使用,释放动态申请的内存空间。
注意:operator new动态申请自定义类型内存空间不调用构造函数。
int main()
{
int* ptr1 = (int*)operator new(3 * sizeof(int)); //开辟3个int型空间
A* ptr2 = (A*)operator new(sizeof(A)); //开辟类A型空间
operator delete(ptr1);
operator delete(ptr2);
return 0;
}
如果采用operator new动态申请的空间过大,那么程序就会抛出异常。演示代码2.1采用operator new函数申请一块大小为2G的内存空间(申请空间过大导致失败),采用try - catch捕获异常,运行程序输出bad allocation。
演示代码2.1:
int main()
{
char* ptr = nullptr;
try
{
ptr = (char*)operator new(1024u * 1024u * 1024u * 2u);
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
如果程序抛出异常,那么其执行流会马上跳转到catch的位置处,哪怕是跨函数,也会跳转,并且,catch后面的语句会照常执行,知道程序结束或程序崩溃。演示代码2.2在函数func中申请了一块2G的内存空间,在申请完空间后输出"operator new successful",但是,由于期望申请的空间过大,导致申请失败,因此,程序不会输出"operator new successful",而是会直接跳转执行catch中的程序,输出bad allocation。
演示代码2.2:
char* func()
{
char* ptr = (char*)operator new(1024u * 1024u * 1024u * 2u);
cout << "operator new successful" << endl;
return ptr;
}
int main()
{
char* ptr = nullptr;
try
{
ptr = func();
}
catch(const exception& e)
{
cout << "operator new fail" << endl;
cout << e.what() << endl;
}
delete ptr;
return 0;
}
2.3 new/delete的实现原理
new/delete的底层是通过调用operator new和operator delete来实现的。
- new的实现原理:(1)调用operator new申请内存空间 (2)调用自定义类型的构造函数
- delete的实现原理:(1)调用自定义类型的析构函数 (2)调用operator delete释放内存空间
- new[]的实现原理:(1)调用operator new[]申请空间 (2)调用自定义类型的构造函数
- delete[]的实现原理:(1)调用自定义类型的析构函数 (2)调用operator delete[]释放空间
2.4 定位new(placement - new)
使用operator new申请动态内存空间,不会调用自定义类型的构造函数。但是有时候我们希望在operator new函数申请的空间上调用构造函数,可构造函数却不支持直接显式调用,这是就需要用到定位new来实现。
定位new使用语法:new(指向动态开辟的内存空间的指针)类名(传给构造函数的参数)
虽然构造函数不能显示调用,但是析构函数可以显示调用。
演示代码2.3:
class A
{
public:
A(int a1 = 10, int a2 = 20, double a3 = 20.2)
: _a1(a1)
, _a2(a2)
, _a3(a3)
{
std::cout << "A(int a1 = 10, int a2 = 20, double a3 = 20.2)" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
private:
int _a1;
int _a2;
double _a3;
};
int main()
{
A* pa1 = nullptr;
A* pa2 = nullptr;
try
{
pa1 = (A*)operator new(sizeof(A));
pa2 = (A*)operator new(sizeof(A));
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
//类的构造函数不能显示调用
new(pa1)A;
new(pa2)A(20, 30, 40.4);
//析构函数可以显示调用
(*pa1).~A();
(*pa2).~A();
operator delete(pa1);
return 0;
}
2.5 new/delete和malloc/free的区别
- 用法区别:
- malloc需要手动计算申请空间的大小,new只需要在数据类型后面的[]内输入所要申请类型空间的个数即可。
- malloc的返回值类型为void*,需要强制类型转换,而new在使用时会显示说明申请空间的类型,无需强制类型转换。
- 底层区别:
- malloc/free是函数,new/delete是操作符。
- malloc申请空间失败返回NULL,需要判空,而new申请空间失败会抛异常,需要使用catch捕获异常。
- 申请自定义类型空间时,malloc/free只会申请空间,不会调用构造函数和析构函数,new/delete不但会申请和释放空间,还会调用默认构造函数完成初始化以及调用析构函数进行对象资源的清理。
三. 内存泄漏
如果申请了动态内存空间却不手动释放,就会造成内存泄漏。
3.1 什么是内存泄漏
动态申请内存空间,不使用了,但却没有释放,就存在内存泄漏,使可用内存越来越少。
3.2 内存泄漏的危害
- 对于正常结束的进程,进程结束时泄漏掉的内存会自动还给系统,不会有太大危害。
- 对于非正常结束的进程,如僵尸进程,以及需要长期运行的程序,如服务器程序,出现内存泄漏的危害就很大,系统会变得越来越慢,甚至卡死宕机。
所以,动态申请的内存空间一定要记得释放!释放动态内存使用的函数(操作符)一定要与申请内存时用的函数(操作符)匹配:malloc--free、new -- delete、new[] -- delete[]。