前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >今天你学C++了吗?——内存管理

今天你学C++了吗?——内存管理

作者头像
用户11352420
发布2024-12-25 10:30:57
发布2024-12-25 10:30:57
9400
代码可运行
举报
文章被收录于专栏:编程学习编程学习
运行总次数:0
代码可运行

C/C++内存分布

我们先来看看下面这一段代码

代码语言: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);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}

接着再看看下面的题目~

选择题: 选项: A . 栈 B . 堆 C . 数据段 ( 静态区 ) D . 代码段 ( 常量区 ) globalVar 在哪里? ____ staticGlobalVar 在哪里? ____ staticVar 在哪里? ____ localVar 在哪里? ____ num1 在哪里? ____ char2 在哪里? ____ * char2 在哪里? ___ pChar3 在哪里? ____ * pChar3 在哪里? ____ ptr1 在哪里? ____ * ptr1 在哪里? ____

前面在C语言阶段的一篇博客 C语言——动态内存分配我们提到过C/C++程序中内存区域的划分~

C/C++程序内存分配的几个区域:

1. 栈区(stack) >>在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时 这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内 存容量有限。 >>栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

2. 堆区(heap) >>一般由程序员分配释放, 也就是动态内存开辟的空间,若程序员不释放,程序结束时可能由OS回收 ,分配方式类似于链表。

3. 数据段(静态区)(static) >>存放全局变量、静态数据,程序结束后由系统释放~

4. 代码段 >>存放函数体(类成员函数和全局函数)的二进制代码~

知道了这些基础,相信上面的题目也就简单很多了~


正确答案: 选项: A . 栈 B . 堆 C . 数据段 ( 静态区 ) D . 代码段 ( 常量区 ) globalVar 在哪里? C 解析: globalVar是全局变量,存放在数据段 ( 静态区 ) staticGlobalVar 在哪里?C 解析:staticGlobalVar是静态全局变量,存放在数据段 ( 静态区 ) staticVar 在哪里?C 解析: staticVar是静态局部变量,依然是静态变量数据,存放在数据段 ( 静态区 ) localVar 在哪里?A 解析: localVar是局部变量,存放在栈区 num1 在哪里?A 解析:num1也就是数组名,是一个局部数组变量,也存放在栈区 char2 在哪里?A 解析:char2是一个字符数组,也存放在栈上 * char2 在哪里?A 解析:char2是一个字符数组,把后面常量串拷贝过来到数组中,数组在栈上,所以*char2在栈上 pChar3 在哪里?A 解析: pChar3相当于是局部指针变量存放在栈区~ * pChar3 在哪里?D 解析:*pChar3得到的是常量字符串,常量字符在代码段 ptr1 在哪里?A 解析: ptr1相当于是局部指针变量存放在栈区~ * ptr1 在哪里?B 解析:*ptr1得到的是动态申请空间的数据在堆区

画图理解:

补充说明: 1. 又叫堆栈—— 存放非静态局部变量 / 函数参数 / 返回值等等,栈是向下增长的。 2. 内存映射段 是高效的 I/O 映射方式,用于装载一个共享的动态内存库~用户可使用系统接口 创建共享共享内存,做进程间通信~(后面会学习) 3. 用于程序运行时动态内存分配,堆是可以上增长的~ 4. 数据段 -- 存储全局数据和静态数据~ 5. 代码段 -- 可执行的代码 / 只读常量~

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

C语言——动态内存分配

前面C语言阶段我们知道可以通过malloc/calloc/realloc/free来进行动态内存管理~

我们来简单实现一下~

代码语言:javascript
代码运行次数:0
复制
//C语言版动态内存管理
#include<stdio.h>
#include<stdlib.h>//malloc,calloc,realloc,free头文件
int main()
{
	//动态开辟一块空间
	//malloc动态开辟空间,不会进行初始化
	int* ptr1 = (int*)malloc(sizeof(int) * 10);

	//calloc动态开辟空间,并且数据初始化为0
	int* ptr2 = (int*)calloc(10,sizeof(int));

	//realloc扩容
	int* ptr3 = (int*)realloc(ptr2,100);

	free(ptr1);
	//free(ptr2);//err
	//ptr2已经进行扩容——两种情况
	// 原来位置后面的空间足够,直接在后面扩容,扩容返回地址给ptr3,ptr2和ptr3指向同一块空间,只需要free一次
	// 原来位置后面的空间不够,新开一块空间实现扩容,编译器会释放掉ptr2,不需要我们free了
	free(ptr3);//只需要free掉ptr3
	return 0;
}

注意点

》malloc和calloc都是动态开辟一块空间,但是它们的参数不相同 malloc动态开辟空间,不会进行初始化,而calloc动态开辟空间,并且会将数据初始化为0 》使用realloc进行扩容,有两种情况: 1.原来位置后面的空间足够,直接在后面扩容,上面的代码扩容返回地址给ptr3,ptr2和ptr3指向同一块空间,只需要free一次 2.原来位置后面的空间不够,新开一块空间实现扩容,编译器会释放掉ptr2,不需要我们free了

