C/C++内存管理

在之前的C语言的动态内存管理篇章中我们了解了C语言当中的相关内存函数malloc、calloc、realloc,了解了这些函数的使用方法以及各个内存函数之间的区别。在本篇中我们将继续来学习内存管理,并且还将了解两个C++中新的内存管理方式------使用new和delete操作符进行内存管理,接下来就开始本篇的学习吧!


1. C/C++内存分布

在了解C++的内存管理方式前我们先来复习之前学习过的不同的变量的内存分布

接下来我们就先来看下面的一段代码和相关问题来看看之前的内存分布,来看看之前的知识是否掌握了

cpp 复制代码
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.代码段(常量区)

1.globalVar在哪里?____
2.staticGlobalVar在哪里?____
3.staticVar在哪里?____
4.localVar在哪里?____
5.num1 在哪里?____

在以上代码中变量globalVar是全局变量,因此存储静态区

变量staticGlobalVar和staticVar都是static修饰的静态变量,因此这两个变量也存储在静态区

变量localVar是在Test函数内的局部变量,因此该变量存储在栈区

变量num1是在函数Test内的局部变量,表示的是数组首元素的地址,因此该变量存储在栈区

所以以上5个选择题的答案是 C C C A A
1.char2在哪里?____
2.*char2在哪里?___
3.pchar3在哪里?____
4.*pchar3在哪里?____
5.ptr1在哪里?____
6.*ptr1在哪里?____

在以上代码中变量char2是在Test函数内的局部变量,表示的是数组首元素的地址,因此该变量存储在栈区

*char2表示的是数组char中的首元素,但是"abcd"是常量字符串其中的元素不能改变,因此在定义出char2时会将"abcd"拷贝到栈区,因此*char2存储在栈区

pchar3表示的是指向常量字符串"abcd"的指针,因此pchar3存储在栈区

*pchar3得到的是常量字符串"abcd"中的第一个元素,因此*pchar3存储在常量区

ptr1是在函数Test内的局部变量,因此ptr1存储在栈区

*ptr1得到的是我们使用malloc动态申请的内存空间内的第一个元素,因此*ptr1存储在堆区 使用以上6个选择题的答案是A A A D A B

以上代码各个部分的存储就如以下所示:

**【说明】

  1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
    创建共享共享内存,做进程间通信。(在此我们还没有学习Linux,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段--存储全局数据和静态数据。
  5. 代码段--可执行的代码/只读常量。**

2.C++内存管理方式

在了解C++的内存管理方式前先来看看以下的代码检测是否已将C语言中动态内存管理方式理解通透

cpp 复制代码
void Test ()
{
// 1.malloc/calloc/realloc的区别是什么?
    int* p2 = (int*)calloc(4, sizeof (int));
    int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
    free(p3 );
}

首先来看以上的代码需要free(p2)吗?
在以上代码我们是先使用calloc来进行内存申请,之后再使用realloc对已经申请的内存空间进行调整,这时当原使用calloc申请之后的空间充足时在使用realloc时会直接再原空间之后继续扩容,当原空间之后不足时会找到一份新的大小充足的空间并且将原空间的数据拷贝到新的空间,因此在以上的代码当中只需要free(p3)即可,而且如果free(p3)之后再free(p2)程序会奔溃

接下来请回答以下的问题
1. malloc/calloc/realloc的区别?
2. malloc的实现原理?

在通过之前的学习我们知道malloc和calloc的区别是calloc会对申请的内存空间进行初始化,将元素都初始化为0,而malloc则在申请到内存空间后不会进行初始化的操作

malloc之前在动态内存管理篇章已经大体了解了其使用方法,具体的实现原理可以观看以下的视频讲解
【CTF】GLibc堆利用入门-机制介绍_哔哩哔哩_bilibili

复习完C语言的内存管理之后接下来学习C++中心新的内存管理方式

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

接下来就来学习该如何使用new和delete

2.1new/delete操作内置类型

new的使用方法是在new 类型名,并且new还支持在定义时初始化,只要在类型名之后再加上括号------new 类型名 () ,括号内就可以是我们要初始化的值。并且使用new后和malloc一样也需要创建指针来接收其的返回值

而在使用完new要使用delete来实现申请内存空间的释放,delete的使用方法是在delete 指针变量名

例如以下示例:

cpp 复制代码
void Test()
{
 // 动态申请一个int类型的空间
 int* ptr4 = new int;
 
 // 动态申请一个int类型的空间并初始化为10
 int* ptr5 = new int(10);
 
 delete ptr4;
 delete ptr5;
}

在new的使用当中其实还可以同时申请多个相同类型的内存空间,这时只需要使用new 类型名 [个数]的方式就可以实现,这时若要进行在定义时初始化就要在[ ]之后加上{ },在其里面就可以输入各个数据需要初始化的值。在此我们可以完全初始化也就是申请的空间有多少个数据在{ }就有多少个值,但也可以不完全初始化,也就是{ }内的值个数比创建的数据个数要少,这时为显示初始化的数据就会初始化为0

例如以下示例:

cpp 复制代码
void Test()
{
 // 动态申请10个int类型的空间
 int* ptr6 = new int[3];

// 动态申请10个int类型的空间并完全初始化
 int* ptr7 = new int[3]{0,1,2,3,4,5,6,7,8,9};

// 动态申请10个int类型的空间不完全初始化
 int* ptr8 = new int[3]{1,2,3};

 delete[] ptr6;
 delete[] ptr7;
 delete[] ptr8;
}

new和delete操作内置类型的图解如下所示:

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和delete[ ],注意:匹配起来使用。

2.2 new和delete操作自定义类型

在使用new来操作自定义类型时的大体方法也是和操作内置时相同,但和内置类型不同的是在自定义类型中在使用new时若在定义时初始化会调用相应类的构造函数来实现初始化

例如以下示例:

cpp 复制代码
#include<iostream>
using namespace std;


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对于【自定义类型】除了开空间
    //还会调用构造函数和析构函数
    A* p1 = (A*)malloc(sizeof(A));
    A* p2 = new A(1);
    free(p1);
    delete p2;
	
	return 0;
}

