前言:
在上期,我们已经对类和对象的全部知识进行了总结和梳理。在类和对象学习完之后,今天我将给大家呈现的是关于——C/C++内存管理的基本知识。
本文目录
1. C/C++内存分布
2. C语言中动态内存管理方式
(1)C语言跟内存分配方式
(2)C语言跟内存申请相关的函数
(3)面试题
3. C++中动态内存管理
3.1 new/delete操作内置类型
3.2 new和delete操作自定义类型
4. operator new与operator delete函数😎
5. new和delete的实现原理
5.1 内置类型
5.2 自定义类型
6. 定位new表达式(placement-new)
7. 常见面试题
7.1 malloc/free和new/delete的区别
8.总结
1. C/C++内存分布
C++的内存管理其实还是延续了C语言的内存管理的规则,首先第一点带大家解答为什么需要内存管理?🤔
- 因为在程序里面需要不同类型或者不同性质的数据,那这些不同的数据是存在不同的区域,例如我之前讲过内存中有堆区,栈区,静态区等不同的区域。
- 有这些区域的本质原因,即是对于不同的数据类型有不同的特性,我们为了方便更好的对其进行管理呢,就会把一些相同特性的数据分到同一区域进行管理。
接下来,我们通过习题的方式带大家来具体的理解其中的关系:
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在哪里?____
解析如下:
- 【globalVar】: globalVar作为全局变量在数据段(静态区)
- 【staticGlobalVar】:staticGlobalVar作为静态全局变量在静态区
- 【staticVar】:staticVar作为静态局部变量在静态区
- 【localVar】: localVar作为局部变量,局部变量放在栈区
- 【num1】: num1为局部变量,一样也存放在栈区
以上这个五个我相信大多数小伙伴都可以作对的,接下来我们讲解下面几个:
- 【char2】: char2是一个数组,跟num1的区别是(num1是自己确定大小,而char2是通过初始化来确定大小),所以不难得出num1和char2是在一个地方的,即——作为局部变量,放在栈区
- 【*char2】:char2是一个数组,把后面常量字符串拷贝过来到数组中,数组在栈上,所以*char2在栈上
- 【pChar3】: pChar3局部变量在栈区
- 【* pChar3】: *pChar3得到的是字符串常量字符在代码段
- 【ptr1】: ptr1是指针,作为局部变量在栈区
- 【* ptr1】: *ptr1就是ptr指向的那块空间,因此得到的是动态申请空间的数据在堆区
接下来,我们结合图形,大家可以直观的感受!!
接下来,还有个连环的问题,大家在看看下列的题目,不知道各位是否能够拿下它呢?
sizeof(num1) = ____;
sizeof(char2) = ____; strlen(char2) = ____;
sizeof(pChar3) = ____; strlen(pChar3) = ____;
sizeof(ptr1) = ____;
解析:
- sizeof(num1) = __40__; sizeof数组名,即计算数组大小,10个整形数据一共40字节
- sizeof(char2) = __5__; 包括\0的空间,因此为5
- strlen(char2) = __4__; 遇到\0即截止,不包括\0的长度,因此为4
- sizeof(pChar3) = __4__; 因为pChar3为指针,所以跟后面指向的美誉关系,32位下为大小4,64位下8
- strlen(pChar3) = __4__; 字符串“abcd”的长度,不包括\0的长度
- sizeof(ptr1) = __4__; ptr1是指针,同上
【说明】🤫
- 1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
- 3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 4. 数据段--存储全局数据和静态数据。
- 5. 代码段--可执行的代码/只读常量。
通过上述的问题带大家仔细再次认识了一下程序在内中的分布问题。接下来,我们将探讨关于动态内存管理方式的问题!!!
2. C语言中动态内存管理方式
这个知识点,我们在之前就已经具体的讲到过了,在这里给大家简单的在过一遍。这里给大家一段代码,大家先回顾一下之前有关的知识:
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}
(1)C语言跟内存分配方式
- <1>从静态存储区域分配.
- 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量.
- <2>在栈上创建
- 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限.
- <3>从堆上分配,亦称动态内存分配.
- 程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存.动态内存的生存期由用户决定,使用非常灵活,但也很容易出现问题
(2)C语言跟内存申请相关的函数
C语言跟内存申请相关的函数主要有calloc、malloc、free、realloc等.
- <a>malloc分配的内存是位于堆中的,并且没有初始化内存的内容,因此基本上malloc之后,调用函数memset来初始化这部分的内存空间.
- <b>calloc则将初始化这部分的内存,设置为0.
- <c>realloc则对malloc申请的内存进行大小的调整.
- <d>申请的内存最终需要通过函数free来释放.
⭐切记:
- 当程序运行过程中malloc了,但是没有free的话,会造成内存泄漏.一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使用,造成不断的向系统申请内存,使得系统可用内存不断减少.
对于malloc详细的知识可以参考如下地址:
malloc函数
对于calloc详细的知识可以参考如下地址:
calloc函数
对于realloc详细的知识可以参考如下地址:
realloc函数
(3)面试题
接下来给大家解答一个常见的面试题——malloc/calloc/realloc的区别?
区别:
(1)函数malloc不能初始化所分配的内存空间,而函数calloc能
- 如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;
- 反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.
- 也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间已经被重新分配)那么就可能会出现问题.
(2)函数calloc() 会将所分配的内存空间中的每一位都初始化为零
- 也就是说,如果你是字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;
- 如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;
- 如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零.
(3)函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void*类型.
- void*表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针.
(4)realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.
- 当然,如果是缩小,则被缩小的那一部分的内容会丢失.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址.
- 相反,realloc返回的指针很可能指向一个新的地址.
(5)realloc是从堆上分配内存的.
- 当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;
- 如果数据后面的字节不够,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动.
3. C++中动态内存管理
3.1 new/delete操作内置类型
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:
- 通过new和delete操作符进行动态内存管理。
首先第一个知识点来了,那就是小伙伴们知道默认情况下【new】会不会初始化呢?
我通过调试带大家仔细瞧瞧是不是像我所说的那样:
- 从上述我们不难发现,不管是对于【new】定义的【p1】来说,还是通过传统的【malloc】方式定义的【p2】来说,默认都是没有进行初始化操作的!!!
那么大家就会好奇了,【new】是否可以初始化呢?
- 答案当然是可以的,接下来我带大家看看具体的操作。
此时,一个可能让大家混淆的点就出现了,大家是否能够区别下述这种方法呢?😖
int* p1 = new int(1);//初始化
int* p3 = new int[10];
大家是否知道这两个的区别呢?不知道没关系,接下来我给大家解答一下:
- 对于【int* p1 = new int(1);】:这是进行初始化操作,申请一个(int)类型,并初始化为1;
- 对于(int* p3 = new int[10];):它的意思动态申请10个int类型的空间
- 大家一定区分二者之间的差别,不要搞混淆了!!
那对于(int* p3 = new int[10];)这种情况,我们是否还能对其初始化呢?其实也是可以的,C++支持这样的操作,具体如下所示:
int* p4 = new int[10] {1, 2, 3, 4};
当我们想对其进行初始化时,只需在后面加上【{}】即可,那么是不是呢?我通过调试给大家展示:
我们从上述不难看出,当我们用【new】时是不是比我们用【malloc】方便得多呀!
- 对于【malloc】我们不仅需要强转类型,还需要进行检查
还是通过代码,大家就可以一目了然两者之间的差别到底有多大:
注意:
- 此时很多小伙伴或许就会问,难道【new】不会失败吗?其实是会的,它失败的机制跟【malloc】是不一样的,它通过“抛异常”来进行错误判别的,我们后面会讲到。
我用一张图给大家总结,大家看下表就能直观的理解上面的知识了:
特别注意一点:💣
- 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],注意:匹配起来使用。
3.2 new和delete操作自定义类型
上述我们已经知道一点,new/delete 和 malloc/free对于内置类型的处理几乎是一样的,但是对于自定义类型是否也是一样的呢?
我们还是通过代码来大家理解这个问题,大家看以下代码,最终的结果是什么呢?
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;
return 0;
}
解析:
我们直接打印看看最后的结果是什么。
- 通过上述不知道大家有没有发现一个点呀!那就是此时这里我们是定义的两个,怎么只调用了一次构造和析构函数呢?是谁没有调用呢?接下来,通过调试,我带大家一步一步的去查看
解析:
- 第一步,首先是对【p1】进行的操作,此时当执行完【p1】之后,我们发现并没有去调用构造函数。
解析:
- 此时,当我们去执行完【p2】之后,我们发现,此时程序就去调用了构造函数,并且成功的完成了初始化操作。继续执行
解析:
- 当执行完毕,对其进行释放的时候,我们可以发现,程序是先对【p1】进行的释放,但是此时并没有去调用析构函数。
解析:
- 最后,当我们对【p2】进行释放的时候,此时程序调用了析构函数,并且也成功的把【p2】释放掉了。
因此,综上所述:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与 free不会。
4. operator new与operator delete函数😎
- 上述我们已经知道对于 【new】和【delete】是用户进行动态内存申请和释放的操作符;
- 而接下来要学习的operator new 和operator delete是系统提供的全局函数;
- new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。
大家是不是一看到这个就以为是函数重载呀!在之前我们已经学习过,【new】是函数,而【operator】是重载的符号:
- 其实不然啊,大家千万不要这么理解。这里的两个函数是库里面提供的两个全局函数,不是运算符重载哟!!!
- 取这个名字给大家造成了极大的误解,至于为什么要这么定义呢?我们也不得而知了,可能我们的祖师爷在设计的时候没有想到好名字,这就造成了许多学习【C++】的在这里吃了一个亏。
接下来带大家浅浅的看一下库里面是怎么实现这两个函数的,以下为库里面的代码(看不明白没关系)
对于【operator new】,库里面是这么写的:
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
解析:
- 大家看代码,我们可以看到operator new里面是不是调用的【malloc】啊,只是它这里跟【malloc】不同的是,紧接着往下看我们可以发现,上述代码【malloc】之后赋值给了【p】,然后判断,如果【p】等于0的话就调用【(_callnewh(size) == 0】这个函数,然后进入里面进行了“抛异常”,即【malloc】失败了就会抛异常。
而对于【operator delete】,库里面是这么写的:
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
解析:
- 上面那一大串的大家都可以不用看,看最后的一两行,最后是不是显示的【free】啊!
因此,我们可以得出一个结论:
- 通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。
- operator delete 最终是通过free来释放空间的。
- 因此,【operator delete】和【operator new】本质上就是【delete】和【new】的封装。
接下来就给大家讲解其中的原因?
首先,我们怎么使用者两个函数呢?其实吧,这两个函数的实现跟【malloc】是类似的,我们举例观察:
解析:
- 对于上述的【p1】,我们使用的是【operator new】,我们不难发现跟【malloc】的实现方式很类似。
- 只是底层的机制不一样罢了。对于【malloc】实现,会进行严格的检查,而对于【operator new】则是失败后“抛异常”!!
紧接着大家是否好奇为什么会有这两个函数呢?
- 我们都知道【c++】兼容C语言,就拿我们之前以及讲过的“引用”为例,当在【C++】中为了实现这个会去单独的创造一套语法啊什么的出来吗?应该不会吧。引用底层是按指针的方式来实现引用,那么这时还有必要再去创造新的东西呢?结果可能而知。
- 现在我们来对比一下【new】的实现,第一步是申请空间,紧接着就是调用构造函数。对于申请空间是不是就是去堆上申请啊,在【C】语言中我们也是从堆上申请的呀!而在【C】语言中,对于申请空间,是不是就要调用【malloc】。
- 而对于【C++】来说,因为它是面向对象的语言,虽然兼容C语言,但是新增的那部分是面向对象的,而面向对象的语言处理异常使用的是“抛异常”。而在【C】语言中,对于失败,返回的是【null】,就不符合我们的需求,所以C++就用【operator new】去封装【malloc】,封装【malloc】失败之后“抛异常”!!!🥶
我们在通过反汇编的角度去看看,不难发现底层确实像我们所说的那样:
5. new和delete的实现原理
5.1 内置类型
- 如果申请的是内置类型的空间,new和malloc,delete和free基本类似.
- 不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申 请空间失败时会抛异常,malloc会返回NULL。
5.2 自定义类型
new的原理
- 1. 调用operator new函数申请空间
- 2. 在申请的空间上执行构造函数,完成对象的构造
delete的原理
- 1. 在空间上执行析构函数,完成对象中资源的清理工作
- 2. 调用operator delete函数释放对象的空间
new T[N]的原理
- 1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对 象空间的申请
- 2. 在申请的空间上执行N次构造函数
delete[]的原理
- 1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释 放空间
6. 定位new表达式(placement-new)
作用:定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
- new (place_address) type或者new (place_address) type(initializer-list)
- place_address必须是一个指针,initializer-list是类型的初始化列表
我们先看一下以下代码:
int main()
{
A aa;
A* p1 = (A*)malloc(sizeof(A));
if (p1 == nullptr)
{
perror("malloc fail");
}
return 0;
}
解析:
此时,我们这里有一块已经【malloc】出来的空间,此时当我们在想对其进行初始化时是不行的
此时对于【C++】来说,对已经有一块空间的进行初始化,此时应该怎么办呢?基于这种情况,就引入了——定位new
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
new(p1)A(1);
接下来,我们通过调试带大家去看看:
解析:
- 对于没有参数的时候,调试出来的我们发现也没有参数。
解析:
- 对于有参数的时候,调试出来的我们对其进行了初始化操作。
当我们想去调用析构函数时,我们可以显示的去调用是可以的,具体如下:
int main()
{
A aa;
A* p1 = (A*)malloc(sizeof(A));
if (p1 == nullptr)
{
perror("malloc fail");
}
//new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
new(p1)A(1);
p1->~A(); //调用析构函数,显示的去调用
free(p1);
return 0;
}
输出结果为:
大家看完这个是不是会觉得十分麻烦呀!这么复杂搞得,我们直接【new】不是更好吗?具体如下:
A* p2 = new A;
- 其实确实是这样的,在实际当中对其自定义类型,我们直接用【new】就可以了,没必要再去【malloc】紧接着再去使用这个定位new。
那么何时我们改用这个呢?
- 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
如果还没明白。接下来我讲个故事,大家可以类比的进行理解:
- 在以前,假如还没有水缸之类的东西用于存储,假如此时我们要煮饭,我们就去水井里舀水回来,总之就是当我们想使用水时时就要去水井里盛水回来;
- 现在引入内存池,意思就相当于现在有装水的容器了,我们可以一次性盛许多水回来,当我们想用时就不用再每次去水井里盛水了,只有当装水的容器里没了之后,我们才去水井里盛水。
7. 常见面试题
7.1 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在释放空间前会调用析构函数完成 空间中资源的清理
8.总结
以上便是关于C/C++内存管理的问题,还有部分关于内存泄漏的问题,我们到后面在讲。最后,总结一下本期的内容。
对于内存管理呢,C/C++其实是类似的,唯独在C++中引入了【new】和【delete】机制。
并且建议使用【new】和【delete】机制,原因有二:
- 第一是因为用起来方便许多相比于【malloc】这种方式;
- 第二个原因针对自定义类型,【new】和【delete】能更好的调用构造函数和析构函数,而【malloc】则不满足这种场景。
到此,便是关于内存管理的所有内容。如果感觉对您有帮助的话,麻烦点赞三连哟!!!