C/C++内存管理

一、C/C++内存分布

  1. C/C++中定义的变量在内存中分布

eg:

int globalVar = 1;	
int main()
{
	static int staticVar = 1;
	int lovalVar = 1;

	int num1[10] = { 1, 2, 3, 4 };

	//给char2数组初始化
	char char2[] = "abcd";

	//const不影响存储区域,但是不+const会导致权限放大,程序出错
	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);

	return 0;
}

解析上面的代码几处容易判断错的地址:
解析代码

  1. 内存区域划分
功能内存
非静态局部变量,函数参数,返回值…向下增长(内存地址减小的方向)
动态内存分配内存向上增长
数据段存储全局数据和静态数据
代码段可执行代码/只读常量
内存映射段高效的I/O映射方式——共享区(动态库,共享内存等等)

内存映射段包含的东西了解一下即可,而且比较分散,我后面写的动态库加载博客进程间通信的共享内存都有涉及,甚至还有线程的栈也在这个区域

二、C/C++动态内存管理方式

1. C动态内存管理

方式: malloc/calloc/realloc/free

  • malloc:开空间
  • calloc:开空间+初始化为0
  • realloc:扩充空间

eg:

void Test()
{
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);

	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	free(p3);
}

注意
1.异地扩容:代码中p2不需要释放,realloc扩容完成(把内容复制到新的空间)后会释放p2的内容。(p2是野指针)
2.原地扩容:p2和p3指向同一块空间。

2. C++动态内存管理

C++的内存管理方式:new/delete操作符

  1. 申请和释放单个元素的空间,使用new和delete操作符
  2. 申请和释放连续的空间,使用new[]和delete[]

注意:匹配使用

使用:

  1. new/delete对内置类型的操作

eg1:

int main()
{
	//动态申请一个int类型的空间
	int* ptr1 = new int;

	//动态申请一个int类型的空间,并初始化为10
	int* ptr2 = new int(10);
	std::cout << *ptr2 << std::endl;

	//动态申请10个int类型的空间
	int* ptr3 = new int[10];
	for(int i = 0; i < 10; i++)
	{
		cout << *(ptr3+i) << " ";
	}
	cout << endl;

	//把动态申请10个int类型的空间的前3个空间按照{}(代码块)中的内容初始化
	//剩下的空间会自动初始化成0
	int* ptr4 = new int[10]{ 1,2,3 };
	for(int i = 0; i < 10; i++)
	{
		cout << *(ptr4+i) << " ";
	}
	cout << endl;

	delete ptr1;
	delete ptr2;
	delete[] ptr3;
	delete[] ptr4;

	return 0;
}

//运行结果:
//10
//0 0 0 0 0 0 0 0 0 0 
//1 2 3 0 0 0 0 0 0 0 

解析主要部分的代码:
解析代码

  1. new/delete对自定义类型的操作

eg2:

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

private:
	int _a;
};

void Test()
{
	//new和delete对于自定义类型,除了开空间还可以调用构造函数和析构函数
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);
	free(p1);
	delete p2;


	//如果是多参数,且没有默认构造,就要补充所有参数。有默认构造可以根据需要,可以不补齐参数
	A* p3 = new A[4]{ 1, 2, 3, 4 };
	A* p4 = new A[4]{ A(1), A(2), A(3) }; //A(1)-->匿名对象,这里可以不补齐参数
	

	delete[] p3;
	delete[] p4;
}

注意: 在申请自定义类型的空间是,new会调用构造函数,delete会调用析构函数,而malloc和free则不会。内置类型两者几乎一样

三、new和delete的实现原理

1. operator new与operator delete

  • new和delete是用户动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。
  • new在底层调用operator new全局函数来申请空间,delete在底层调用operator delete全局函数来释放空间
  1. operator new:该函数实际通过malloc来申请空间, 申请空间失败,可以执行相应的措施(前提用户设置了。没有设置措施就抛异常,异常在该函数内部设置的)。所以malloc申请空间失败则返回一个错误码 new开辟空间失败就会抛异常。
  2. operator delete:最终是通过free来释放空间。

eg:

int main()
{
	int* p1 = nullptr;
	try       
	{
		do
		{
			//p1 = (int*)malloc(1024 * 1024);
			p1 = new int[1024 * 1024 * 1024];
			cout << p1 << endl;
		} while (p1);
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

//运行结果:
//std::bad_alloc     //抛异常了

2. 实现原理

  1. 对自定义类型实现原理图解:
    new和delete实现原理

文字叙述:

  1. new的原理
    • 先调用operator new函数申请空间
    • 在申请空间后执行构造函数,完成对象的构造
  2. delete的原理
    • 先执行析构函数,完成对象资源的清理工作
    • 然后调用operator delete函数释放对象的空间
  3. new T[N]的原理
    • 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
    • 在申请的空间上执行N次构造函数
  4. delete[]的原理
    • 在释放的空间上执行N次析构函数,完成N个对象中资源的清理
    • 调用operator delete[]函数,实际在operator delete[]中调用operator delete来释放空间
  1. 对内置类型:new/delete和mallco/free基本类似。就new申请失败抛异常,而malloc返回NULL

四、定位new表达式

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

eg:

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

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

private:
	int _a;
};

void Test()
{
	//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);
}

五、内存泄漏

1. 认识内存泄漏

  • 内存泄漏:忘记释放不再使用的内存情况。程序的设计错误失去了对该段内存的控制,造成内存浪费
  • 危害:长期运行的程序出现内存泄漏,可能导致响应越来越慢,最终卡死

eg:这两种情况都是内存泄漏,当然不止这两种情况

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int *p1 = (int *)malloc(sizeof(int));
	int *p2 = new int;

	// 2.异常安全问题
	int *p3 = new int[10];
	Func(); // 假如这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	delete[] p3;
}

2. 内存泄漏分类

C/C++程序中一般关心两方面的内存泄漏:

  1. 堆内存泄漏(Heap leak)
    通过malloc/calloc/realloc/new等方式从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假如程序的设计错误导致这部分内存没有被释放,这部分空间将没有再被使用,就会产生Heap Leak。
  2. 系统资源泄漏
    程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

3. 避免内存泄漏

  1. 工程有良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。

3. 简要介绍堆栈

  1. 对于堆来说,频繁的申请和释放空间,容易造成内存碎片,甚至内存泄漏。栈区由于自动管理,不存在此问题。
  2. 堆的大小受操作系统的限制,栈的大小一般较小,栈空间一般由系统直接分配。
  3. 32位系统下,最大的访问内存空间为4G,所以不可能把所有的内存空间当做堆内存使用。所以堆的内存在这是不是4G。
  4. 堆无法静态分配,只能动态分配。
    • 静态分配(分配的内存空间大小是固定的,适用于存储固定大小的数据结构):指在编译时就确定变量的内存分配位置,而堆的内存分配是在运行时根据需要进行的。
  5. 栈可以通过函数_alloca进行动态分配,不过分配的空间不能通过free或delete释放。

总结

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

  1. 特性、用法上的区别:

    • malloc和free是函数,new和delete是操作符
    • malloc申请的空间不会初始化,new可以初始化
    • malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
    • malloc的返回值为void*, 使用时必须强转,new不需要,因为new后跟的是空间的类型
    • malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  2. 底层的区别:

    • 申请自定义类型对象时,malloc/free只会开辟空间。而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

kpl_20

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

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

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

打赏作者

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

抵扣说明:

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

余额充值