前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【C++】动态内存管理:织梦寻优,在代码世界中编织高效内存的诗篇

【C++】动态内存管理:织梦寻优,在代码世界中编织高效内存的诗篇

作者头像
TANGLONG
发布2025-02-24 08:38:05
发布2025-02-24 08:38:05
6800
代码可运行
举报
运行总次数:0
代码可运行
在这里插入图片描述
在这里插入图片描述

一、复习C/C++内存分布

    在之前C语言的文章中我们详细讲解了C语言的动态内存管理,其中也简单学习了C/C++的内存分布,接下来我们就来通过一些练习来复习一下,C语言动态内存管理文章:【C语言】动态内存管理及相关笔试题

    接下来我们先来看看之前学过的内存分布图,然后再来做题:

    上面就是我们C/C++内存分布的图片了,在给出题目之前我还是提一下,其实这里我们所指的内存其实是语言层的理解,它其实是虚拟地址,而非实际的物理地址,是不是比较吃惊,到时候在学习Linux的时候我会给大家慢慢讲解,接下来我们继续复习,给出题目,如下:

代码语言:javascript
代码运行次数:0
复制
//代码
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);
	free(ptr1);
}
代码语言:javascript
代码运行次数:0
复制
//选择题
选项 : A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)

1. globalVar在哪里?____

2. staticGlobalVar在哪里?____

3. staticVar在哪里?____

4. localVar在哪里?____

5. num1在哪里?____

6. char2在哪里?____

7. *char2在哪里?___

8. pChar3在哪里?____

9. *pChar3在哪里?____

10. ptr1在哪里?____

11. *ptr1在哪里?____

    在上面我们给出了一段代码,里面包含了各种变量,接下来我们就一 一来判断它们属于虚拟内存中的哪个区域:

    1. 首先是第1问,问变量 globalVar 在哪个区域,首先我们要看出它是局部变量还是全局变量,很明显它只是一个普通的全局变量,所以它存放在静态区,选C     2. 接下来是第2问,问变量 staticGlobalVar,它是一个静态的全局变量,我们之前讲过,全局变量和静态变量都存放在静态区,这里的staticGlobalVar是他们的结合体,所以自然就是静态区,选C     3. 然后是第3问,问变量 staticVar 存放在哪里,根据上面的代码我们不难看出,staticVar是一个局部的静态变量,它的作用域是局部的,但是声明周期是全局,存放在静态区,还是选C     4. 接下来是第4问,问变量 localVar 在哪里,很明显它是一个局部变量,并且没有什么特别之处,所以它存放在Test函数的栈帧中,自然就在栈区了,选A     5. 接下来是第5问,问 num1 在哪里,和上面localVar不同的是,num1是一个数组,也存放在Test的栈帧中,属于栈区,选A     6. 接下来是第6问,从这里开始慢慢就有难度了,问 char2 在哪里,注意不要被迷惑了,char2是一个局部的数组,跟上面的num1数组没有什么不同,也属于栈区,选A     7. 接下来是第7问,问 * char2在哪里,我们都知道数组名其实就是首元素的地址,那么 *char2 就是数组的首元素了,也就是字符 ’ a ',整个数组都在栈区中。字符 ’ a ’ 是它的一部分,也属于栈区,选A     8. 接下来是第8问,这个题和下一题就有点小坑了,要注意解答, 它问 pChar3 在哪里,千万要记住,pChar3只是Test函数中的一个局部指针变量,应该属于栈区,选A,很多人把它误以为成常量区     9. 接下来是第9问,这个题问 *pChar3 在哪里,pChar3是一个局部指针变量,存放在栈区,而它指向的内容是一个常量字符串,所以对 pChar3 解引用拿到的是常量区的内容,属于常量区,选D     10. 接下来是第10问,这个题问 ptr1 在哪里,跟上面两个题很类似,这里的ptr1只是一个局部的指针变量。所以属于栈区,选A     11. 最后是第11问,这个题问 *ptr1 在哪里,虽然ptr1只是一个局部的指针变量,属于栈区,但是它指向的内容属于堆区,解引用后拿到的就是堆区的数据,所以选B

    那么上面就是对前面11道题的文字分析了,你答对了几道题呢?接下来我们再结合下面的图片复习一下:

    相信大家已经复习好了C/C++的内存分布了,接下来我们来简单复习一下C语言的动态内存管理

二、简单复习C语言动态内存管理

