C/C++内存管理

前言

动态内存的开辟C语言我们可以通过malloc、calloc、realloc、free等函数来完成我们的需求,但是在C++中我们通过new、delete关键字来完成。本章我们将详细讲解new、delete。

一、回顾C/C++的内存分布&动态管理

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

平时我们编写的程序有:全局对象、局部对象、static对象、常量、函数体等,那他们存储在哪里呢,如下图:


tip:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的非静态局部变量、函数参数、返回数据、返回地址等,栈是向下增长的。
  2. 内存映射段:内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(在Linux详细讲解)
  3. 堆区(heap)堆用于程序运行时动态内存分配,堆是可以向上增长的。一般由程序员分配释放,生存期由我们决定,非常灵活,但是问题也最多。若程序员自己不释放,程序结束时可能由OS回收。
  4. 数据段(静态区)存储全局数据和static数据。内存在程序编译时就已经分配好了,这块内存在程序的整个运行期间都存在,程序结束后由系统释放。
  5. 代码段(常量区)存放可执行的代码和只读常量,可执行的代码------函数体(类成员函数和全局函数)的二进制代码。

混淆点:

  • static修饰局部变量不会影响作用域,但是生命周期变长了。
  • const修饰并不会影响存储的区域,只是修饰的常变量不能被修改了(本质还是变量)

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

代码示例:

cpp 复制代码
int main()
{
	//1、使用malloc向堆区申请10个int型的连续空间
	int* p1 = (int*)malloc(sizeof(int) * 10);
	//判断是否malloc是否开辟成功
	if (nullptr == p1)
	{
		//开辟失败打印错误信息并退出
		perror("malloc");
		return -1;
	}
	//malloc使用之前,必须自己初始化,因为malloc申请完空间,没有初始化,直接返回起始地址
	free(p1);
	p1 = nullptr;//free释放之后p2没有改变,仍然能找到那块空间,所以p2为野指针,有危险,建议free之后都将其置为空

	//2、使用calloc向堆区申请10个int型的连续空间
	int* p2 = (int*)calloc(10, sizeof(int));
	//判断是否calloc是否开辟成功
	if (nullptr == p2)
	{
		//开辟失败打印错误信息并退出
		perror("calloc");
		return -1;
	}
	//calloc可以直接使用,因为calloc申请好空间后,会把空间初始化为0,然后返回起始地址

	//3、使用realloc可以调整动态开辟内存空间,例如将p2所指的动态内存扩大到20个int
	//realloc调整内存空间时存在三种情况:
	//	①原地扩容:原有空间之后有足够大的空间,直接在后面追加空间,返回旧地址
	//	②异地扩容:原有空间之后没有足够大的空间,在堆区另找一块合适的空间,并把旧空间free,同时返回新空间的起始地址
	//	③开辟失败:找不到合适的空间,开辟失败返回null
	int* p3 = (int*)realloc(p2, 20 * sizeof(int));//防止开辟空间失败,使用一个新的指针变量来接收realloc的返回值
	//判断是否扩容成功,扩容成功仍用p2来指向这块空间
	if (p3 != nullptr)
	{
		//扩容成功
		p2 = p3;
		p3 = nullptr;
	}
	else
	{
		//扩容失败,打印错误信息,并退出
		perror("realloc");
		return -1;
	}
	//使用realloc扩容的空间之前,需要初始化
	free(p2);
	p2 = nullptr;//free释放之后p2没有改变,仍然能找到那块空间,所以p2为野指针,有危险,建议free之后都将其置为空

	return 0;
}

