【 C++ 】C/C++内存管理

前言:

😘我的主页:OMGmyhair-优快云博客

目录

一、C/C++内存分布

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

malloc:

calloc:

realloc:

free: 

三、C++内存管理方式

1.用new/delete操作内置类型

2.用new/delete操作自定义类型

3.operator new和operator delete函数


一、C/C++内存分布

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

malloc:

在c语言中,我们可以使用malloc进行动态申请内存:

int main()
{
	int* p = (int*)malloc(sizeof(int) * 2);
	return 0;
}

从上面我们可以看到我们申请了8个字节大小的空间。malloc函数的返回值是void*类型的指针,指向已经开辟好的空间的首地址。我们可以通过强转,转为自己需要的类型。我们用malloc申请到的空间,里面没有初始化,值是不确定的。

如果申请空间失败,将会返回空指针。


calloc:

int main()
{
	//malloc:
	int* p = (int*)malloc(sizeof(int) * 2);

	//calloc:
	int* pc = (int*)calloc(2, sizeof(int));

	return 0;
}

在上面代码中,我们用calloc申请了2个大小为int的空间。calloc函数的返回值是void*类型的指针,指向已经开辟好的空间的首地址。它与malloc的区别在于,calloc申请到的空间每个比特位都会初始化为0。


realloc:

int main()
{
	//malloc:
	int* p = (int*)malloc(sizeof(int) * 2);

	//calloc:
	int* pc = (int*)calloc(2, sizeof(int));

	//realloc:
	pc = (int*)realloc(pc, sizeof(int) * 4);

	return 0;
}

realloc可以用来重新申请空间,第一个参数是原空间的地址,第二个参数是新内存块的大小

如果在原空间的地址上不能往后继续申请内存(后面的位置被占用了),那么realloc会重新开辟一块新内存空间,将原空间上的数据搬到新内存空间,并且对原空间进行释放。新内存空间对于原空间如果更大了,那么多出来的那部分是没有进行初始化的。

如果第一个参数是空指针,那么此时realloc的作用类似于malloc。

当realloc申请空间失败,会返回空指针,但是原空间依旧有效且数据还在。我们用realloc申请巨大的空间来模拟申请空间失败的情况:

int main()
{
	int* p = (int*)calloc(2, sizeof(int));
	cout<<"realloc前p的地址:" << p << endl;
	int* pp = p;
	p = (int*)realloc(p, sizeof(int) * 1024*1024*1024*1024);
	cout <<"realloc后p的地址:" << p << endl;
	cout << "原空间的第一个数:" << pp[0] << endl;
	return 0;
}

结果:

所以我们在使用realloc的时候,要小心申请失败的情况:

int main()
{
	int* p = (int*)calloc(2, sizeof(int));
	int* ppr = (int*)realloc(p, sizeof(int) * 24);
	if (ppr != NULL)
	{
		p = ppr;
	}
	return 0;
}

free: 

如果ptr没有指向由calloc、realloc、malloc开辟的空间,那么产生的结果是不确定的。

但如果ptr是空指针,不会做任何事情。

需要注意的是,free后不会改变ptr的值,ptr依旧指向那片空间,只是此时你在去使用这块空间是非法的。


此处插播一条知识点,为什么在32位环境下指针大小是4个字节?而在64位环境下指针大小是8个字节?

举个例子,当行李箱上假设有3位密码,每一位的范围是从0~9,我们要用多少位数可以表示全部的密码呢?答案是10^{3}

一个字节8个比特位。

首先,指针装的是地址,在32位系统下,内存地址空间大小是2^{32}。当我们表示地址时,每一位的范围是0~1,一共32个比特位,地址也就需要用32个比特位去表示,也就是4个字节。

那么64位环境下就更好理解了。在64位系统下,内存地址空间大小是2^{64}。当我们表示地址时,每一位的范围是0~1,一共64个比特位,地址也就需要用64个比特位去表示,也就是8个字节。


三、C++内存管理方式

1.用new/delete操作内置类型

int main()
{
	//动态申请一个int大小的内存空间
	int* p1 = new int;

	//动态申请一个int大小的内存空间,并且初始化为1
	int* p2 = new int(1);

	//动态申请一个3个int大小的内存空间
	int* p3 = new int[3];

	//动态申请一个3个int大小的内存空间,并且初始化为0、1、2
	int* p4 = new int[3] {0, 1, 2};

	delete p1;
	delete p2;
	delete[]p3;
	delete[]p4;
	return 0;
}

当我们申请/释放单个元素的空间时,使用new/delete。当我们申请或者释放连续的空间时,使用new[]或者delete[]。注意要搭配起来使用,如果不搭配使用的后果在文章的后面会讲到。


2.用new/delete操作自定义类型

在这里,new/delete和malloc/free的区别就明显体现出来了。

class A
{
public:
	A(int a1 = 1, int a2 = 2)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1 = 1, int a2 = 2)" << endl;
	}

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

private:
	int _a1;
	int _a2;
};

int main()
{
	cout << "malloc:" << endl;
	A* ma = (A*)malloc(sizeof(A));
	cout << "-------------------------------------" << endl;

	cout << endl << "new:" << endl;
	A* na = new A;
	cout << "-------------------------------------" << endl;

	cout << endl << "free:" << endl;
	free(ma);
	cout << "-------------------------------------" << endl;

	cout << endl << "delete:" << endl;
	delete na;
	cout << "-------------------------------------" << endl;
	return 0;
}

看看运行结果:

