详解c++---内存管理

c语言在堆上申请空间

在之前的学习中我们知道c语言主要是通过malloc free calloc,realloc这几个函数在堆上申请和释放的空间,那么这里为了防止大家忘了这几个函数,我们下面给大家看看这几个函数的使用方法。

malloc

在这里插入图片描述
那么这个函数的参数就是你想要开辟的空间的大小,这个函数的返回值就是一个指针,这个指针指向的就是我们这个内存的起始位置,但是这个指针的类型是void*类型,所以我们在使用这个函数的时候我们就得加一个强制类型转换,其次通过上面的介绍我们还知道这里开辟的空间他是不会主动初始化的,所以在我们不主动给这段空间赋值之前这段内存里面存放的是随机值,而且当我们申请一段特别大的空间,或者不停的申请的话,我们这里会出现申请空间失败的情况,所以当空间申请失败的话,这个函数会放回一个空指针,所以在往后我们用这个函数来申请空间的时候,我们就得在申请空间之后判断一些这里的空间是否开辟成功,如果开辟失败的话我们就打印一些文字出来用于警示使用者,那么我们这里就可以通过下面的代码来给那大家演示一下这个函数的使用,比如说我想要在堆上申请一个int大小的空间:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* pa = (int*)malloc(sizeof(int));
	if (pa == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	*pa = 2022;
	printf("该内存上的数据为:%d", *pa);
	 free(pa);
	 pa=NULL;
	return 0;
}

运行结果如下:
在这里插入图片描述
如果我想要申请两个int类型大小的空间,我们就只用在malloc函数中的sizeof后面加个*2即可,那么我们的代码如下:

int main()
{
	int* pa = (int*)malloc(sizeof(int)*2);
	if (pa == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	*pa = 2022;
	*(pa + 1) = 2023;
	printf("该内存上的数据为:%d %d", *pa,*(pa+1));
	free(pa);
	 pa=NULL;
	return 0;
}

其代码的运行结果如下:
在这里插入图片描述

calloc

calloc的函数介绍如下:
在这里插入图片描述
calloc和malloc这两个函数的功能大致是一样的,这两个函数的区别就在calloc的参数不一样,并且calloc在开辟空间的时候会顺带进行初始化,将空间中的内容全部为0,malloc的参数只有一个就是你要开辟空间的大小,而这里的参数有两个其意思就是你想要开辟多少个类型大小的空间,比如说我想要开辟3个大小为int类型的空间,那么这样的话参数就是3,sizeof(int),我想要开辟4个大小为char类型的空间,那么这样的话参数就是:4,sizeof(char)等等,这里的返回值也是和malloc一样的如果开辟成功就返回这里的空间的起始地址,如果开辟失败这里就返回一个空指针,那么我们来看看下面的例子,我要开辟4个大小为int类型的空间,并且不对这些空间做任何的操作,来看看这个空间中的值,那么我们的代码就如下:

int main()
{
	int* pa = (int *)calloc(4, sizeof(int));
	if (pa == NULL)
	{
		printf("calloc fail");
		exit(-1);
	}
	printf("%d ", *pa);
	printf("%d ", *(pa + 1));
	printf("%d ", *(pa + 2));
	printf("%d ", *(pa + 3));
	free(pa);
	pa=NULL;
	return 0;
}

我们将这个代码运行一下就可以看到这里打印出来的结果都是0
在这里插入图片描述

realloc

我们来看看这个函数的基本介绍:
在这里插入图片描述
realloc这个函数的功能就是改变我们之前开辟的空间大小,比如说我之前开辟了一个20字节空间,那么我想把这个空间的扩大成40个字节,那么这里我们就可以使用realloc这个函数来实现扩容,那么这个函数有两个参数,一个参数是原来空间的地址,另外一个参数就是你想要更改的空间大小的值,当然扩容也有成功或者失败,如果我们这里扩容成功了就会返回该空间新的地址当然这个地址可能跟原来一模一样,也可能跟原来不一样,那么这个取决于堆中的情况,既然有成功那么就会有失败,如果是失败的话我们这个函数返回的值就是一个空指针,所以当我们在用这个函数来开辟空间的话我们这里就不要用原来的那个指针来进行接收,因为一旦开辟失败我们那个指针的值就会变成空指针,这就导致了我们的空间不仅开辟失败了,而且原来的那个空间我们还找不到了,这就会导致内存泄漏这种问题的发生,那么我们可以看看下面的这个例子:先用malloc开辟20字节的空间,然后再用realloc将这个空间放大到40个字节,那么这里我们的代码就如下:

int main()
{
	int* p = (int*)malloc(sizeof(int) * 5);
	if (p == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	for (int i = 0; i < 4; i++)
	{
		*(p + i) = i;
	}
	int* pa = (int*)realloc(p, 40);
	if (pa == NULL)
	{
		printf("realloc fail\n");
		exit(-1);
	}
	p = pa;
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}	
	free(p);
	p = NULL;
	pa = NULL;
	return 0;
}

这段代码运行的结果就如下:
在这里插入图片描述
当然我们这里是放大的情况,我们的realloc还可以将空间缩小,那么这里我们就不多说了,大家自己下去尝试一下。

free

在这里插入图片描述
当我们在堆中申请了空间之后,如果有朝一日我们不想使用这个空间之后,我们就可以通过free函数将这个空间释放掉,其形式就是在free函数中的括号里面写进你想要释放的空间的地址。这里就不多讲了因为上面的例子中有,那么以上的几个函数就是我们的c语言中的向堆中申请空间,那我们c++又是如何来向堆中申请空间的呢?我们接着往下看。

c++中向堆中申请空间的形式

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

new的介绍

在c语言中我们是通过函数来向堆中申请,但是在c++当中我们是通过关键字来向空间申请的空间,其基本形式如下:

int main()
{
	int* p = new int;
	return 0;
}

new后面加个你想开辟的空间的类型,比如说整型,字符型等等,然后再用一个对应类型的指针来进行接收就行,那么这就是我们new开辟空间的形式,那么这种形式他是不会对我们开辟的空间进行初始化的,我们可以通过内存来看到这里p所指向的内容是一个随机值:
在这里插入图片描述
所以当我们用这种形式来进行初始化的话是不会对开辟出来的空间进行初始化的,所以如果你想要对其进行初始化的话我们就可以在new的类型后面加上一个括号,在括号里面输入你想要初始化的值,比如说这样:

int main()
{
	int* p = new int(10);
	return 0;
}

这样的话我们再来看看内存中的值就可以发现堆上对应的那个空间就被初始化成为了10:
在这里插入图片描述
当然在我们实际使用的过程中我们不仅仅只创建一个整型的空间,有时候我们还要创建一个数组,这个数组中含有100个整型,那么面对这种情况我们就得在类型的后面加上一个方括号,在方括号里填入你想要开辟类型大小的个数,比如说我想要开辟10个整型大小的空间,那么我们这里就得在方括号里填入10,比如说我们下面的代码:

int main()
{
	int* p = new int [10];
	return 0;
}

在这里插入图片描述
当然对于这种形式我们的编译器也是不会对这个内存中的数据做任何的初始化,如果你想要对这个内存中的数据进行初始化的话,我们就得在后面加个花括号,在花括号里面填入你想要初始化成什么样的数据,比如说我们下面的代码:

int main()
{
	int* p = new int[10]{0,1,2,3,4,5,6,7,8,9};
	return 0;
}

在这里插入图片描述
那么以上就是我们new关键字的介绍希望大家可以理解。

delete的介绍

在c语言当中我们是使用free函数来释放这里申请的空间,那么在c++当中我们是使用关键字delete来释放我们这里申请的空间,那么这里的形式就是delet后面加上空间的起始地址就就可以了,比如说我们上面的代码要释放空间的形式就如下,对于单个数据就是这样的:

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

对于多个数据,我们这里的形式就得做出一些改变:在地址的前面加上一个方括号,这样就表示这里是一个连续的数据,比如说我们上面申请的数组,在释放的时候就是这样的:

int main()
{
	int* p = new int [10];
	delete[] p;
	return 0;
}

那么这里大家要注意的一点就是我们这里在释放的时候一定要做到匹配,当我们只申请了一个空间的时候我们在释放的时候就不要加上方括号,如果我们这里申请了一连续的空间的时候我们这里就必须得加方括号。如果不匹配的话我们这里就有可能报错。

new与自定义类型

对于内置内省c++的初始化方式和c语言的初始化方式没有任何的区别,但是对于自定义类型两者就会有些不同,比如说我们下面自己写了一个类:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

我们用malloc在堆上实例化出来一个对象时,我们可以看到它是不会调用这个对象中的构造函数的:

int main()
{
	A* PA = (A*)malloc(sizeof(A));
	if (PA == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	free(PA);
	return 0;
}

在这里插入图片描述
但是我们用new来实例化对象的话,我们可以看到它是会调用这个类中的构造函数和析构函数的,比如说我们下面的代码。

int main()
{
	A* PA = (A*)new A;
	delete PA;
	return 0;
}

在这里插入图片描述

new与malloc的不同

在申请或者扩容空间的时候,malloc和realloc和celloc如果申请或者扩容失败就会返回一个空指针,比如说我们下面的代码:

int main()
{
	while (1)
	{
		int* p = (int*)malloc(sizeof(int) * 1024*1024);
		if (p == NULL)
		{
			printf("malloc fail\n");
			break;
		}
		printf("success malloc\n");
	}
	return 0;
}

那么这段代码运行了一小会之后它就会打印出来malloc fail这就话,那么这就说明malloc在申请空间失败的时候确实会返回一个空指针
在这里插入图片描述
但是new却不会,当他申请空间失败的时候他会返回抛异常而不是返回一个空指针,比如说我们下面的代码:

int main()
{
	while (1)
	{
		int* p = new int[sizeof(int) * 1024 * 1024];
		if (p == NULL)
		{
			printf("malloc fail\n");
			break;
		}
		printf("success malloc\n");
	}
	return 0;
}

我们这里将malloc改成了new,同样放到一个循环里面,但是这里我们却发现在执行的过程中直到编译结束了也没有打印出来malloc fail这句话,那么这就说明new在开辟空间失败的时候是不会返回空指针的,而是会抛异常

在这里插入图片描述
我们再来看看下面的代码:

void Test()
{
	while (1)
	{
		// new失败 抛异常 -- 不需要检查返回值
		char* p1 = new char[1024 * 1024 * 1024];
		cout << (void*)p1 << endl;
	}
}

int main()
{
	try
	{
		Test();	
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

我们将这个代码运行一下就可以看到这里申请空间失败了,但是屏幕上也打印出来该错误的原因,那么这就是两者的不同:new失败 抛异常 – 不需要检查返回值。
在这里插入图片描述

定位new

我们大家都知道类中的构造函数一般都是编译器自己调用的,但是类中的析构函数却可以我们人为的手动调用,那么这里就有个问题我们能不能人为的手动的调用构造函数呢?答案是可以的,因为我们上面讲过用malloc来创建一个类的时候他是不会主动的调用构造函数和析构函数的,所以我们这里就可以使用定位new表达式在已分配的原始内存空间中调用构造函数初始化一个对象。其初始化的格式为:new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表,比如说我们下面的代码:

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
// 定位new
int main()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
	p1->~A();
	free(p1);
	return 0;
}

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

operator new与operator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是
系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过
operator delete全局函数来释放空间。所以在实际的使用过程中我们是可以直接通过operator new和operator delete来申请和释放空间的。我们来看看这两个函数的底层的实现:


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的底层实现,大家其实可以看到这里还是通过mallloc来申请的空间,我们再来看看operator delete的底层实现:

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 delete其实也是通过free来实现的功能,那么这就说明我们平时使用的new他只不过是一个封装,当我们在调用他的时候他会干两件事情一个是调用函数operator new另外一件事就是调用构造函数,当他调用operator new的时候,其实他的底层还是调用malloc函数来申请的空间,那么这里我们可以通过汇编指令来看看

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}

	int _a;
};
int main()
{
	A* pa = new A;
	delete  pa;
	return 0;
}

在这里插入图片描述
那么通过这个截图我们可以看到这个new调用了两个函数一个就是operator new,另外一个就是类中的构造函数,那么这里大家了解一下即可。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶超凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值