一. 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在哪里?____ 2. 填空题: sizeof(num1) = ____; sizeof(char2) = ____; strlen(char2) = ____; sizeof(pChar3) = ____; strlen(pChar3) = ____; sizeof(ptr1) = ____;
其中前面1~5没有什么难点
全局变量在静态区
被Static修饰的变量在静态区
局部变量放到栈区
故答案为 C C C A A
我们再来看看后面六个
指针是在栈区(局部变量)
动态开辟出的空间是在堆区
只要记住这两点上面的题目基本不会错
唯一值得注意的是这一行代码
char char2[] = "abcd";
*char2的在哪里?
因为这时候的char2是一个指向首元素的地址 (不懂的可以去复习下C语言数组章节)
对于它解引用出来的是首元素 也就是字符‘a’ 和常量的字符串类型不匹配
于是这个时候就会在栈上拷贝一份临时的数据
所以说它在栈区中
故答案为
A A A A D A B
看看这张图 之后我们便开始做填空题
sizeof(num1) = ____; sizeof(char2) = ____; strlen(char2) = ____; sizeof(pChar3) = ____; strlen(pChar3) = ____; sizeof(ptr1) = ____;
这里唯一需要注意的一点是 字符串后面的‘/0’ 在计算大小是算作是一个大小 在计算长度时忽略不计
所以说sizeof(char2) = 5
其他的题目都很简单 这里直接给出答案
40 5 4 4 4 4 (32位系统)
这里提出一个问题 sizeof和strlen的区别是什么?
答:一个是计算大小占多少个字节 一个是计算字符串长度是多少
二. C语言中的动态内存管理方式
C语言中实现内存管理主要是通过下面这四个函数分别是
malloc
calloc
realloc
free
其中常见的面试题有
malloc calloc realloc之间的区别是什么 ?
这里可以直接参考萌新以前写的博客
C语言动态内存开辟
着重要注意下malloc和calloc的区别
一个是不初始化 一个是全部初始化为0
三. C++中动态内存管理方式
我们说C++是基于C语言的优化 所以说C语言中的动态内存管理方式在C++中也是可以使用的
但是呢在某些特殊场景下 C语言中的动态内存管理会不能操作 或者说操作起来很麻烦
所以说这个时候我们C++就诞生了新的内存管理方式
3.1 new和delete操作符操作内置类型
注意我的副标题写的是什么? 操作符
说明我们这里的两个关键字是操作符 而C语言中的malloc ralloc cealloc free都是函数 这里要注意
void Test() { // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请10个int类型的空间 int* ptr6 = new int[3]; delete ptr4; delete ptr5; delete[] ptr6; }
我们来看看debug过程
从下面的监视窗口同学们应该就能理解了初始化和new的基本使用了
我们这里继续走下去
delete之后发现全部被删除了
这里我们总结下
总结
对于申请和释放单个元素的空间 我们使用new和delete
如果想要初始化 我们在后面加上括号 括号里面写上我们要初始化的值就好
对于申请和释放连续的空间的时候我们使用 new[] 和delete[]
这里要注意的一点是 在C++11更新的标准中增加了对于连续空间的初始化方式
代码表示如下
int* ptr6 = new int[3]{1,2,3};
在C++11标准以后 我们也可以使用大括号对于连续的空间进行初始化
3.2 new和delete操作符操作自定义类型 (重点之一)
还是一样 我们先来看代码
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; struct ListNode { ListNode* _next; int _val; ListNode(int val) :_next(nullptr) ,_val(val) {} }; int main() { //自定义类型 //new和delete相比malloc/free,除了空间管理还会调用构造函数和析构函数 A* p1 = new A; A* p2 = (A*)malloc(sizeof(A)); delete p1; free(p2); ListNode* n1 = new ListNode(1); ListNode* n2 = new ListNode(2); ListNode* n3 = new ListNode(3); ListNode* n4 = new ListNode(4); n1->_next = n2; return 0; }
我们这里发现会自动调用构造函数和析构函数
这就是为什么C++中会使用new和delete操作符的原因之一
之后我们再来看看malloc出来的空间 能不能调用构造和析构
(其实这里已经能猜出来了 不能 )
这里什么都没有发生 符合预期
我们发现对于Listnode来说 也是new出来就初始化了
比起malloc来说方便不少
3.3 面向过程和面向对象的错误区别(重点之一)
面向过程的语言的错误是通过错误码识别的
面向对象的语言的错误是通过抛出异常识别的
还记得C语言中我们malloc失败是怎么报错的嘛?
代码表示如下
A* p2 = (A*)malloc(sizeof(A)); if (p2==NULL) { perror("malloc fail"); exit(-1); }
但是在C++中我们的报错是这样子的
(这里的语法记住就好 不要求现在掌握)
int main() { try { while (1) { new char[1024u * 1024u * 1024u]; cout << "yes" << endl; } cout << "test" << endl; } catch (exception& e) { cout << e.what() << endl; } return 0; }
这里我们可以发现两个点
1 new空间失败之后会直接抛出一个异常到catch 捕获异常
2 new空间失败后后面的语句不会执行
为了验证第二点我们可以这样试试
可以完美验证我们的思路
3.4 类型不匹配遇到的错误
如果我们使用了new 我们销毁要使用delete
如果我们使用了new[] 我们销毁要使用delete[]
如果我们使用了 malloc realloc calloc 我们要使用free
不能乱
横穿马路有风险 你这次侥幸没被创不代表你永远不会被创
四. operator new和operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是
系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过
operator delete全局函数来释放空间。
其实底层就是用malloc来实现的
这里简单画图方便大家理解下
源码表示如下 (这里看看就好)
/* 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 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)
五. new和delete的实现原理
5.1 内置类型
在实现内置类型开辟销毁空间的时候基本和malloc free相似
唯一不同的是如果开辟失败
new会抛出一个异常
malloc会返回一个空指针
5.2 自定义类型
new的原理
调用operator new函数申请空间
在申请的空间上执行构造函数,完成对象的构造
delete的原理
在空间上执行析构函数,完成对象中资源的清理工作
调用operator delete函数释放对象的空间
new T[N]的原理
调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对
象空间的申请
在申请的空间上执行N次构造函数
delete[]的原理
在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释
放空间
总结下
new是先开辟空间再调用构造函数
delete是先调用析构函数再销毁空间
六. 定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
它的使用格式如下
new (place_address) type(无参)
或者说
new (place_address) type(initializer-list)(有参)
我们来看看代码
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 = new A; A* p3 = (A*)malloc(sizeof(A)); if (p3 == nullptr) { perror("malloc fail"); exit(-1); } //new(p3)A(1); return 0; }
来看看debug窗口是什么样子的
这里的p1初始化了 但是p3没有初始化
但是我们应该怎么调用构造函数呢?
这里给出一个叫做定位new的解决方案
new(p3)A(1);
我们发现 这里确实初始化成功了
而析构函数的调用就简单多了 直接指针指向析构函数就可以
七. 常见面试题
7.1 malloc和new的区别 delete和free的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地
方是:
1.malloc和free是函数,new和delete是操作符
这个我们再前面就讲解过了
2.malloc申请的空间不会初始化,new可以初始化
这里就是为什么C++中要创造new 两大创造new的原因之一
3.malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
这个是语法上的特点
4.malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型也是语法上的特点
5.malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
这个应该是要记住的 两大创造new的原因之一
6.申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
这个是malloc和delete的自定义类型实现原理 我们在上面讲解过了
应该怎么理解并记忆呢?
首先是语法上的三个特点
int*p = (int *)malloc(sizeof(4))
int* p = new int(3);
观察这两段代码我们不难发现 2 4
之后如果开辟多个空间的话 我们不难发现 3
int* p = new int[5];
回答上来这三点之后我们就开始想 为什么C++要创建new
一个是初始化 一个是抛出异常
最后我们回顾下new实现自定义类型的原理就好
7.2 内存泄漏
什么是内存泄漏呢?
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害是什么呢?
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。
大家可以写出这段代码 并且打开资源管理器
我们可以发现 这个程序占用的内存越来越高了
int main() { while (1) { int* p = new int[1024]; cout << p << endl; } return 0; }
关于怎么预防内存泄漏其实都是我们后面要学的内容
老师上课的时候也没有重点讲解 所以说博主这里就不提及了 后面遇到再说
总结
本篇博客博主主要讲解了C++中的内存管理
由于博主水平有限 所以错误在所难免 希望大佬看到之后可以指正
如果这篇博客帮助到了你 别忘了一键三连啊
阿尼亚 哇酷哇酷!