tip:

  • malloc、calloc、realloc、free是C语言 提供的开辟动态内存四个库函数
  • malloc函数:C语言提供的一个动态内存开辟的函数
    • 函数原型:void* malloc(size_t size);
    • 函数作用:在堆区申请一块连续可用的空间,并返回指向这块空间的指针
    • 具体讲解malloc函数
      • 如果开辟成功,则返回一个指向开辟好的空间的起始地址
      • 如果开辟失败,则返回一个NULL。因此malloc的返回值需要做检查。
      • 返回值的类型是void*,因为malloc函数不知道用户开辟什么类型的空间,具体类型在用户使用时自己决定。
      • 参数size是开辟空间的大小(单位:字节),注意:如果参数为0,malloc的行为是标准未定义的,取决于编译器。
  • calloc函数:也是C语言提供用来动态开辟内存的函数
    • 函数原型:void* calloc(size_t num,size_t size);
    • 函数作用:calloc函数的功能是为num个大小为size的元素开辟一块空间,并且将空间的每个字节初始化为0,然后返回它的起始地址
    • calloc于malloc的区别
      • malloc只有一个参数,开辟空间的大小;calloc有两个参数,分别为num元素的个数和size每个元素的大小
      • malloc申请到空间,没有初始化,直接返回起始地址;calloc申请好空间后,会把空间的每一个字节初始化为0,然后返回起始地址
      • malloc的效率比calloc高一些,但是malloc不会初始化空间。
  • realloc函数:调整动态开辟内存空间的大小的函数
    • 函数原型:void* realloc(void* ptr,size_t size);
    • 函数作用:realloc函数让动态内存管理更加灵活,可以做到对动态内存大小的调整。
    • 具体讲解
      • ptr是要调整的动态内存地址,如果ptr为一个NULL,则功能与malloc函数类似
      • size是调整之后的新大小
      • realloc在调整内存空间时存在三种情况:
        • ①原地扩容:原有空间后面有足够大的空间,直接在原有空间后面追加空间,原有空间数据不变,新空间数据为随机值,返回旧的空间起始地址。
        • ②异地扩容:原有空间后面没有足够大的空间,realloc会另找一块合适大小的新的连续的空间,把旧空间的数据拷贝到新空间的前面位置,并且把旧的空间free,同时返回新的空间的起始地址。
        • 扩容失败:找不到合适的空间,realloc开辟空间失败返回NULL。
        • 建议:综上三种情况,建议使用一个新的指针变量来接收realloc函数的返回值,防止realloc开辟失败后,原有空间也不能使用。
      • 判断realloc是否开辟成功,开辟成功仍然使用扩容之前的指针变量保存realloc的返回值,开辟失败打印错误并退出。
  • free函数:C语言提供来专门用来做动态内存的释放和回收的函数
    • 函数原型:void free(void* ptr);
    • 函数作用:free函数专门用来释放动态开辟的内存。
    • 具体讲解
      • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
      • 如果参数ptr是NULL,则函数什么事都不做。
      • 参数ptr是指向释放的动态内存的内存的指针,类型为void*是因为不知道是什么类型的指针。
    • 编程好习惯
      • 当动态内存使用完之后一定要记得使用free释放所开辟的空间,因为可能造成内存泄漏。
      • free释放完之后的指向动态内存开辟的空间的指针变量不会改变,仍指向那块空间,有危险可能造成非法访问或free多次,所以建议free之后一定记得将其置为NULL
  • 动态内存的两种回收方式
    • 主动释放------free
    • 程序结束(操作系统回收):程序结束时可能由OS回收。但是如果程序在服务器上7*24一直运行,用户不主动free或者找不到这块空间,就会造成内存泄漏。
    • 内存泄漏:内存泄漏是指程序中已分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
  • void*指针类型:当用户不确定指针变量是什么类型时,设计成void*,void*是无具体类型的指针
    • void*的优点:可以存储任意类型的地址
    • void*的缺点:不能直接使用,因为是无具体类型,不知道是什么类型的地址。
    • 使用方式:强制类型转换。

二、C++内存管理方式

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

1、new/delete操作内置类型

代码示例:

