前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【细品C++】C++动态内存管理

【细品C++】C++动态内存管理

作者头像
Crrrush
发布2023-06-23 14:40:45
1540
发布2023-06-23 14:40:45
举报
文章被收录于专栏:后端开发练级指南

写在前面

本篇文章将为你讲解C++动态内存管理,也就是new系列套件,但是由于C++兼容C语言,所以我会提及C语言的动态内存管理方式,也就是malloc系列套件。如果你学过C语言并且对C语言动态内存管理方式有一定的了解,那么本文的对比讲解也许能对你的理解有所帮助,那如果你没有接触过C语言可以选择性的观看本文章的内容。

虚拟内存

在聊内存管理之前,我们先来简单了解一下虚拟内存。虚拟内存是一个抽象概念,它为每个进程提供独占主存的假象。为什么要提供这个假象呢?下面我在这简单解释一下,毕竟这是属于操作系统的知识,这里只需要简单理解一下能帮助我们理解就行。

进程是跑在操作系统之上的,而操作系统为更好地封装以保护自身的安全,不提供真正地物理内存而给进程提供这个假象,让程序使用这套虚拟内存间接的与计算机沟通。也就是说,一份代码为了能与计算机沟通,必须遵守虚拟内存的规则,那么程序编译的时候就也需要遵守这套虚拟内存的规则。

这是从整体的角度看待,下面从语言的角度看内存区域划分也就是虚拟进程地址空间。

虚拟进程地址空间

分布

在本篇文章中,我们只需要关注这几个区域:

  • 代码段:存放可执行的代码和只读常量。
  • 数据段:存放全局数据、静态数据。
  • 堆:程序运行时创建,用于程序运行时申请动态内存,堆是可以向上增长的(堆区空间不够分配时)。在堆区的数据存储空间是由用户自主申请,自主释放的。
  • 栈:程序运行时创建,又叫堆栈,存放非静态局部变量、函数参数、返回值等。栈区的数据存储空间由系统自动分配,自动释放(如直接定义的局部变量,存储在函数栈帧中,当该函数结束时,函数栈帧销毁,栈区空间减小,局部数据的空间自然就释放了)。

参照下图看看C++程序内容对应的位置:

大小

在32位平台上,一个地址(指针)能标识的二进制数字位数有32位。而一个指针指向的地址空间大小是一个字节,那么,也就是说,在32位机器中,虚拟进程地址空间的大小为4G。

而在虚拟进程空间中,栈区的预留空间通常只有几M(具体由不同操作系统决定),堆区的预留空间大小一般没有软限制,Linux下通常小于3G,windows下通常小于2G。栈区和堆区可利用空间相差之大由此可见。

空间的划分与内存管理

通过上面的内容我们知道两个结论:

  1. 栈区的可利用空间相比堆区很少。
  2. 堆区的资源需要我们主动申请以及释放,可以灵活地根据我们的需求存储数据。

所以,由于栈区可利用空间小,C/C++程序经常需要使用堆上的空间存储数据,并且,堆上的空间资源需要用户也就是我们程序员通过代码申请,那么C/C++就需要提供一套申请堆上空间资源的方法,这个方法也叫做C/C++动态内存管理。在C语言中,管理动态内存的方式是malloc/calloc/realloc/free几个函数组成的套件。在C++中,则是new/new type[]/delete/delete[]几个操作符组成的套件。

C/C++动态内存使用(管理)方式

C语言动态内存管理方式:malloc/calloc/realloc/free

使用演示:

代码语言:javascript
复制
void test()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)calloc(4,sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 8);

	free(p1);
	free(p3);
}

这里我就不作过多讲解了,对C语言内存管理方式不够了解或者已经比较生疏了的话可以看看我的这篇文章

C++内存管理方式

由于C++是兼容C的,所以C语言内存管理方式在C++中可以继续使用,但C语言的内存管理方式并不适合C++中的某些场景,且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过**new**和**delete**操作符进行动态内存管理