C语言的动态内存管理通常是通过几个函数来实现,我们这里将它们简单介绍一下就好了:     1. malloc:它的参数是我们要在堆上开辟的空间的大小,单位是字节,它返回一个void*的指针,需要我们接收时强转为对应的指针类型,如果开辟失败会返回空指针     2. calloc:它的参数与malloc不同,第一个参数是要开辟多少个变量,第二个参数是一个变量的大小,单位是字节,其次它会把所有变量都初始化为0,其它与malloc没什么区别     3. realloc:当我们使用malloc或者calloc开辟空间后,发现开辟的空间不够用了,于是我们可以使用realloc进行扩容,有可能是在之前空间的后面扩容,但是如果之前空间的后面不够用,realloc就会另外开辟一块空间,将之前的数据拷贝过来,然后释放掉之前的空间,它的第一个参数是要扩容的指针,第二个参数是扩容后的大小,单位是字节,如果扩容失败会返回空指针,否则返回扩容的空间的首地址,要注意的是如果第一个参数是空指针,那么此时它的作用和malloc一致     4. free:使用上面三个函数动态开辟的内存不会自动释放,需要我们手动进行释放,否则就会造成内存泄漏,也就是当前程序不使用这段空间了,但是又没有释放,就导致了内存的浪费,称为内存泄漏,解决办法就是使用free函数对空间进行释放

    上面就是对C语言中动态内存管理的简单复习,接下来我们才进入今天的重点:C++中的动态内存管理是如何使用的

三、C++动态内存管理

    C++中的动态内存管理仍然可以使用C语言的那几个函数,但是在某些场景有局限性,我们可以使用C++自己的内存管理方式:通过new和delete操作符进行动态内存管理

new与new[]

    我们先来讲解new怎么使用,我们将内置类型和自定义类型进行分类讨论,首先是内置类型,如下:

代码语言:javascript
代码运行次数:0
复制
//不初始化
int* p1 = new int;
//初始化
int* p2 = new int(1);

    我们可以看到new用起来非常方便,不用计算要开辟的空间的大小,也不用我们强转类型,只需要写出对应的类型,并且还能够进行随意的初始化,非常方便,当然,如果我们想开辟一个数组,我们就可以这样用,如下:

代码语言:javascript
代码运行次数:0
复制
//不初始化
int*  arr1 = new int[3];
//初始化(C++11支持)
int* arr2 = new int[3]{1, 2, 3};

    那么上面就是使用new开辟内置类型空间的使用方法,如果是单个变量就使用new,如果是数组就使用new[],接下来我们开始介绍new如何给自定义类型的对象开辟空间,如下:

代码语言:javascript
代码运行次数:0
复制
class Date
{
public:
	Date(int year = 2025, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//不初始化(会调用默认构造,没有默认构造会报错)
	Date* d1 = new Date;
	//利用构造进行初始化
	Date* d2 = new Date(2025, 2, 20);
	d1->Print();
	d2->Print();
	return 0;
}

    可以看到使用new来为自定义类型开辟空间非常好用,如果我们不传参初始化也会给我们调用默认构造,如果我们想传参初始化同样也可以调用构造,非常重要,而C语言的内存管理函数就做不到这一点,接下来我们看看程序的运行结果:

    可以看到确实如我们预期所料,没有问题,至于我们想申请自定义类型数组,还是使用new[],也会自动调用默认构造,或者使用大括号显示传参,如下:

代码语言:javascript
代码运行次数:0
复制
int main()
{
	//不初始化(自动调用默认构造,没有默认构造就报错)
	Date* arr = new Date[2];
	//利用构造初始化
	Date* arr = new Date[2]{ {2025, 1, 2}, {2024, 2, 3} };
	return 0;
}

delete与delete[]

    当我们使用完动态开辟的内存后,我们需要将它释放掉,C++提供了操作符delete让我们释放空间,首先是释放单个内置类型,如下:

代码语言:javascript
代码运行次数:0
复制
//开辟
int* p1 = new int(1);
//释放
delete p1;

    使用方法如上,非常简单,接着要注意的是,上面的方法仅适用于开辟了一个对象,如果是数组需要使用delete[]进行释放,如下:

代码语言:javascript
代码运行次数:0
复制
//开辟
int* arr = new int[10];
//释放
delete[] arr;

    上面就是使用delete和delete[]释放内置类型的教程,接下来我们学习使用delete和delete[]释放自定义类型,其实使用方法是一模一样的,只是要特殊说明的是,delete和delete[]释放自定义类型的空间时会调用这个自定义类型的析构,然后再释放空间,我们写个程序验证一下:

代码语言:javascript
代码运行次数:0
复制
class A
{
public:
	A(int a = 10)
		:_a(a)
	{
		cout << "调用了构造函数A()" << endl;
	}

	~A()
	{
		cout << "调用了析构函数~A()" << endl;
	}

private:
	int _a;
};

int main()
{
	A* arr = new A[10];
	cout << endl;
	delete[] arr;
	return 0;
}

    上面我们开辟了一个数组,数组的每个元素都是一个A类的对象,按照我们之前讲的,这里为了初始化这些对象会调用10次构造函数,然后我们最后释放arr的时候,会调用10次析构,我们来运行一下程序看看是不是这样的,如下:

    可以看到确实如我们预期所料,这里调用了10次构造和析构,但是有的同学可能就有疑问了,delete就已经释放空间了,为什么还要继续调用析构呢?在上面的日期类和A类中可能还看不出来,接下来我们举个更加复杂一点的例子解析,如下:

代码语言:javascript
代码运行次数:0
复制
class stack
{
public:
	stack(int n = 10)
		:_size(0)
		,_capacity(10)
	{
		_arr = new int[10];
	}

	~stack()
	{
		if(_arr)
			delete[] _arr;
		_size = _capacity = 0;
	}

private:
	int* _arr;
	int _size;
	int _capacity;
};

int main()
{
	stack* pst = new stack;
	delete pst;
	return 0;
}

    在上面我们简单写了一个栈的构造与析构,并且使用new去申请了一个对象,随后直接将它释放掉,我们主要关注的是为什么delete和delete[]释放空间前要调用析构函数,我们画一个示意图,如下:

    当我们开辟好空间后,pst指向了一个堆上的stack对象,然后stack中的_arr成员变量就指向了一个堆上的整型数组,我们来看看delete不自动调用stack的析构而直接释放stack对象会怎么样,如图:

    可以看到这个时候程序就会出问题,因为我们只释放了stack对象的空间,_arr指向堆上的整型数组,这个数组还没有被释放,会引发内存泄漏,所以delete和delete[]释放对象时,必须先调用它的析构,然后再释放这个对象,如下图:

    这下我们就真正理解到为什么释放自定义类型的对象时,要先调用它的析构,然后再释放这个对象了,因为这个对象的成员可能指向堆上的空间,所以要先调用析构释放这个成员指向的堆的空间,再释放掉整个对象

四、operator new与operator delete(重点)

    new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间

operator new

    new在底层调用了函数operator new来申请空间,那么operator new是如何实现的呢?我们来看看源码中是如何写的,如下:

代码语言:javascript
代码运行次数:0
复制
/*
 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就好了,为什么还要搞出来一个operator new,这是因为malloc没有成功申请到空间返回空指针,不太符合面向对象语言的方式,所以重新写一个函数operator new

    这个函数封装了原本的malloc,封装的目的是为了更好地以面向对象的方式解决问题,operator new会判断malloc的返回值,如果不为空就直接返回了,如果为空进入判断就抛出异常,异常我们在后面的部分会讲到,是C++解决错误的方式,而不是依靠返回值,由于涉及到继承等其它知识,这里就简单说一下就好了

    总之只需要知道为什么我们不直接使用malloc,而是将malloc封装成为operator new,目的就是为了让申请空间失败后抛出异常,从而让我们能够捕获

operator delete

    那么我们知道了operator new的底层是malloc,那么有没有可能operator delete的底层是free呢?我们来看看源码,如下:

代码语言:javascript
代码运行次数:0
复制
 /*
 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;
 }

    可以看到operator delete确实封装了free,delete就用它来释放空间,通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的

    operator new和operator delete这两个小点还是很重要的,涉及到了new和delete的底层,有时还会出现在面试中,当然,有了这些前置知识,我们就可以更好的学习后面的new和delete的原理了

五、new与delete原理

内置类型

    如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回空指针

自定义类型

new的原理

    1. 调用operator new函数申请空间(operator new的底层封装了malloc,根据malloc的返回值来决定是否抛出异常)     2. 在申请的空间上执行构造函数,如果没有传参那么调用默认构造,如果传参了就按照参数完成构造,最终完成对象的构造

delete的原理
  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间(operaotr delete底层封装了free)
new T[N]的原理

    1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请     2. 在申请的空间上执行N次构造函数

delete[]的原理

    1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理     2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释 放空间

六、C++与C语言动态管理区别总结

    1. malloc、calloc、realloc以及free是函数,new和delete是操作符     2. malloc需要计算开辟空间的大小,并且需要强转类型,new无需计算空间的大小,只需要写出对应的类型     3. new底层封装了operator new,operator new底层又封装了malloc,delete底层封装了operator delete,operator delete又封装了free     4. new会调用operator new来开辟空间,对于内置类型可以初始化,对于自定义类型可以调用对应的构造函数来构造对象,而malloc没有这些功能     5. delete在释放自定义类型的对象时,会调用对象的析构函数,再调用operator delete来释放空间     6. C语言判断是否出错依赖malloc的返回值,而new则是抛异常

    那么今天C++的动态内存管理部分就到这里啦,希望大家有所收获,同时下一篇文章我们就会了解到泛型编程了,也就是模板,敬请期待吧!     bye~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、复习C/C++内存分布
  • 二、简单复习C语言动态内存管理
  • 三、C++动态内存管理
    • new与new[]
    • delete与delete[]
  • 四、operator new与operator delete(重点)
    • operator new
    • operator delete
  • 五、new与delete原理
    • 内置类型
    • 自定义类型
      • new的原理
      • delete的原理
      • new T[N]的原理
      • delete[]的原理
  • 六、C++与C语言动态管理区别总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档