cpp 复制代码
int main()
{
	//动态申请一个int类型的空间
	//C
	int* ptr1 = (int*)malloc(sizeof(int));
	free(ptr1);
	ptr1 = nullptr;
	//CPP
	int* ptr2 = new int;
	delete ptr2;
	ptr2 = nullptr;

	//动态申请一个int类型的空间,并将其初始化为10
	//C
	int* ptr3 = (int*)malloc(sizeof(int));
	*ptr3 = 10;//malloc不能初始化,realloc只能将其初始化为0
	free(ptr3);
	ptr3 = nullptr;
	//CPP
	int* ptr4 = new int(10);//单个对象可以使用()初始化
	delete ptr4;
	ptr4 = nullptr;

	//动态申请10个int类型的空间
	//C
	int* ptr5 = (int*)malloc(sizeof(int) * 10);
	free(ptr5);
	//CPP
	int* ptr6 = new int[10];
	delete[] ptr6;

	//动态申请3个int类型的空间,并将其初始化为1,2,3
	int* ptr7 = new int[3]{ 1,2,3 };//多个对象可以使用C++11的初始化列表{}初始化
	delete[] ptr7;

	return 0;
}

tip:

  • 申请和释放单个元素 的空间,使用new和delete 操作符,申请和释放连续 的空间,使用new[]和delete[],注意:匹配起来使用。
  • delete释放完空间之后与free一样,不会改变指向动态内存开辟的空间的指针变量,有危险,建议释放完之后将其置为空指针。
  • malloc/free和new/delete的区别:
    • malloc和free是函数,new和delete是操作符
    • malloc申请的空间不会初始化,new可以初始化,初始化方式------①单个对象使用()初始化;②动态数组使用列表初始化{}
    • malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
    • malloc的返回值为void*,在使用时必须强转类型,new不需要,因为new后跟的是空间的类型

总结: 动态申请内置类型的数据,new和malloc除了用法上面,其他方面没有什么区别。

祖师爷不会因为用法麻烦就搞出来了new和delete,最重要的是因为malloc和free无法解决自定义类型的动态申请。

2、new和delete操作自定义类型

代码示例:

cpp 复制代码
//注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	// new/delete 和 malloc/free最大区别是 new/delete对于自定义类型除了开空间还会调用构造函数和析构函数
	//malloc单纯开空间
	A* p1 = (A*)malloc(sizeof(A));
	//free单纯释放空间
	free(p1);
	//new:开空间+调用构造函数
	A* p2 = new A(1);
	//delete:调用析构函数+释放空间
	delete p2;
	
	//new多个自定义对象也会调用多个构造函数
	A* p6 = new A[10]{A(1), A(2)};//没有显式初始化的自定义对象调用默认构造初始化
	//delete多个自定义对象也会调用多个析构函数
	delete[] p6;
	return 0;
}

运行结果:

tip:

  • malloc/free和new/free最大的区别:在动态申请自定义类型的数据时,malloc/new除了用法上不同,还有一个最大的区别,new会调用构造函数初始化,delete会调用析构函数清理,而malloc与free不会。
  • 注意:当自定义类型没有默认构造函数时,new对象时就必须完成初始化。
  • new多个自定义对象会调用多个构造函数,delete多个自定义对象,也会调用多个析构函数。

三、new和delete的实现原理

1、operator new与operator delete函数(重点)

new和delete 是用户进行动态内存申请和释放的操作符operator new和operator delete 是系统提供的全局函数

new在底层调用operator new 全局函数来申请空间,delete在底层调用operator delete 全局函数来释放空间。

