您现在的位置是:首页 >学无止境 >C & C++ 的内存管理(C++)网站首页学无止境

C & C++ 的内存管理(C++)

楠舍! 2024-07-15 06:01:02
简介C & C++ 的内存管理(C++)

目录

C / C++ 的内存分布

C / C++ 程序内存区域划分:​

C语言内存管理

C中动态内存管理方式:

C++内存管理

C++内存管理的方式:

new / delete 操作内置类型

new 和 delete 操作自定义类型

new 和 delete 与 malloc 和 free 的区别:

operator new 与 operator delete 函数

operator new 和 operator delete 函数

new和delete的实现原理

内置类型

自定义类型

定位new表达式(placement-new)

常见面试题

malloc/free 和 new/delete的区别?

内存泄露

什么是内存泄露,内存泄露的危害?

内存泄露分类

堆内存泄露

系统资源泄露

如何检测内存泄露

如何避免内存泄露


C / C++ 的内存分布

C / C++ 程序内存区域划分:


说明:

1、栈,又被叫做堆栈,里面存放 函数栈帧,非静态局部变量/函数参数/返回值……等。栈是向下增长的(所谓向下增长,就是从高地址往低地址处存数据)

2、内存映射区是高效的IO映射方式,用于装载一个动态的内存库,用户可以使用系统接口,创建共享内存,做进程间的通信!

3、堆,用于程序运行时动态内存分配,堆是向上增长的!

4、数据段,存储全局数据和静态数据

5、代码段,可执行的代码 或 只读变量


C语言内存管理

C中动态内存管理方式:

C中用动态开辟空间函数来管理动态内存(详解可看主页C语言动态内存管理文章)

函数分别是:malloc 、calloc、realloc、free 

下面看看它的应用实例(用C实现一个栈)


代码:


//C语言实现栈
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>

typedef int DateType;

typedef struct Stack
{
	DateType* _a;
	int size;
	int capacity;
}ST;

//初始化栈
void InitStack(ST* ps)
{
	//开空间
	ps->_a = (DateType*)malloc(sizeof(DateType) * 4);
	if (ps->_a == NULL)
	{
		perror("malloc tail:");
		return;
	}
	ps->size = 0;
	ps->capacity = 4;
}

//扩容
void ChilckCapacity(ST* ps)
{
	if (ps->size == ps->capacity)
	{
		//进行增容
		DateType* tmp = (DateType*)realloc(ps->_a, sizeof(DateType) * ps->capacity * 2);
		if (NULL == tmp)
		{
			perror("realloc tail ");
			return;
		}
		ps->_a = tmp;
		ps->capacity *= 2;
	}
}

//入栈
void PushStack(ST* ps,DateType x)
{
	//扩容
	ChilckCapacity(ps);

	//添加数据
	ps->_a[ps->size++] = x;
}

//判断栈是否为空
bool EmptyStatic(ST* ps)
{
	return ps->size == 0;
}

