内存管理
一、C/C++内存分布
C/C++中程序内存区域划分为栈、内存映射段、堆、数据段、代码段
栈:存放非静态局部变量、函数参数、返回值等等,是向下增长的
内存映射段:用于装载一个共享的动态内存库,做映射
堆:用于程序运行时动态内存分配,是向上增长的
数据段:存储全局变量和静态数据,也叫静态区
代码段:存储可执行的代码以及只读常量,也叫常量区
#include <stdlib.h>
int a = 1;
static int b = 1;
void Test()
{
static int c = 1;
int d = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pchar3 = "abcd";
int* p1 = (int*)malloc(sizeof(int) * 4);
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 4);
free(p1);
free(p3);
}
栈中数据:d、num1、*num1、char2、*char2、pchar3、p1、p2、p3
堆中数据:*p1、*p2、*p3
数据段中数据:a、b、c
代码段中数据:*pchar3
二、C语言中动态内存管理方式
C语言中动态内存管理的方式有malloc、calloc、realloc、free
这里我们在以前的博文动态内存管理中有详细的介绍,这里我们可以移步来阅读一下,不做过多赘述
三、C++内存管理方式
两个关键字:new和delete
1、new和delete操作内置类型
void Test()
{
// 动态申请一个int类型的空间
int* p1 = new int;
// 动态申请一个int类型的空间并初始化为0
int* p2 = new int(0);
// 动态申请10个int类型的空间
int* p3 = new int[10];
//动态申请10个int类型的空间并部分初始化
int* p4 = new int[10] {0, 1, 2};
delete p1;
delete p2;
delete[] p3;
delete[] p4;
}
new的对内置类型使用就是new后面加一个内置类型,返回一个该类型的指针,方括号是数组元素个数,圆括号是一个元素时初始化,花括号与数组的赋值相同,是数组元素初始化
delete是与new配套使用的,要与new的类型一一对应,否则出现的问题是不可预料的,不同的编译器有不同的问题
申请和释放单个元素的空间用new和delete,申请和释放多个元素的空间用new[]和delete[]
2、new和delete操作自定义类型
new和delete比malloc等C语言的内存管理方式更强的地方之一就在于new和delete可以分别调用自定义类型的构造和析构函数,而malloc它们不行
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));
A* p2 = new A(1);
free(p1);
delete p2;
//内置类型
int* p3 = (int*)malloc(sizeof(int));
int* p4 = new int;
free(p3);
delete p4;
//多个自定义类型
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
图中我们可以很明显的看到,malloc是不调用构造函数的,free也是不调用析构函数的,而new和delete就可以
四、operator new 和operator delete 函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数,是new和delete的底层函数,也就是说,new和delete是通过operator new 和operator delete 实现的申请和释放空间,下面我们来了解一下operator new 和operator delete的机制
operator new:通过malloc来申请空间,当malloc申请空间成功后直接返回,当malloc申请失败后尝试执行用户设置的空间不足的应对措施,继续申请,否则就抛异常
operator delete :最终通过free来释放空间
所以它们归根结底还是从C语言的根中生长的,最终还要回到malloc 和 free 中,只不过我们经过包装,使得它有更多的作用
五、new 和 delete 实现的原理
1、内置类型
如果申请的是内置类型的空间,new与malloc、free与delete基本相似,不同点在于new在申请空间失败时会抛异常,而malloc会返回NULL
2、自定义类型
(1)new
首先调用operator new函数申请空间,然后在申请的空间上执行构造函数
(2)delete
首先在空间上执行析构函数,清理对象中的资源,然后调用operator delete函数释放对象的空间
(3)new[ ]
首先调用operator new[ ]函数申请空间,实际上是调用多个operator new函数申请空间,然后在申请的空间上执行多个构造函数
(4)delete[ ]
首先在空间上执行多次析构函数,清理多个对象中的资源,然后调用operator delete[ ]函数释放对象的空间,也就是调用多个operator delete
六、定位new(placement new)表达式
定位new的表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
格式:new(指针) type 或者 new(指针) type(类型的初始化列表)
场景:配合内存池使用,因为内存池分配出的内存没有初始化,如果是自定义类型的对象,就需要使用new的定义表达式进行显示调用构造函数进行初始化
(内存池是在真正使用内存之前,预先分配一定数量的、大小相等或相近的内存块留作备用。当有新的内存需求时,就从内存池中分配一块内存块,若内存块不够则继续申请新的内存块)
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// p1现在指向的是与A对象相同大小的一段空间,不是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
// 如果A类的构造函数有参数时,此处需要传参
new(p1)A;
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
//初始化列表初始化
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
七、malloc和new、free和delete的区别
共同点:都从堆上申请空间,需要用户手动释放
不同点:
(1)malloc和free是函数,new和delete是操作符
(2)malloc申请空间不会初始化,new申请空间会初始化
(3)malloc申请空间需要手动计算空间大小并传递,new只要在后边跟上空间的类型就可以自动计算空间大小,如果new多个对象只要在后边[ ]中加入指定对象个数就可以了
(4)malloc的返回值为void*,在使用时需要强制类型转换,new的返回值就为后边所跟类型type的指针形式type*,不需要强制类型转换
(5)malloc申请空间失败时返回NULL,所以使用时必须判空,new申请失败需要捕获异常
(6)申请自定义类型的时候,malloc和free只会申请和释放空间,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间后会调用析构函数完成空间中资源的清理
八、内存泄漏
1、内存泄漏的危害
内存泄漏我们在之前也提到过,它是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况,指应用程序分配某段内存后因为设计错误失去了对某段内存的控制,导致我们不能再使用这一块内存,而不是内存在物理上的消失
长期运行的程序,比如说某某公司的服务器,如果出现内存泄漏影响会很大,会导致响应越来越慢,最终导致无可控制内存可用,程序卡死
2、内存泄漏的种类
(1)堆内存泄漏:就是malloc、calloc、realloc或者new从堆中申请的一块内存用完后必须调用free或new释放掉,不释放就会造成堆内存泄漏
(2)系统资源泄露:就是程序使用系统分配的资源没有使用对应的函数释放掉,导致系统资源的浪费
3、避免内存泄漏的方法
(1)工程前期良好的设计规范,养成良好的代码习惯,申请了内存用完就要释放
(2)采用RAII思想或者智能指针来管理资源
(RAII ,也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源)
(智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象)
(3)在工作时有些公司内部的库中有检测功能
(4)内存泄漏工具检测
九、抛异常及捕获异常
需要一套关键字:try 和 catch,try用于包围可能抛出异常的代码,catch用于捕获并处理try中抛出的异常
#include <stdexcept> // 包含std::runtime_error
//这个runtime_error是一个运行时错误,可以直接被捕获到
void test()
{
try
{
throw std::runtime_error("发生了运行时错误!");
//new就有throw的功能,将错误抛出
}
catch (const std::runtime_error& e)
{
std::cerr << "捕获到异常: " << e.what() << std::endl;
}
//这里cerr是标准错误输出流,专门用来输出错误信息,当然用cout也一样可以
std::cout << "程序继续执行" << std::endl;
}
今日分享结束~