new/delete操作内置类型

代码语言:javascript
复制
void test()
{
	//动态申请一个int类型大小的空间
	int* ptr1 = new int;

	//动态申请一个int类型大小的空间并初始化为10
	int* ptr2 = new int(10);

	//动态申请5个int类型大小的空间
	int* ptr3 = new int[5];
    
    //批量申请空间并初始化
    int* ptr4 = new int[10]{1,2,3,4};//后面未指定的为0
    
    
	//返还申请到的空间
	delete ptr1;
	delete ptr2;
	delete[] ptr3;
    delete[] ptr4;
}

C++对于内置类型的操作和C语言那一套相比只是简化了。但是,如果你对C语言内存管理还熟悉的话,你应该还记得C语言申请完内存还需要检查是否申请成功,为什么C++没有了?其实有的,只不过面向对象的语言更喜欢用捕异常的方式解决,而捕异常的操作被内置到了new操作符底层中,下面的内容会提到。

注意:申请和释放单个元素的空间,使用**new**和**delete**操作符,申请和释放连续的空间,使用**new[]**和**delete[]**,一定要匹配起来使用!!! 如果不匹配使用,会出现各种出人预料且棘手的问题(取决于编译器以及运行环境)。

new/delete操作自定义类型

代码语言:javascript
复制
class A
{
public:
	A()
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
};

void test()
{
	// new/delete 和 malloc/free套件最大区别:
	// new/delete 对于自定义类型除了开空间还会调用构造函数和析构函数
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;
	free(p1);
	delete p2;

	cout << "---------------------" << endl;// boundary

	p1 = (A*)malloc(10 * sizeof(A));
	p2 = new A[10];

	free(p1);
	delete[] p2;
}

运行截图:

注意:在申请自定义类型的空间时,**new**会调用构造函数,**delete**会调用析构函数,而**malloc**/**free**不会。

new/delete底层讲解

new/delete本质其实还是通过对mallocfree的封装实现的,下面从里到外带你看看是如何封装的。

operator newoperator delete函数

代码语言:javascript
复制
/*
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**来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则抛异常operator delete**最终也是通过**free**来释放空间的

注意:**operator new**和**operator delete**这两个函数不是操作符重载!重载操作符函数至少有一个参数是自定义类型参数,这两个函数是库里面实现的全局函数。这只是C++设计时一个命名失误。

newdelete的实现原理

内置类型

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

自定义类型

  • 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来释放空间。

new为例,简单看看汇编验证一下:

实验代码:

代码语言:javascript
复制
class A
{
public:
	A(int a = 0) :_a(a) {cout << "A():" << this << endl;}

	~A() {cout << "~A():" << this << endl;}

private:
	int _a;
};

int main()
{
	A* a1 = new A(1);

	delete a1;

	return 0;
}

运行截图:

定位new(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

使用格式:

new(place_address)type**或者**new(place_address)type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表。

使用演示:

代码语言:javascript
复制
class A
{
public:
	A(int a = 0) :_a(a) {cout << "A():" << this << endl;}

	~A() {cout << "~A():" << this << endl;}

private:
	int _a;
};

//定位new / replacement new
int main()
{
	A* p1 = (A*)malloc(sizeof A);
	//p1现在指向的不过是A对象相同大小的一段空间,还不是算是一个对象,因为构造函数没有执行
	new(p1)A;
	p1->~A();//析构函数	显式调用公有成员函数
	free(p1);

	A* p2 = (A*)operator new(sizeof A);
	new(p2)A(10);
	p2->~A();
	operator delete(p2);

	return 0;
}

运行截图:

使用场景:

相比于new,定位new并不怎么好用,所以实际应用场景非常少,一般是配合内存池使用的(池化技术)。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示构造函数进行初始化。

常见面试题

malloc/freenew/delete的区别

共同点:都是从堆上申请空间,并且需要从堆上申请空间,并且需要用户手动释放。

不同点:

  1. mallocfree是函数,newdelete是操作符。
  2. malloc申请的空间不会初始化,new可以初始化。
  3. malloc申请空间时需要手动计算空间大小并传递,new只需在其后跟上空间所存储数据的类型即可,如果是多个对象,[]中指定对象个数即可。
  4. malloc的返回值为void*,在使用时必须强制类型转换,new不需要,因为new后跟的是空间的类型。
  5. malloc申请空间失败时,返回的是NULL,隐私使用时必须判空,new不需要,但是new需要捕获异常。
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。

以上内容不必死记硬背,看看即可,只有你把本篇文章所讲的内容理解好自然就能将这两组套件的差异了然于胸。这两组套件最本质的区别还是使用方式以及底层的差别,只要理解了这点,结合前面的知识想讲出来不是什么难事。

内存泄漏

概念

由于C/C++的动态内存管理都是提供给用户(程序员)自行申请动态内存和返还动态内存的方式,由用户自行申请和返回动态内存资源,所以这就导致了一个问题,由于种种原因,程序可能会无法正常地返还资源。

演示:

代码语言:javascript
复制
void errorfunc()
{
	exit(-1);//这里直接退出程序跳过资源释放
}

void test()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;

	//free(p1);
	//delete p2;
	//忘记释放	代码本身的错误


	int* pa = new int[10];

	errorfunc();//导致程序出现异常的函数
	//由于程序已经出现异常,无法到执行delete[] pa,导致pa指向的资源没被释放,造成程序泄露
	delete[] pa;
}

int main()
{
	test();
	return 0;
}

危害

以上代码简单演示了内存泄露,但是,这样的代码真的会运行崩溃吗?来看看运行截图:

欸?没出现问题哦!这份代码不是向操作系统申请了资源没有返还吗?为什么操作系统不报警?

要解释这个现象需要涉及一些操作系统的知识,展开来讲内容太多了,这里简单解释一下。还记得本篇文章一开头讲的虚拟进程地址空间吗?操作系统为每一个程序提供一个独占内存的假象,程序实际使用的内存是经由虚拟地址映射到实际地址的,而对于一个进程,当进程正常退出时,操作系统会自动回收这个进程对应的所有资源,包括虚拟进程地址空间对应的一整块资源。

那么既然如此,内存泄露是不是就没有危害了?并不是,要知道,并不是所有程序都是像这个程序一样很快就跑完的!有的程序,例如操作系统,后台服务是需要一直挂着的,像这种程序基本上很少结束甚至基本不会结束,对于这样的程序,内存泄露就会导致程序崩溃甚至机器崩溃。

如何避免

  1. 好的编程习惯,使用配套的内存管理套件,记得释放资源,起码最简单的错误不能犯。
  2. 智能指针。
  3. 第三方工具。

由于这个话题能谈的东西比较多,就不在本篇博客展开谈了。

结语

以上就是C++动态内存管理方式的讲解,希望能帮助到你的C++学习。如果你觉得做的还不错的话还请点赞收藏加分享,当然如果发现我写的有误或者有建议给我的话欢迎在评论区或者私信告诉我。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 虚拟内存
    • 虚拟进程地址空间
      • 分布
      • 大小
      • 空间的划分与内存管理
  • C/C++动态内存使用(管理)方式
    • C语言动态内存管理方式:malloc/calloc/realloc/free
      • C++内存管理方式
        • new/delete操作内置类型
          • new/delete操作自定义类型
          • new/delete底层讲解
            • operator new与operator delete函数
              • new和delete的实现原理
                • 内置类型
                • 自定义类型
            • 定位new(placement-new)
            • 常见面试题
              • malloc/free和new/delete的区别
                • 内存泄漏
                  • 概念
                  • 危害
                  • 如何避免
              • 结语
              相关产品与服务
              数据保险箱
              数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档