去年的今日,博主第一次发文,那时初出茅庐,没什么经验。时隔一年,更加优质的博文献上,希望可以帮助到更多的人❤️❤️❤️
Hello,大家好,本文要为大家带来的是C/C++中的内存管理,也将为您解答什么是【栈区】、【堆区】、【静态区】等等,更好地认识数据在内存中的分布到底是怎样的
首先我们要先来了解一下内存中的五大区域划分,总共是有【栈区】、【堆区】、【共享段库】、【静态区/数据段】、【代码段】这些
虚拟进程地址空间
的最上层是个高地址,它是给Linux的内核空间(Kernal)使用的,接下去的 栈区 建立出栈帧存储局部数据,即用即销毁,例如我们在构造二叉树的时候递归调用完当前父节点的左子树时,其右子树其实使用的也是使用的同一块空间malloc
去堆区中申请空间,本文我们会大量地讲到有关动态内存的申请这一块的内容接下去我们来看下面的一段代码和相关问题
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);
}
globalVar
、staticGlobalVar
、staticVar
都是存放在数据段(静态区)的,其生命周期是从程序开始到结束为止*char2
来说,很多同学就会认为它是在【常量区】中的,还记得我们在C语言的数组章节所谈到字符数组吗,其数组名为首元素地址,那我们对首元素地址去进行解引用的话就拿到了首字符的地址,那么这只是一个字符而已,并不是一个字符串,所以是存放在【栈区】中的*pChar3
呢,很明显它是pChar3
是一个指针,其指向的是【常量区】中的一个常量字符串,此时对这个指针去进行解引用也就找到了这个字符串,那么*pChar3
即存放在【常量区】中*ptr1
,它指向的是堆区中的一块空间,*
解引用即存放在【堆区】中sizeof(num)
即为40char2
这个字符数组里面存放着一个字符串,那使用【sizeof()】去进行求解的话会去统计加上\0
之后一共有多少个字符,那很明显就是5。【strlen()】的话是请求从字符串首到\0
为止的字符个数,不计算\0
,那么就一共有4个字符sizeof(pChar3)
,要知道它可是个指针,那对于指针来说均为 4/8 取决于当前的运行环境是32位还是64位的,那么strlen(pChar3)
即是在求解这个字符串的长度,即为4sizeof(ptr1)
,它也是一个指针,所以大小为 4/8 个字节sizeof() 是操作符,不是函数,它是用来计算对象或者类型创建的对象所占内存空间的大小
strlen() 是函数,它是用来求字符串长度的,计算的是字符串之前 ‘\0’ 出现的字符个数,如果没有看到 ‘\0’ 会继续往后找
看完了上面的这些题后,我们再来在通过画图来进行一个对照,就可以看得非常清晰了
这一块读者可以直接看此篇文章 C生万物 | 细说动态内存管理,此处不再做赘述
【面试题】
malloc
用于分配指定大小的未初始化内存块,其不会对申请出来的内存块做初始化工作calloc
用于分配指定数量和大小的连续内存块,并将其初始化为0realloc
用于重新分配内存块的大小,并尽可能保留原有数据。其有两种扩容机制,分别为【本地扩容】和【异地扩容】,具体的扩容机制细述可以到上面的文章中进行查看C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过
new
和delete
操作符进行动态内存管理。
new
这个关键字来动态申请空间// 动态申请一个int类型的空间
int* p1 = new int;
// 动态申请一个int类型的空间并初始化为10
int* p2 = new int(10);
// 动态申请10个int类型的空间
int* p3 = new int[10];
free
,但是在C++中呢,我们使用delete
,对于普通的空间我们直接delete即可,但是对于数组来说,我们要使用delete[]
,这点要牢记了delete p1;
delete p2;
delete[] p3;
malloc
在开辟出空间的时候无法去做到初始化,那C++中的new
呢,可以吗?通过调试我们可以观察到除了p2
所指向的那块空间初始化了,其余都没有,那就可以说明它是可以去一个初始化工作的此时我们就要来所说C++在通过
new
开辟出一块空间的时候,如何去做一个初始化的工作
new 数据类型(初始化数值)
的方式即可;而对于像数组这样的空间,我们要使用new int[5]{初始化数值}
的形式去进行,此时才可以做到一个初始化int* p2 = new int(10);
int* p3 = new int[5]{ 1,2,3,4,5 };
看完了使用
new/delete
如何去操作C++中的【内置类型】,接下去我们来看看我们要如何去操作一个自定义类型
BuyListNode()
函数,很是麻烦struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* BuyListNode(int x)
{
struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
if (NULL == node)
{
perror("fail malloc");
exit(-1);
}
node->val = x;
node->next = NULL;
return node;
}
struct ListNode* n1 = BuyListNode(1);
struct ListNode* n2 = BuyListNode(2);
struct ListNode* n3 = BuyListNode(3);
struct ListNode {
int val;
struct ListNode* next;
ListNode(int x)
: val(x)
, next(NULL)
{}
};
ListNode* n4 = new ListNode(1);
ListNode* n5 = new ListNode(2);
ListNode* n6 = new ListNode(3);
n1
、n2
、n3
开出了空间并进行了一个初始化的工作所以经过上面的观察我们可以知道在C++中使用
new
是会去自动调用构造函数并完成初始化的
delete
而言,就会去调用这个析构函数,我们通过调试再来看看那如果我们操作的是多个对象呢,会去调用几次【构造】和【析构】?
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
{}
,然后在里面给到初始化的值即可A* p3 = new A[4]{ 1,2,3,4 };
A* p3 = new A[4]{A(1), A(2), A(3), A(4)};
最后,还有一点要切记,malloc
出来的一定要用free
,而new
出来的一定要用delete
,千万不可混用了!!!
好,最后我们来小结一下上面的内容
new/malloc
除了用法上面,没有什么本质区别new/malloc
除了用法上面,还有一个重大的区别,即new/delete
会去调用构造函数并初始化,析构函数清理malloc
出来的就要用free
释放,new
出来的就要用delete
释放,不要混淆了上面我们讲到了,
new
和delete
是用户进行动态内存申请和释放的操作符,而本小节我们则要来讲有关operator new
和operator delete
这两个系统提供的全局函数
new
去开空间并进行初始化,那在编译器底层究竟是如何去实现这一块逻辑的呢?这我们需要通过汇编来进行查看A* a1 = new A(1);
delete a1;
new在底层调用operator new全局函数来申请空间
call
指令的调用,分别是调用【operator new】从堆区去开空间和调用【A::A】这个构造函数去进行初始化工作delete在底层通过operator delete全局函数来释放空间
call
指令的调用,分别是调用【A::~A】去析构函数释放资源和调用【operator delete】这个函数去释放从堆区申请的空间。不过呢,它们这两个部分被编译器做了一个封装,在外层我们还需用通过一个call
指令和jmp
指令去做一个跳转,才能看到底层的这块实现那有些同学一定会很好奇这个【operator new】和【operator delete】到底是个什么东西,现在我们就来讲讲这两个全局函数
operator new
,通过查看它的源代码我们可以发现其内部还是使用【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_dbg()
这个函数,它其实就是我们在C语言中所写的free()
函数,那么就可以得出其实这个函数底层也和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;
}
那既然这两个全局函数的底层实现用的都是【malloc】和【free】的话,是不是我们在使用的时候就可以直接用operator new
和operator delete
来进行替代呢?
operator new
和operator delete
,然后再去运行看看,会发生什么?int main(void)
{
int* p1 = (int*)operator new(sizeof(int));
int* p2 = new int;
operator delete(p1);
delete p2;
A* a1 = (A*)operator new(sizeof(A));
A* a2 = new A(1);
operator delete(a1);
delete a2;
return 0;
}
operator new
和operator delete
的效果就等同于【malloc】和【free】,因为从上面的源码我们观察到了其内部是包含了这两个内存函数的在上一小节中,我们学习到了两个全局函数, 分别是【operator new】和【operator delete】,通过分析可以得出它们的底层都是基于【malloc】和【free】来进行实现的。本小结呢,我们继续回归C++中的
new
和delete
,来讲它们的底层实现原理
1024 * 1024
个字节的空间,我们之前在使用【malloc】的时候一般都都会去检查一下,因为VS2019的编译器这块检查得过于严格了,其实对于【malloc】来说一般是不会申请失败的,但是对于下面这种,却会出现类似的问题,我们一起来瞧瞧int main(void)
{
int* p1 = nullptr;
do
{
p1 = (int*)malloc(1024 * 1024);
cout << p1 << endl;
} while (p1);
return 0;
}
p1
为空了,那也就说明空间申请失败了Vistual Studio Debugger
来进行查看,一般我们启动程序后,这个进程就会开始跑了,此时我们可以观察到这个内存的占比是很快地飚了上去,但是在1900M左右就停了下来,为什么呢?本身进程的地址空间就只有4个G,那在这里我估计分配给VS的就只有2个G,但是呢又不是实打实的2个G,所以呢将内存申请完了之后就返回了NULL[抛异常]
的形式来返回失败的结果,那具体怎么抛呢,我们先将上述的代码改成C++的形式int main(void)
{
int* p1 = nullptr;
do
{
p1 = new int[1024 * 1024];
cout << p1 << endl;
} while (p1);
return 0;
}
p1
并没有变为0x0000000
,而是在引发了一个异常,这就是C++对于某些问题喜欢用的方式catch
部分的内容,通过w.what()
输出打印出了【bad allocation】这个问题,意思就是申请失败被错误分配上面主要是做一个铺垫,带读者进一步地了解C++程序如何通过
new
与内存打交到,接下去呢我们来讲讲更加复杂一些的自定义类型
首先来看看【new】和【delete】的真正执行原理吧,学习了operator new
和operator delete
之后相信你对这些一定会产生共鸣
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main(void)
{
// 需要申请一个堆上的栈对象
Stack* s1 = new Stack();
delete s1;
return 0;
}
p1
,接着我们使用到了new
在堆区中为其开辟出了一块空间来存放这个对象中的成员变量,但是呢,这个对象中有一个array
指针,它也需要一块空间来存放,于是我们又在堆区中开辟出了一块空间初始化了这个_array
指针,让其也指向堆区中的一块空间[new]
的原理再来分析一遍:首先需要为这个栈对象在【堆区】开辟出一块空间,这件事情就需要交给operator new
来做 ,当空间开好之后,我们知道还会去调用构造函数
来完成一个初始化的工作,对于内置类型的话不做处理,但是对于自定义类型的话会去调用它的默认构造函数,不过我们这里写了构造函数的话就会去调用我们写过的,将其他两个内置类型也去做一个初始化new
之后,我们再来讲讲delete
,那我么直接通过原理来进行描述,在这里我们首先要去做的就是调用【析构函数】,那有同学问:为何没有像new
那样先去释放空间呢,而是先去调用了析构函数?_array
就变成了一个野指针,此时若再去调用【析构函数】的话就会出现大问题,所以说我们要先去调用析构函数释放掉_array
所指向的这块堆区中的空间,然后再使用operator delete
去释放掉这块空间,这即是[delete]
的调用原理看完了【new】和【delete】的调用原理之后,我们再来看看【new T[N]】和【delete[]】的原理
其实原理是差不多的,只不过在上面是对单个对象进行操作,这里是对多个对象进行操作,读者可以试着自己去模拟一下,理解理解,这里便不做过多展开
讲了这么多,本小节我们讲点拓展知识,这一块可能比较难一些,不做要求
【概念】:
【使用格式】:
new (place_address) type
new (place_address) type(initializer-list)
place_address
必须是一个指针,initializer-list
是类型的初始化列表
【使用场景】:
话不多说,概念了解后我们来举个例子说明一下
p1
现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行,那我们若要去调用这个类A的构造函数的话,就可以使用这个【定位new表达式】了,按照上面所给的使用格式来即可A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
free()
释放空间即可,严格遵循上面new
与delete
的调用原理p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
首先我们要了解一下什么是池化技术,它是一种常见的优化技术,用于提高资源利用效率和性能。其主要分为以下几种
好,通过上面这么一个案例,相信你对于古人的智慧一定是非常崇敬,接下去呢我们再来举一个例子
通过上述的这两个案例,我们很好地认识到了通过某个第三方工具去帮助我们完成一些事情,可以做到高效地完成某事,现在就让我们来谈谈【内存池】这个东西
malloc
或者是free
的话,就会产生很多的内存碎片,虽然它们所占的空间并不小,但是随着这些碎片的慢慢增多,就会导致内存渐渐拥挤;对于new
和delete
也是同理,因此我们必须想出一些办法,可以不用那么频繁地去堆中多次地申请空间new
去申请空间,否则的话就是直接到堆上去申请了,而是通过内存池提供给我们的一个单独的接口,我们只需要通过调用这个接口,使用【定位new表达式】申请到里面的空间,然后再调用构造函数进行初始化即可对于上面的池化技术,就讲这么多,因为用的场景不多,只是有时候C++为了追求极致的性能而所需要使用的一些手段
construct()
的内部就是去调用【定位new】,而destroy()
就是去显示地调用析构函数,从中涉及一些C++的模版相关知识,我们在下一文就会介绍【共同点】:都是从堆上申请空间,并且需要用户手动释放
【不同点】:
从特性 + 语法来看
1️⃣ malloc和free是函数,new和delete是操作符
2️⃣ malloc申请的空间不会初始化,new可以初始化
3️⃣ malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
4️⃣ malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
5️⃣ malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
从底层来看
申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
接下去,我们再来讲讲与本文关联性比较大的一些面试题,例如:内存泄漏
什么是内存泄漏:
内存泄漏的危害:
举个例子,就像是下面这样,我们在使用malloc
或者new
之后忘记去做一个释放了,此时就一定会出现内存泄漏的问题
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
再举个例子,可以看到下面这里确实是在new
之后进行了delete
,但是呢中间有个Func()函数却出现了异常,便会导致程序不会再执行下去了,那么也就造成了内存的一个泄漏问题
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
C/C++程序中一般我们关心两种方面的内存泄漏:
malloc / calloc / realloc / new
等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。在VS下,可以使用windows操作系统提供的
_CrtDumpMemoryLeaks()
函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息
int main()
{
int* p = new int[10];
// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
_CrtDumpMemoryLeaks();
return 0;
}
// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
C++中关于堆和栈的说法,哪个是错误的:( C )
A.堆的大小仅受操作系统的限制,栈的大小一般较小
B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题
C.堆和栈都可以静态分配
D.堆和栈都可以动态分配
【解析】:
A. 堆大小受限于操作系统,而栈空间一般由系统直接分配
B. 频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题
C. 堆无法静态分配,只能动态分配(malloc / new)
D. 栈可以通过函数 _alloca 进行动态分配,不过注意,所分配空间不能通过free或delete进行释放
使用 char* p = new char[100]
申请一段内存,然后使用delete p
释放,有什么问题?( B )
A.会有内存泄露
B.不会有内存泄露,但不建议用
C.编译就会报错,必须使用delete []p
D.编译没问题,运行会直接崩溃
【解析】:
A. 因为delete内部封装了free,所以对于内置类型而言,可以做到精确释放,不会造成内存泄漏
B. 正确。不会造成内存泄漏,应该用delete[]
C. 编译不会报错,建议针对数组释放使用delete[], 如果是自定义类型,不使用方括号就会运行时错误
D. 对于内置类型,程序不会崩溃,但不建议这样使用
最后,我们来总结一下本文所学习的内容
malloc
、calloc
、realloc
、free
这些内存函数,也当时做了一个回顾。看完它们之后我们就开始介绍C++中是如何实现动态内存管理,使用到的关键字为new/delete
,其不仅可以去操作内置类型,也可以去操作自定义类型,其会去调用构造函数并初始化,调用析构函数清理空间new/delete
之后,我们便开始拓展学习了两个全局函数,分别是operator new
和operator delete
,通过汇编的查看发现了new/delete
在底层就会去调用二者,透过观察源码,了解到了原来其内部还调用了[malloc]
和[free]
这两个内存函数,这似乎增长了我们了我们的知识面以上就是本文要介绍的所有内容,感谢您的阅读