以上代码的输出结果如下所示:

通过以上的示例就可以了解到在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。

其实在new操作自定义类型时也可以使用new 类型名 [ ] 来实现多个空间的连续开辟

例如以下示例:

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
int main()
{
	
	A* p1 = (A*)malloc(sizeof(A)*3);
	A* p2 = new A[3]{1,2,3};
	free(p1);
	delete [] p2;
	
	return 0;
}

3. new和delete的实现原理

在了解new和delete的实现原理之前我们先要来了解operator new与operator delete函数

3.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)

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常,这和malloc申请空间失败时就返回NULL有所不同。operator delete 最终是通过free来释放空间的

3.2new和delete实现原理

3.2.1内置类型

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

3.2.2自定义类型

**new的原理

  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造**
    **delete的原理
  3. 在空间上执行析构函数,完成对象中资源的清理工作
  4. 调用operator delete函数释放对象的空间**
cpp 复制代码
#include<iostream>
using namespace std;

class a
{
public:
	a(int a = 0)
		: _a(a)
	{
		cout << "a():" << this << endl;
	}
	~a()
	{
		cout << "~a():" << this << endl;
	}
private:
	int _a;
};
int main()
{
	
	a* p1 = (a*)malloc(sizeof(a)*3);
	a* p2 = new a(3);
	free(p1);
	delete  p2;
	
	return 0;
}

我们通过以上的代码的反汇编代码来了解new和delete在实例当中是如何调用operator new和operator delete

new T[N]、delete []和new、delete的实现方式大致一样只不过调用函数次数更多

**new T[N]的原理

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

那么在了解了new和delete的实现原理后,你可能就会有疑问了,在之前我们的示例当中new能实现要求的地方使用malloc也可以实现,那么学习new有什么必要呢?

确实在以上使用new的地方使用malloc也都是可以的,看起来new只是写起来没有malloc那么繁琐,但其实在一些情况下我们是只能使用new和delete的,例如以下的情况

cpp 复制代码
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = new STDataType [n];
		
		_capacity = n;
		_top = 0;
	}
	Stack(const Stack& st)
	{
		_a = new STDataType[st._capacity];
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
int main()
{
	//假设要在堆区上申请一个栈对象
	Stack* st = new Stack(10);

	delete st;

	return 0;
}

就例如以上示例若我们要在堆上申请一个栈对象这时malloc和free就无法实现要求了,这是因为在栈对象中有自资源的申请和清理工作需要在初始化时调用类Stack的构造函数,在释放该对象之前调用类Stack的析构函数再进行空间的释放,这些过程当中malloc是无法在初始化时调用构造函数;free无法在释放空间之前调用析构函数,因此在这种情况下就只能使用new和delete

以上的代码中各个变量存储以及关系图如下所示:

以上结合new和delete实现过程图如下所示:

4.定位new表达式(placement-new) (了解)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如
果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

cpp 复制代码
#include<iostream>
using namespace std;

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));
	// 注意:如果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;
}

5.malloc/free和new/delete的区别

在以上我们了解了new'和delete的使用方法以及实现时的原理,那么接下来我们就来详细了解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在释放空间前会调用析构函数完成空间中资源的清理释放**

以上就是本篇的所有内容了,希望能得到你的点赞和收藏!

相关推荐
m0_631270405 分钟前
标准C++(二)
开发语言·c++·算法
沫刃起8 分钟前
Codeforces Round 972 (Div. 2) C. Lazy Narek
数据结构·c++·算法
GZM8888881 小时前
配置VS Code以进行C/C++编程:深入探讨与实操指南
c++
martian6651 小时前
学懂C++(六十):C++ 11、C++ 14、C++ 17、C++ 20新特性大总结(万字详解大全)
开发语言·c++·c++20
小灰灰爱代码2 小时前
C++——判断year是不是闰年。
数据结构·c++·算法
小灰灰爱代码2 小时前
C++——求3个数中最大的数(分别考虑整数、双精度数、长整数数的情况),用函数重载方法。
数据结构·c++·算法
handsome2133 小时前
WSL中使用GPU加速AMBER MD--测试
笔记·学习
爱coding的橙子4 小时前
CCF-CSP认证考试准备第十七天
数据结构·c++·算法
WZF-Sang4 小时前
Linux权限理解【Shell的理解】【linux权限的概念、管理、切换】【粘滞位理解】
linux·运维·服务器·开发语言·学习
狂飙的张兴发5 小时前
认知小文2《成功之路:习惯、学习与实践》
学习·考研·职场和发展·跳槽·学习方法·改行学it·高考