可以看到对比malloc,new会去调用自定义类型的构造函数。而对比free,delete会去调用自定义类型的析构函数。

对于内置类型而言new/delete和malloc/free是几乎一样的。


3.operator new和operator delete函数

new delete 是用户进行 动态内存申请和释放的操作符 operator new operator delete
系统提供的 全局函数 new 在底层调用 operator new 全局函数来申请空间, delete 在底层通过
operator delete 全局函数来释放空间。
对于自定义类型,new会进行 开空间+调用构造函数,而这里开空间用的是operator new来申请空间。
为什么不用malloc来申请空间呢?
如果malloc申请失败会返回空指针,而C++希望有另外一套执行来应对申请失败的情况:如果malloc申请空间成功就直接返回,否则执行用户的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。
这里的operator new就相当于升级版的malloc。
对于自定义类型,delete会先析构再使用operator delete进行释放空间,而operator delete的底层是_free_dbg。
这里的_free_dbg是什么呢?
首先free其实是宏函数,它的底层是_free_dbg。
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

总结一下:


4.new和delete的实现原理

(1)内置类型

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

int main()
{
	//抛异常
	try
	{
		while (1)
		{
			int* p = new int[1024 * 1024];
			int* p1 = new int[1024 * 1024];
		}
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

(2)自定义类型

new和delete对于自定义类型的原理,前面已经描述过,这里就不再赘述。

(2)-1 new [N]的原理

1.调用operator new[]函数,这里的operator new[]其实就是调用operator new实现对N个对象空间申请

2.在申请的空间上进行N次构造函数

(2)-2 delete[]的原理

1.在将要释放的对象空间上调用N个析构函数,实现对N个对象的资源清理。

2.调用operator delete[]释放空间,实际上就是调用operator delete来释放空间。


5.new/delete和new[]/delete[]错误搭配以及使用free进行释放new

(1)内置类型

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

这个代码有什么危险吗?会产生内存泄漏吗?

答案是什么都不会发生。

因为new[]归根结底还是malloc,delete归根结底也还是free。所以在这个场景下你甚至还能用free来释放空间(不推荐)。

(2)自定义类型

1.场景1

如果我们用free去释放new的空间,编译器不会报。我们也会发现free不会调用析构函数。如果我们在析构函数中有资源的释放,可能会造成内存泄漏。

2.场景2
class A
{
public:
	A(int a1 = 1, int a2 = 2)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1 = 1, int a2 = 2)" << endl;
	}

private:
	int _a1;
	int _a2;
};

int main()
{
	A* p1 = new A[3];
	delete p1;
	return 0;
}

正常通过。(注意和场景3的对比)

3.场景3
class B
{
public:
	B(int b1 = 1, int b2 = 2)
		:_b1(b1)
		, _b2(b2)
	{
		cout << "B(int b1 = 1, int b2 = 2)" << endl;
	}

	~B()
	{
		cout << "~B()" << endl;
	}
private:
	int _b1;
	int _b2;
};

int main()
{
	B* p1 = new B[3];
	delete p1;
	return 0;
}

查看运行结果:

对比与场景2,同样是用delete对new[]申请的资源进行释放,为什么B类运行时就产生了崩溃呢?

delete[]与delete的区别

我们通过比较可以发现,B跟A相比多了析构函数。当用new[]申请多个B类对象的空间时,new[]其实会在有析构函数的类前面多开出4个字节的空间来存储对象个数。因为你有析构函数,编译器怕你析构函数中有释放资源的操作,所以会记下对象的个数来多次调用析构函数避免内存泄漏。如果你没有析构函数,编译器认为没有释放资源,也就懒得给你另开空间记个数了:

A类和B类大小一样,都是8个字节,2个int成员变量。我们来看看new[]对A类开辟了多少空间,对B类又开辟了多少空间。

我们都知道不能对申请的空间只释放一部分。当B类有析构时,使用delete进行释放就是只对申请的空间释放一部分,导致了程序崩溃。所以我们在使用new/delete和new[]/delete[]时,一定要搭配使用,以免造成错误。


6.定位new表达式(placement-new)

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

使用格式如下:

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

在这里,我们用operator new或者malloc去开辟空间,这里不调用构造,用operator new不用像malloc一样检查返回值,因为operator new直接抛异常。

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

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p1 = (A*)operator new(sizeof(A));//开辟空间
	new(p1)A(1);//调用构造函数
	p1->~A();//调用析构函数
	operator delete(p1);//释放空间

	A* p2 = (A*)malloc(sizeof(A));//开辟空间
	new(p2)A();//调用构造函数
	p2->~A();//调用析构函数
	free(p2);//释放空间

	return 0;
}

你可能会觉得这不是脱裤子放屁吗?为什么不直接使用new一键开辟空间+调用构造函数呢?

这里要引入一个概念,池化技术——提高性能,像类似内存池、线程池、连接池都属于池化技术。而我们这里涉及的就是内存池。

我们可以将内存池比作一个受到管理的大湖,周围的人都需要到这里来打水。而山上有座寺庙,频繁用水经常排队。因此干脆专门给寺庙开了一个小水洼,这个小水洼供寺庙专用,当小水洼中的水不够了就可以去大湖要。而这里的内存池就是小水洼,堆就是大湖。

使用定位new表达式也是为了提高效率。


7.malloc/freenew/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 在释放空间前会调用析构函数完成
空间中资源的清理释放



如果这篇文章有帮助到你,请留下您珍贵的点赞、收藏+评论,这对于我将是莫大的鼓励!学海无涯,共勉!😘😊😗💕💕😗😊😘




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值