C++内存管理方式

C 语言内存管理方式当然可以继续在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,所以 C++ 提出了自己的内存管理方式: 通过 new和delete操作符 进行动态内存管理

new/delete操作内置类型

例:

代码语言:javascript
代码运行次数:0
复制
//C++版动态内存管理
#include<iostream>
using namespace std;
void test()
{
	// 动态申请一个int类型的空间
	int* ptr4 = new int;

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

	// 动态申请3个int类型的空间
	int* ptr6 = new int[3];
	int* ptr7 = new int[3] {1, 2, 3};//可以在后面使用{}进行初始化
	//使用delete释放空间
	delete ptr4;
	delete ptr5;//申请和释放单个元素的空间,使用new和delete操作符
	//匹配使用!!!
	delete[] ptr6; //申请和释放连续的空间,使用new[]和delete[]
	delete[] ptr7;
}
int main()
{
	test();
	return 0;
}

画图理解:

new和delete操作自定义类型

我们来看看下面的代码:

代码语言:javascript
代码运行次数:0
复制
class Example
{
private:
	int _x;
	int _y;
public:
	//构造函数
	Example(int x = 0, int y = 0) :_x(x), _y(y)
	{
		cout << "Example " << this << endl;
	}
	//析构函数
	~Example()
	{
		cout << "~Example " << this << endl;
	}
};

void test2()
{
	Example* e1 = new Example(1,2);
	Example* e2 = (Example*)malloc(sizeof(Example));
	Example* e3 = (Example*)calloc(1, sizeof(Example));
	cout << endl;

	Example* e4 = new Example[3];
	Example* e5 = new Example[3]{ {1,2},{3,4},{5,6} };//类似于二维数组初始化
	Example* e6 = (Example*)malloc(sizeof(Example) * 3);
	Example* e7 = (Example*)calloc(3, sizeof(Example));
	cout << endl;

	delete e1;
	free(e2);
	free(e3);
	cout << endl;

	delete[]e4;
	delete[]e5;
	free(e6);
	free(e7);
}

int main()
{
	//test1();
	test2();
	return 0;
}

在这里, new/delete 和 malloc/calloc/free最大区别就体现出来了 》 在申请自定义类型的空间时, new会调用构造函数,delete会调用析构函数 ,而 malloc free 不会~ 》 对于内置类型,它们的效果是差不多的,都是开辟空间~

事实上,上面的代码还涉及到编译器优化和类型转换~

》对象数组我们是用来存储对象的数组 》我们正常的使用方式应该是先创建几个对象,再把它们存储到数组里面,而上面我们直接使用 { 整型,整型 } 实现了对象的创建,这是因为编译器优化和类型转换实现了这样一个功能,在这里也就直接调用构造函数实例化出我们需要的对象~

例:

代码语言:javascript
代码运行次数:0
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//构造函数
	Example(int x = 0, int y = 0) :_x(x), _y(y)
	{
		cout << "Example " << this << endl;
	}
	//析构函数
	~Example()
	{
		cout << "~Example " << this << endl;
	}
};

void test2()
{
	Example* e1 = new Example(1, 2);
	Example* e2 = (Example*)malloc(sizeof(Example));
	Example* e3 = (Example*)calloc(1, sizeof(Example));
	cout << endl;
	//对象数组
	//创建对象放到数组里面
	//数组名也就是首元素地址
    //一种方式
	Example e11(1, 1), e22(2, 2), e33(3, 3);
	Example* e123 = new Example[3]{ e11,e22,e33 };
	//使用默认值
	Example* e4 = new Example[3];
	//这一种直接写的方式是隐式类型转换+编译器优化——直接调用了构造函数
	Example* e5 = new Example[3]{ {1,2},{3,4},{5,6} };//类似于二维数组初始化
	Example* e6 = (Example*)malloc(sizeof(Example) * 3);
	Example* e7 = (Example*)calloc(3, sizeof(Example));
	cout << endl;

	delete e1;
	free(e2);
	free(e3);
	cout << endl;

	delete[] e123;
	delete[]e4;
	delete[]e5;
	free(e6);
	free(e7);
}

int main()
{
	//test1();
	test2();
	return 0;
}

operator new与operator delete函数

operator new 和 operator delete看起来和new、delete差不多,它们有什么特别的作用呢?我们一起来看看~

new和delete是用户进行动态内存申请和释放的操作符operator new 和operator delete是系统提供的全局函数new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间 》 事实上, new的核心操作是operator new + 构造函数 delete的核心操作是析构函数+operator delete

我们结合下面的代码来进行理解:

先来看看operator new和operator delete的实现:

代码语言: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 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来释放空间的~ 所以: new的核心操作是operator new + 构造函数 delete的核心操作是析构函数+operator delete 有的人可能会好奇既然是这样那为什么不直接使用malloc+构造函数呢,我们知道malloc开辟空间失败会返回NULL,如果直接使用malloc,那么开辟空间失败就不能够发现异常原因,这并不是我们希望的~我们希望处理异常更加清晰~

new和delete的实现原理

有了前面的这些基础,我们对new和delete的原理以及使用就更加清晰了,接下来,我们就来总结一下new和delete的实现原理

内置类型

》 如果申请的是内置类型的空间,new 和 malloc , delete 和 free 原理基本类似~ 》 不同的地方: 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来释 放空间

malloc/free和new/delete的异同

说了前面这么多,我们来看看malloc/free和new/delete的异同~

相同点:malloc/free和new/delete 都是从堆上申请空间,并且需要用户手动释放

不同点: 1. 》 malloc和free 是 函数 》 new和delete 是 操作符 2. 》 malloc 申请的空间不会初始化 (calloc也只是可以初始化为0) 》 new 可以 初始化成我们想要的值 3. 》 malloc 申请空间时, 需要手动计算空间大小(字节为单位)并传递 》 new 只需在其后跟上空间的类型即可 ,如果是 多个对象,[ ]中指定对象个数 就可以了 同时使用new/delete需要注意的是 匹配使用 : 申请和释放单个元素的空间,使用new和delete操作符 申请和释放连续的空间,使用new[ ]和delete[ ] 4. 》 malloc 的返回值为 void*, 在使用时必须 强转 为我们想要的类型 》 new不需要强制类型转换 ,因为 new后面跟的就是空间的类型 5. 》 malloc申请空间失败时,返回的是NULL ,因此 使用时必须判空看是否开辟成功 (前面的代码小练习因为一般不会开辟失败,就没有进行判断) 》 new不需要判断是否为空 ,但是 new需要捕获异常 (后面的博客会讲到) 6. (最大的区别) 》 申请 自定义类型对象 时, malloc/free只会开辟空间,不会调用构造函数与析构函数 》 new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放

!!!不要混合使用!!!

我们来看看混合使用的例子:

1、malloc开辟,delete释放(这里无论是内置类型,还是自定义类型都会有警告提示,但是不会报错)

2、new [] 开辟,delete释放

a、内置类型(会有警告提示,但是不会报错)

b、自定义类型(报错)

这是为什么呢?我们一起来发现原因~

我们来看看它们的反汇编

这里内置类型需要多少空间,就开辟了多少~没有什么太大的问题~

而自定义类型使用new[ ] 开辟了84个字节空间,本来应该是80个字节空间就可以的,这是因为多开辟了4个字节空间来存放数据个数,为什么要存数据个数呢?new是知道要开辟多少个空间的,但是delete不知道数据个数,他需要通过这个来知道自己需要调用多少次析构函数~所以我们需要s使用delete[ ]

报错的原因也就是因为调用的析构函数次数不够,多开的空间没有释放,造成内存泄漏,同时释放空间的位置也不对~

画图理解:

所以在使用的时候, 一定要匹配使用,一定要匹配使用,一定要匹配使用!!! 避免问题出现!!!

定位new表达式(placement-new)

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

使用格式: new (place_address) type或者new (place_address) type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景: 定位new表达式在实际中一般是配合内存池使用。因为 内存池分配出的内存没有初始化 ,所以 如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化 。

例:

代码语言:javascript
代码运行次数:0
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//构造函数
	Example(int x = 0, int y = 0) :_x(x), _y(y)
	{
		cout << "Example " << this << endl;
	}
	//析构函数
	~Example()
	{
		cout << "~Example " << this << endl;
	}
};

int main()
{
	Example* e1 = new Example(1, 1);//日常使用new,可以直接初始化

	//定位new表达式
	//e2、e3现在指向的是与Example对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	Example* e2 = (Example*)malloc(sizeof(Example));//已经开辟的空间
	new(e2)Example(1, 2);//已分配的原始内存空间中调用构造函数初始化一个对象
	
	Example* e3 = (Example*)operator new(sizeof(Example));//已经开辟的空间
	//operator new与malloc使用类似,只开辟空间,没有初始化
	new(e3)Example(3, 4);//已分配的原始内存空间中调用构造函数初始化一个对象
	//e3->Example();//不支持显示调用构造函数

	delete e1;//与new匹配

	free(e2);//与malloc匹配

	e3->~Example();//支持显示调用析构函数
	operator delete(e3);//与operator new匹配
	return 0;
}

通过监视,我们发现使用定位new表达式成功初始化了已开辟的空间~

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • C/C++内存分布
  • C语言中动态内存管理方式:malloc/calloc/realloc/free
  • C++内存管理方式
    • new/delete操作内置类型
    • new和delete操作自定义类型
  • operator new与operator delete函数
  • new和delete的实现原理
    • 内置类型
    • 自定义类型
  • malloc/free和new/delete的异同
  • !!!不要混合使用!!!
  • 定位new表达式(placement-new)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档