👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
目录
一、C/C++内存分布
首先回顾,常见的数据是如何在内存中存储的:
接着可以来看看以下的题目:
【选择题】
选项:
A.栈(局部变量)
B.堆
C.静态区(全局变量和静态变量)
D.代码段(常量区)
1. a在哪里?
2. static_b在哪里?
3. static_c在哪里?
4. d在哪里?
5. arr1在哪里?
6. arr2 在哪里?
7. *arr2在哪里?
8. arr3在哪里?
9. *arr3在哪里?
10. ptr1在哪里?
11. *ptr1在哪里?
【答案 + 解析】
【填空题】
1. sizeof(arr1) = ____;
2. sizeof(arr2) = ____;
3. sizeof(arr3) = ____;
4. sizeof(ptr1) = ____;
5. strlen(arr2) = ____;
6. strlen(arr3) = ____;
【答案+ 解析】
【问答题】
sizeof
和strlen
区别?
strlen
函数用于计算字符串的长度,只统计字符串中字符的数量,不包括结尾的空字符。- 而
sizeof
操作符用于计算变量或类型的大小,一般单位为字节,通常用于计算内存大小。
二、C语言中动态内存管理
往期回顾:[C语言中动态内存管理]
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
int* p1 = (int*)malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
free(p3);
return 0;
}
- 问:这里需要
free(p2)
吗?答案当然是不需要的。
realloc
功能是在已有的内存空间扩容,然而扩容会存在原地扩容和异地扩容(当原有空间之后没有足够大的空间)。但是扩容后,返回的都是调整之后的内存的起始地址,因此只要释放了p3
,p2
自然而然就被释放掉了。
2.1 面试题
malloc/calloc/realloc
的区别?
malloc
和calloc
都是向内存申请空间,不同的是malloc
分配的内存区域中的初始值是随机的,而calloc
会将分配的内存区域中的初始值全部初始化为0realloc
可以扩大原有的内存空间,而扩容的方式有2种,一种是原地扩容(在已有的内存空间后扩容);还有一种是异地扩容(当原有空间之后没有足够大的空间),它的扩容方式是:它会在内存空间重新开辟一块空间,然后把原有的内存空间中的数据复制到新的空间,最后再释放原有的内存空间
malloc
的实现原理
- 维护一个空闲内存块链表,其中每个内存块都记录了其大小和是否已被分配。
- 当程序调用
malloc
函数请求分配一块内存时,遍历空闲内存块链表,找到第一个大小足够的内存块并将其从链表中移除。- 如果找到的内存块比请求的内存大,则将其分裂成两个部分,一个用于满足请求,另一个用于将来的分配请求。
- 如果没有找到足够大小的内存块,则向操作系统请求更多的内存。
- 将已分配的内存块的大小和状态记录下来,并将其返回给程序。
需要注意的是,malloc
的实现可能因操作系统和编译器而异,但其基本原理都是以上述步骤为基础的。同时,为了避免内存泄漏和内存碎片化等问题,malloc
通常还会实现一些额外的功能,如内存池、缓存、对齐等。
三、C++内存管理方式
由于C++是兼容
C
语言的,因此C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new
和delete
操作符进行动态内存管理。
3.1 new/delete操作内置类型
#include <iostream>
using namespace std;
int main()
{
// C语言版
int* p1 = (int*)malloc(sizeof(int));
free(p1);
// C++版 new + delete
// 用法: new + 类型
int* p2 = new int;
delete p2; // 释放
// 开辟40个字节,也就是10个整型
// C语言版
int* p3 = (int*)malloc(sizeof(int) * 10);
free(p3);
// C++版
int* p4 = new int[10];
delete[] p4;
return 0;
}
当然,C++
中的new
还能对空间进行初始化
#include <iostream>
using namespace std;
int main()
{
int* p4 = new int[10]{ 1, 2 ,3 };
for (int i = 0; i < 10; i++)
{
cout << p4[i] << ' ';
}
cout << endl;
delete[] p4;
int* p5 = new int(10);
cout << *p5 << endl;
delete p5;
return 0;
}
【程序结果】
p4
是申请10
个int
的数组,对于数组要用{}
初始化;p5
是申请1
个数组,初始化为10
,用()
初始化- 若不进行初始化,
new
和malloc
一样,开辟的空间都是 随机值
new
和delete
操作符需要 匹配 起来使用,不能与malloc
和free
混用
3.2 new和delete操作自定义类型
new/delete
和malloc/free
最大区别是:new/delete
对于自定义类型除了开空间和释放空间,还会调用构造函数(new
)和析构函数(delete
)。(这点非常重要!!!)
#include <iostream>
#include <stdlib.h>
using namespace std;
struct ListNode
{
public:
// 构造函数
ListNode(int x)
:_val(x)
, _next(nullptr)
{
cout << "调用了构造函数" << endl;
}
// 析构函数
~ListNode()
{
cout << "调用析构函数" << endl;
}
int _val;
struct ListNode* _next;
};
struct ListNode* BuyListNode(int x)
{
// 单纯开空间
// C语言版太太太长了
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
if (newnode == nullptr)
return nullptr;
newnode->_next = nullptr;
newnode->_val = x;
return newnode;
}
int main()
{
// C语言版
struct ListNode* n1 = BuyListNode(1);
struct ListNode* n2 = BuyListNode(2);
struct ListNode* n3 = BuyListNode(3);
// 开空间+调用构造函数初始化
ListNode* nn1 = new ListNode(1);
ListNode* nn2 = new ListNode(2);
ListNode* nn3 = new ListNode(3);
delete nn1;
delete nn2;
delete nn3;
return 0;
}
【调试结果 + 程序运行结果】
四、operator new与operator delete函数(重点)
4.1 介绍operator new与operator delete函数
new
和delete
是用户进行动态内存申请和释放的 操作符,operator new
和operator delete
是系统提供的 全局函数,new
在底层调用operator new
全局函数来申请空间,delete
在底层通过operator delete
全局函数来释放空间。注意:不是对new
和delete
重载。
以下是operator new
和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 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)
// 是个宏
通过以上两个全局函数,我们发现:
operator new
函数本质是封装了malloc
。operator delete
本质是封装了free
。
以下是operator new
和operator delete
的使用方法:
// operator new和operator new是库函数提供的
// 因此可以直接使用
// 用法和malloc、free是一样的
#include <iostream>
using namespace std;
int main()
{
int* p1 = (int*)operator new(sizeof(int));
operator delete(p1);
return 0;
}
通过以上代码我们发现:
operator new
和operator delete
的用法和功能都分别与malloc
、free
一样。它们都不会去调用构造函数和析构函数,不过还是有区别的:
operator new
不需要检查开辟空间的合法性(不需要特别判断p1
的返回值)。operator new
开辟空间失败就抛异常(对于面向对象语言处理失败,不喜欢用返回值,建议用抛异常)。
4.2 new和delete的底层原理
new
的底层原理就是转换成调用operator new + 构造函数
,我们可以通过查看反汇编来验证:
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 0)
:_a(x)
{
cout << "A(int x = 0)" << endl;
}
private:
int _a;
};
int main()
{
A* a1 = new A;
delete a1;
return 0;
}
【反汇编】
- 同样的,
delete
的底层原理:转换成调用operator delete + 析构函数
【反汇编】
因此,我们就可以得到new、operator new、malloc
和delete、operator delete、free
之间的关系:
从这也说明new
和delete
以及malloc
和free
一定要配对使用
五、new和delete的实现原理
5.1 内置类型
如果申请的是内置类型的空间,
new
和malloc
,delete
和free
基本类似,不同的地方是:new/delete
申请和释放的是单个元素的空间,new[]
和delete[]
申请的是连续空间,而且new
在申请空间失败时会抛异常,malloc
会返回NULL
。
5.2 自定义类型
new
的原理
调用
operator new
函数申请空间 + 调用构造函数。
delete
的原理
执行析构函数,完成对象中资源的清理工作 + 调用
operator delete
函数释放对象。
new 类型[数组元素个数]
的原理
- 调用
operator new[]
函数,在operator new[]
中实际调用operator new
函数完成N
个对象空间的申请- 在申请的空间上执行
N
次构造函数
delete[]
的原理
- 在释放的对象空间上执行
N
次析构函数,完成N
个对象中资源的清理- 调用
operator delete[]
释放空间,实际在operator delete[]
中调用operator delete
来释放空间
六、定位new表达式(placement-new) (了解)
- 用途
定位
new
表达式是 在已分配的原始内存空间中调用构造函数初始化一个对象。就比如malloc
,对于自定义类型,它不会去调用构造函数,但是使用定位new
可以显示调用
- 使用格式
new(指针) + 类型
new(指针) type(类型的初始化列表)
class A
{
public:
// 构造函数
A(int a = 0)
: _a(a)
{
std::cout << "construct" << std::endl;
}
// 析构函数
~A()
{
std::cout << "clear" << std::endl;
}
private:
int _a;
};
int main()
{
// p1指向一块内存空间,但空间未被初始化
A *p1 = (A *)malloc(sizeof(A));
// 定位new表达式:new(指针) + 类型
// 注意:如果A类的不是默认构造函数有参数时,此处需要传参
new (p1) A;
// malloc不会自动调用析构函数
// 调用析构函数
p1->~A();
// p1是malloc出来的
// 因此要用free来释放
free(p1);
return 0;
}
- 常见使用场景
定位
new
表达式在实际中一般是 配合内存池使用(内存池知识点)。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new
的定义表达式进行显示调构造函数进行初始化。
七、常见面试题
7.1 malloc/free和new/delete的区别
malloc/free
和new/delete
的 共同点 是:都是从堆上申请空间,并且需要用户手动释放- 不同点是:
①malloc
和free
是 函数,new
和delete
是 操作符
②malloc
申请的空间 不会初始化,new
可以 初始化
③malloc
申请空间时,需要手动计算空间大小并传递,new
只需在其后跟上空间的类型即可,如果是多个对象,[]
中指定对象个数即可
④malloc
的返回值为void*
, 使用时必须强转,new
不需要,因为new
后跟的是空间的类型
⑤malloc
申请空间失败时,返回的是NULL
,因此使用时必须判空,而new
不需要,但是new
需要 捕获异常
⑥申请 自定义类型对象 时,malloc/free
只会开辟空间,不会调用构造函数与析构函数,而new
在 申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
7.2 内存泄漏
7.2.1 什么是内存泄漏
内存泄漏指:因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
7.2.2 内存泄漏的危害
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
7.2.3 内存泄漏分类(了解)
C/C++
程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(
Heap leak
)
堆内存指的是程序执行中依据须要分配通过
malloc/calloc/realloc/new
等从堆中分配的一块内存,用完后必须通过调用相应的free
或者delete
删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏
- 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
7.2.4 如何避免内存泄漏
- 事前预防型。如 智能指针 等。
- 事后查错型。如泄漏检测工具。
1. linux下内存泄漏检测工具
3. 在windows下使用第三方工具:VLD工具说明
3. 其他工具:内存泄漏工具比较