//出栈
void PopStack(ST* ps)
{
	//判断栈是否出空了
	if (EmptyStatic(ps))
	{
		printf("栈为空了,不可再出了
");
		return;
	}

	--ps->size;
}

//返回栈顶元素
DateType TopStack(ST* ps)
{
	//栈为空就提示
	assert(!EmptyStatic(ps));
	return ps->_a[ps->size - 1];
}

//栈元素个数
int SizeStack(ST* ps)
{
	return ps->size;
}

//栈的销毁
void DestoryStack(ST* ps)
{
	free(ps->_a);
	ps->_a = NULL;
	ps->size = 0;
	ps->capacity = 0;
}

int main()
{
	ST st;

	//初始化栈
	InitStack(&st);

	//入栈
	PushStack(&st, 1);
	PushStack(&st, 2);
	PushStack(&st, 3);
	PushStack(&st, 4);

	//出栈
	PopStack(&st);

	//返回栈顶元素
	printf("%d
", TopStack(&st));

	//入栈
	PushStack(&st, 5);

	//返回栈顶元素
	printf("%d
", TopStack(&st));

	//栈里面元素的个数
	printf("Stack Size = %d
", SizeStack(&st));

	//入栈
	PopStack(&st);
	PopStack(&st);
	PopStack(&st);
	PopStack(&st);
	PopStack(&st);
	//printf("%d
", TopStack(&st));

	//销毁栈
	DestoryStack(&st);

	return 0;
}

上述代码是C语言对动态内存的管理方式(利用malloc,calloc,realloc 函数管理)


C++内存管理

C++内存管理的方式:

因为C++兼容C语言,C语言中的内存管理的方式在C++中是可以继续使用的。但在有些场景下C语言管理内存的方式是无能为力的,而且使用起来非常麻烦,所以C++提出了自己的一套内存管理的方式!

C++通过 new 和 delete 操作符进行动态内存管理!


new / delete 操作内置类型

new 和 delete:

在C++中,new 是用来开辟空间的,delete是用来释放空间的!


写法:

申请单个类型的空间:

1、申请空间不初始化

        指针变量 = new 类型;

2、申请空间并初始化

        指针变量 = new 类型(空间的初始值);


申请多个空间

1、申请空间不初始化

        指针变量 = new 类型[个数];

2、申请空间并初始化

        指针变量 = new 类型[个数]{初始化内容};


释放单个类型空间:

        delete 指针变量;

释放多个空间:

        delete[] 指针变量;


代码:

#include <iostream>
using namespace std;

int main()
{
	//动态申请一个int类型的空间
	int* p = new int;
	cout << *p << endl;//打印,不初始化是随机值
	delete p; //释放该空间


	//动态申请一个int类型的空间并初始化
	int* p1 = new int(3);
	cout << *p1 << endl;//打印
	delete p1;//释放该空概念


	//动态申请10个int类型的空间
	int* p2 = new int[10];
	//利用for进行打印
	for (int i = 0; i < 10; i++)
	{
		cout << p2[i] << " ";//不初始化是随机值
	}
	cout << endl;
	delete[] p2; //释放该空间


	//动态申请10个int类型的空间并初始化
	int* p3 = new int[10]{ 1,2,3,4,5,6,7,8,9,10 };
	//利用for进行打印
	for (int i = 0; i < 10; i++)
	{
		cout << p3[i] << " ";//不初始化是随机值
	}
	cout << endl;
	delete[] p3;//释放该空间

	return 0;
}

注意:

在C++中申请和释放单个元素的空间,直接用new 和 delete 。申请和释放连续的多个空间,使用 new[] 和 delete[] 。一定要相互匹配使用!


理解代码:

#include <iostream>
using namespace std;

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

	// 动态申请一个int类型的空间并初始化为10
	int* ptr5 = new int(10);

	// 动态申请10个int类型的空间
	int* ptr6 = new int[3];

	//进行释放
	delete ptr4;
	delete ptr5;
	delete[] ptr6;

	return 0;
}


注意:

C++兼容C语言,C++中虽然可以继续使用C语言管理内存的方式。但C++有自己的一套管理内存的方式,切记两者不能混合使用,否则会出现不可预测的结果! 


new 和 delete 操作自定义类型

new 和 delete 与 malloc 和 free 的区别:

区别:

对于自定义类型而言,new 和 delete   在申请和释放空间的时候会去调用自定义类型的构造函数和析构函数(new 调用构造函数,delete调用析构函数)。而 malloc 和 free 则不会。


对于内置类型而言,两者是没有任何区别的,只是动态申请空间!


代码:

#include <iostream>
using namespace std;
 
class A
{
public:
	//构造函数 
 A(int a = 0)
 : _a(a)
 {
 cout << "A():" << this << endl;
//注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
 }
 
 //析构函数 
 ~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;
 
 // 内置类型是几乎是一样的
 int* p3 = (int*)malloc(sizeof(int)); // C
 int* p4 = new int;
 free(p3);
 delete p4;

 A* p5 = (A*)malloc(sizeof(A)*10);
 A* p6 = new A[10];
 free(p5);
 delete[] p6;
 
 return 0;
}

operator new 与 operator delete 函数

operator new 和 operator delete 函数

operator new 和 operator delete :

new和delete 是用户用来动态内存申请和释放的操作符(注意new 和 delete是操作符)

operator new 和 operator delete 是两个全局函数,并不是重载!


底层关系:

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


operator new  的实现:

/*
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)
比特就业课
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果
malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施
就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
5. new和delete的实现原理
5.1 内置类型
     if (_callnewh(size) == 0)
     {
         // report no memory
         // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
         static const std::bad_alloc nomem;
         _RAISE(nomem);
     }
return (p);
}

operator delete 的实现:

/*
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 申请空间成功就直接返回,否则执行用户提供的空间不足应对政策,如果用户提供政策继续申请,否则就抛异常。operator delete 是通过 free 来释放空间的


new和delete的实现原理

内置类型

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

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


代码:

#include <iostream>
using namespace std;

int main()
{
	//对于内置类型,new和malloc delete 和free 是类似的

	int* ps = (int*)malloc(sizeof(int));
	free(ps);

	int* p = new int;
	delete p;

	char* pps = (char*)malloc(sizeof(char) * 10);
	free(pps);

	char* pp = new char[10];
	delete[] pp;

	//new/delete申请和释放的是单个元素的空间,
	//new[]和delete[]申请和释放的是连续空间

	return 0;
}

自定义类型

new的原理:

        1、调用operator new函数申请空间

        2、在申请空间之后调用构造函数,完成对象的构造


delete的原理:

        1、在空间上执行析构函数,完成对象的资源清理

        2、调用operator delete 进行释放对象空间


new T[N] 的原理:

        1、调用operator new[]  函数,在operator new[] 中实际调用operator new 函数完成N个对象空间的申请

        2、在申请的N个对象空间上,对每个对象都执行一次构造函数,总共执行N次构造函数


delete[] 的原理:

        1、在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

        2、调用operator delete[]  来释放空间,实际在operator delete[] 中调用operator delete 来进行释放空间!



代码:

#include <iostream>
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year=0,int month=0,int day=0)
		:_year(year),
		_month(month),
		_day(day)
	{
		cout << "Date(int year=0,int month=0,int day=0)" << endl;
	}

	//析构
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//自定义类型开空间
	// 对于new来说先调用opertor new 函数开空间
	// 再调用构造函数
	// operator new 本质上还是用malloc 来开空间
	// 对于delete 先调用 析构函数
	//再调用operator delete 函数进行释放空间
	//operator delete 实际上是调用free 来进行释放空间!
	Date* p = new Date;
	delete p;

	return 0;
}

定位new表达式(placement-new)

概念:

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


使用格式:

new(place_address)type 或者 new (place_address) type(initializer-list)

place_address 必须是一个指针,inializer_list 是类型的初始化列表


使用场景:

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


说明:

    实际使用情况下定位new表达式是与内存池配合使用的
    内存池也属于堆区,是在堆区提前拿到一块空间,供用户使用,
    加强了程序的效率,速度快,用户后续开空间就直接可以在内存池中使用空间
    内存池中的空间对于自定义类型是不会自动调用析构和构造函数的
    此时我们就可以用到定位new表达式进行显式的调用构造和析构!
    p1现在指向的只不过是与A对象相同大小的一段空间,
    还不能算是一个对象,因为构造函数没有执行


代码:

#include <iostream>
#include <string.h>
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()
{
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;  // 通过定位new表达式显式的调用构造函数!
	//显示的调用析构函数
	p1->~A();
	free(p1);

	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);//利用定位new表达式显式的调用构造函数,并且给构造函数传参

	//显式的调用析构函数
	p2->~A();
	operator delete(p2);

//实际使用情况下定位new表达式是与内存池配合使用的
// 内存池也属于堆区,是在堆区提前拿到一块空间,供用户使用,
// 加强了程序的效率,速度快,用户后续开空间就直接可以在内存池中使用空间
// 内存池中的空间对于自定义类型是不会自动调用析构和构造函数的
// 此时我们就可以用到定位new表达式进行显式的调用构造和析构!
// p1现在指向的只不过是与A对象相同大小的一段空间,
//还不能算是一个对象,因为构造函数没有执行

	return 0;
}

常见面试题

malloc/free 和 new/delete的区别?

解答:

malloc/free 和 new/delete 的共同点:

都是从堆上申请空间,并且需要用户手动释放!


malloc/free 和 new/delete 的不同点:

1、malloc / free 是函数,new / delete 是操作符

2、malloc / free 申请的空间不会初始化,而new / delete 可以初始化

3、malloc 申请空间时,需要手动计算空间大小并传递,new只需要在后面跟上空间的类型即可,如果是多个对象 [] 中指定对象个数即可!

4、malloc的返回值为void*,在使用时必须强转,new不需要,因为new后面跟的是空间类型!

5、malloc 申请空间失败,返回的是NULL,因此使用时必须判空,new不需要判断,但是要捕获异常!

6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理


内存泄露

什么是内存泄露,内存泄露的危害?

什么是内存泄露:

内存泄露指的是,因为疏忽或错误造成程序未能释放已经不再使用的内存情况,内存泄露并不是指内存物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费!(内存泄露就是空间申请之后不释放,且后续不能再进行释放的情况!)


内存泄露的危害:

长期运行的应用程序出现内存泄露,影响很大,如操作系统、后台服务器等等。出现内存泄露会导致响应越来越慢,最终卡死!(若程序不长期运行,程序结束操作系统会自动进行释放内存,即处理内存泄露问题)


代码:

#include <iostream>
using namespace std;

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;
}

int main()
{
	return 0;
}

内存泄露分类

堆内存泄露

堆内存泄露指的是程序执行中需要通过malloc / realloc / calloc / new 等从堆中分配的一块内存,用完后必须通过相对应的free或delete释放掉,假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法被使用,会出现heap leak(大量泄露)


系统资源泄露

指程序使用系统分配的资源,比方套接字,文件描述符、管道……等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定!


如何检测内存泄露

在VS下使用Windows操作系统提供的_CrtDumpMemoryLeaks()函数进行简单检测,该函数只报出了大概泄露了多少个字节,没有其它更准确的位置信息


代码:


注意:

写代码一定要小心,尤其是动态内存操作时一定要记着释放,但有些情况下还是防不胜防,简单的泄露可以采用上述方式快速定位判断。如果工程比较大,内存泄露位置比较多, 不太好查时,一般都是借助第三方内存泄露检查工具来处理的!


内存泄露检测工具:

在Linux下的内存泄露检测Linux下几款内存泄露的工具

在Windows下使用第三方工具:VLD内存泄露库

其它工具:内存泄露检测工具


如何避免内存泄露

养好良好的编程习惯,有申请就要有对应的释放,用智能指针预防内存泄露,或者用检测泄露的工具进行查询!


总结:

1、事前预防型,如智能指针等!

2、事后查错型,如泄露工具!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。