cpp 复制代码
/*
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)

tip:

  • 因为面向对象语言处理失败,不喜欢用返回值,更建议用抛异常,所以使用operator new开空间。(使用operator delete释放空间是为了与operator new配对。)
  • 因为C语言库里面已经有了malloc开空间和free释放空间,所以operator new和operator delete复用了malloc和free
    • operator new实际也是通过malloc来申请空间, 如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则抛异常。
    • operator delete最终是通过free来释放空间的。

代码示例:抛异常的情况

cpp 复制代码
int main()
{
	int* p = nullptr;
	try
	{
		do
		{
			//p = (int*)malloc(1024 * 1024);//malloc开辟失败,返回空
			p = new int[1024 * 1024];//new开辟失败,抛异常直接跳到catch
			cout << p << endl;
		} while (p);
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

异常的相关知识点,后续会详细讲解,现在只需要知道new开辟空间失败后会抛异常

2、内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似。

不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请和释放的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

3、自定义类型

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 4)
		:_top(0)//栈顶指针初始化为0,指向栈顶指针的下一个元素
		, _capacity(capacity)
	{
		_arr = (int*)malloc(sizeof(int) * capacity);
		//检查......
	}
	~Stack()
	{
		free(_arr);
		_arr = nullptr;
		_top = 0;
		_capacity = 0;
	}
private:
	int* _arr;
	int _top;
	int _capacity;
};

int main()
{
	try
	{
		//动态申请一个堆区的Stack对象
		//使用malloc不会调用构造函数初始化,free也不会调用析构函数清理
		//所以使用new和delete
		//new:开空间 + 调用构造函数
		Stack* p = new Stack;
		//delete: 调用析构 + 释放空间
		delete p;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

F10调试,观察其汇编代码,如下:


tip:

  • new的原理
    1. 调用operator new函数申请空间
    2. 在申请的空间上执行构造函数,完成对象的初始化
  • delete的原理
    1. 在空间上执行析构函数,完成对象中资源的清理工作(如上Stack对象,如果先释放对象,就找不到空间了,无法执行析构,可能造成内存泄漏。)
    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表达式(placement-new)(了解)

1、引入

使用malloc动态申请自定义类型,只是单纯的开空间,不会调用构造函数。我们有什么方法可以使已开辟的自定义类型的空间调用构造函数呢》

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

即:非new操作符动态申请的自定义类型,想要显示的调用构造函数,需要使用定位new表达式

2、解析定位new表达式

代码示例:

cpp 复制代码
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()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	//显示调用构造函数------需要使用定位new
	new(p1)A;// 注意:如果A类的构造函数有参数时,此处需要传参
	//显示调用析构函数------直接调用
	p1->~A();
	free(p1);

	return 0;
}

tip:

  • 使用格式
    1. 不带参:new(目标指针变量)type
    2. 带参:new(目标指针变量)type(该类型的初始化列表)
  • 使用场景
    1. 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定位表达式进行显示调用构造函数进行初始化。
    2. 例如:C++中STL的链表,库中的实现就应用了内存池与定位new表达式。

补充: 显示调用析构函数------目标指针变量->析构函数。

五、总结malloc/free和new/delete的区别

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

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

总结:C++使用new/delete管理内存最大的原因不是用法上变简洁了,而是申请自定义类型new除了开空间还会调用构造函数,delete除了释放空间还会调用析构函数清理。

相关推荐
海螺姑娘的小魏20 分钟前
Effective C++ 条款 26:尽可能延后变量定义式的出现时间
开发语言·c++
w(゚Д゚)w吓洗宝宝了26 分钟前
C++ 环境搭建 - 安装编译器、IDE选择
开发语言·c++·ide
王老师青少年编程1 小时前
gesp(二级)(16)洛谷:B4037:[GESP202409 二级] 小杨的 N 字矩阵
数据结构·c++·算法·gesp·csp·信奥赛
No0d1es2 小时前
2024年12月青少年软件编程(C语言/C++)等级考试试卷(三级)
c语言·开发语言·青少年编程·电子学会·三级
机器视觉知识推荐、就业指导2 小时前
C++设计模式:解释器模式(简单的数学表达式解析器)
c++·设计模式·解释器模式
海螺姑娘的小魏2 小时前
Effective C++ 条款 16:成对使用 `new` 和 `delete` 时要采取相同形式
开发语言·c++
茶猫_2 小时前
力扣面试题 - 40 迷路的机器人 C语言解法
c语言·数据结构·算法·leetcode·机器人·深度优先
点云SLAM3 小时前
C++创建文件夹和文件夹下相关操作
开发语言·c++·算法
CodeClimb3 小时前
【华为OD-E卷 - 猜字谜100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
_小柏_3 小时前
C/C++基础知识复习(46)
c语言·开发语言·c++