C++——内存管理

1. C/C++内存分布

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

一段代码的相关问题:

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

1. 选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?____
*pChar3在哪里?____
ptr1在哪里?____
*ptr1在哪里?____

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

重点:malloc/calloc/realloc的区别

realloc有两种扩容方式:原地扩容和异地扩容

验证realloc是异地扩容还是原地扩容,代码如下:

cpp 复制代码
int main()
{
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int)*10);

	cout << p2 << endl;
	cout << p3 << endl;

	return 0;
}

运行结果:

calloc和realloc指向的是不是同一块空间,所以就是异地扩容,地址是一样的话就是原地扩容。

3. C++内存管理方式

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

3.1 new/delete 操作内置类型

cpp 复制代码
void Test()
{
	//动态申请一个int类型的空间  --不会初始化的
	int* ptr4 = new int;
	

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

	//动态申请3个int类型的空间 --多个对象
	int* ptr6 = new int[8];

	//多个对象的初始化
	int* ptr7 = new int[8] {1, 2, 3};  //前3个初始化为1 2 3,后5个默认给0

	delete ptr4;
	delete ptr5;

	delete[] ptr6;
	delete[] ptr7;
}

调试结果:

注意:申请和释放单个元素的空间,使用new和delete操作符;申请和释放连续的空间,使用的是new[ ]和delete[ ],注意:需要匹配起来使用。混着使用会出现错误,无法达到预期的效果。

C++中的new/delete:

1.new/delete是操作符;C语言是一个函数
2.new是不需要强转类型的;C语言对应类型是 void* 需要进行强转
3.不用程序员的去计算需要的字节数,编译器自行计算,多个对象只需要给个数就可以了;C语言需要程序员去计算需要多少字节,什么类型
4.初始化多元化,单个对象的初始化(用"( )")和多个对象的初始化(用"{ }");C语言只能通过calloc来进行初始化
5.C++是通过抛异常;C语言是要检查返回值的,但是一般都不会失败(堆空间较大)

抛异常的代码:

cpp 复制代码
double Divide(int a,int b)
{
	try
	{   //当b==0时抛出异常 
		if (b == 0)
		{
			string s("Divide by zero condition!");
			throw s;
		}
		else
		{
			return ((double)a / (double)b);
		}
	}
	catch(int errid)
	{
		cout << errid << endl;
	}
	return 0;
}

对异常进行进一步的处理:捕获异常,如果一场没有被捕获的话就会报错,代码如下:

cpp 复制代码
double Divide(int a,int b)
{
		//当b==0时抛出异常 
		if (b == 0)
		{
			string s("Divide by zero condition!");
			throw s;
		}
		else
		{
			return ((double)a / (double)b);
		}


	return 0;
}

int main()
{
	try
	{
	int len, time;
	cin >> len >> time;
	
	cout << Divide(len, time) << endl;
	}
	catch(const string& s)//抛出什么类型就捕获什么类型
	{
		cout << s << endl;
	}
}

抛异常直接到catch,没有抛异常是不走catch。

写一个程序,看一下堆上面到底能申请多少次的4MB空间的开辟:

cpp 复制代码
void Func()
{
	int i = 1;
	while (1)
	{
		int* p1 = new int[1024 * 1024]; //4MB

		cout << i << "->" << p1 << endl;
		i++;
	}

}

int main()
{
	try
	{
		Func();

	}
	catch(const std::exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果:

计算一下大概可以开多少的空间:

堆的空间接近两个G(2048MB)的空间,32位下整个的空间大小差不多才4G(2^32字节)。

3.2 new和delete操作自定义类型

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

	}

private:
	int _a;
};

int main()
{
	//malloc单独的开辟空间
	A* p1 = (A*)malloc(sizeof(A));
	if(p1==nullptr)
	{
		//....
	}

	//区别:
	// 内置类型是可以初始化的(显示写值才初始化)
	// new对于自定义类型是会调用构造函数(一定)初始化
	A* p2 = new A;
	free(p1);
	delete p2;//调用析构函数

	return 0;
}

运行结果:

new/delete对于内置类型相比C语言来说只有用法上的区别,功能上区别不大;对于自定义类型new/delete就有本质区别了,new/delete除了开空间还在调用构造函数和析构函数,malloc/free只是单纯的开空间,除了用法上在功能上也有本质的区别了。

多个对象会多次调用构造函数:

cpp 复制代码
int main()
{
	
	A* p1 = (A*)malloc(sizeof(A));
	if(p1==nullptr)
	{
		//....
	}

	A* p2 = new A;
	free(p1);
	delete p2; //调用析构函数

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

	return 0;
}

运行结果:

new调用的是默认构造函数,如果没有实现默认构造函数(显式写无参、显式写全缺省、编译器自己生成)就会报错。

没有提供默认构造函数:

cpp 复制代码
class A
{
public:
	A(int a )  //不是默认构造
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;

	}

private:
	int _a;
};

int main()
{
	//malloc单独的开辟空间
	A* p1 = (A*)malloc(sizeof(A));
	if(p1==nullptr)
	{
		//....
	}

	//区别:
	// 内置类型是可以初始化的(显示写值才初始化)
	// new对于自定义类型是会调用构造函数(一定)初始化
	A* p2 = new A(1);  //new提供显示传参
	free(p1);
	delete p2; //调用析构函数

	A* p5 = (A*)malloc(sizeof(A) * 10);

	//A a1(1), a2(2), a3(3);
	//A* p6 = new A[3]{ a1,a2,a3 };//传A对象初始化

	A* p6 = new A[3]{ 1,2,3 };//单个参数构造函数支持隐式类型转换
	free(p5);
	delete[] p6;

	return 0;
}

运行结果:

多个参数构造函数支持隐式类型转换:

cpp 复制代码
class A
{
public:
	A(int a1,int a2) // 无默认构造
	: _a1(a1)
	, _a2(a2)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;

	}

private:
	int _a1;
	int _a2;
};

int main()
{

	A* p1 = (A*)malloc(sizeof(A));
	if(p1==nullptr)
	{
		//....
	}

	A* p2 = new A(1, 1);  //new提供显示传参
	free(p1);
	delete p2; //调用析构函数

	A* p5 = (A*)malloc(sizeof(A) * 10);

	//A a1(1), a2(2), a3(3);
	//A* p6 = new A[3]{ a1,a2,a3 };//传A对象初始化

	//A* p6 = new A[3]{ 1,2,3 };//单个参数构造函数支持隐式类型转换
	A* p6 = new A[3]{ {1,1},{2,2},{3,3} };//多个参数构造函数支持隐式类型转换
	free(p5);
	delete[] p6;

	return 0;
}

运行结果:

cpp 复制代码
A* p6 = new A[10]{ {1,1},{2,2},{3,3} };

这样是会报错的:

原因是当后面初始化的值的个数小于[ ]里面的元素的个数的时候就会报错,没写的话后面默认是默认构造给的值但是我们有没有实现默认构造所以就会报错,在这里就强调了默认构造的重要性, 所以加上默认构造编译才能通过**:**

提供了默认构造,以下的写法也是可以的:

cpp 复制代码
A* p6 = new A[10];

调试结果:

如果没有提供默认构造,就只有带参的构造只有显式写了,自定义类型如果没有默认构造,new之后就必须要初始化,不初始化的话就会报错,有默认构造就调用默认构造。C++中new操作之后对于自定义类型一定要调用构造,比起C语言最大最大的区别。

推荐使用new/delete的原因:

1.new/delete用法比起C语言更简单
2. new/delete支持的功能更健全,更好用
3.提供默认构造调用默认构造,没有默认构造必须得传参,调用传参的构造

4. operator new 和 operator delete 函数(重要点进行详解)

operator new 和 operator delete 函数不是对new和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)
{  
//返回值:void*          参数:字节数 size_t size

	// 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就是封装的malloc:

cpp 复制代码
/*
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;
}

其实operator delete就是封装的free:

cpp 复制代码
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

operator new 和 operator delete其实跟malloc和free的用法一样,其次它们是malloc和free的封装,operator new 和 operator delete是可以直接使用的,跟malloc/free的区别:没有什么区别,唯独只有一点点区别,operator new是抛异常,捕获异常;不返回空指针,不用检查返回值。虽然operator new 和 operator delete是可以直接使用的但是我们一般不使用,一般使用new和delete

5.new和delete的实现原理

5.1内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本相似,不同的地方是:

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

5.2 自定义类型

  • new的原理

1.调用operator new函数申请空间
2.在申请的空间上执行构造函数,完成对象的构造

  • delete的原理

1.在空间上执行析构函数,完成对象中资源的清理工作
2.调用operator delete函数释放对象的空间

  • new T[N]的原理

1.调用operator new[ ]函数,在operator new[ ]中实际调用那个operator new函数完成N个对象空间的申请
2.在申请的空间上执行N次构造函数

  • delete[ ]的原理

1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2.调用operator delete[ ]释放空间,史记载operator delete[ ]中调用operator delete来释放空间

cpp 复制代码
class A
{
public:
	//A(int a1,int a2) // 无默认构造
	A(int a1 = 0, int a2 = 0)
		: _a1(a1)
		, _a2(a2)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;

	}

private:
	int _a1;
	int _a2;
};

int main()
{
	//new和delete的实现原理
	//A* p2 = (A*)operator new(sizeof(A));

	//operator delete(p2);

	//new 核心操作: operator new(不直接使用malloc)+构造函数
	A* p2 = new A(1, 1);

	//delete 核心操作:析构函数+operator delete
	delete p2;

	return 0;
}

不使用malloc而是使用operator new 的原因是new失败了抛异常,而malloc失败是返回空。

cpp 复制代码
A* p2 = new A(1, 1);  反汇编
cpp 复制代码
delete p2;  反汇编
cpp 复制代码
A* p3 = new A[10];  多个对象

不再调用一个构造函数,而是要调用多个构造函数。

cpp 复制代码
delete [] p3;   多个对象

不再调用一次析构函数,而是要调用多个析构函数。

匹配问题:(一定要匹配,有时候要报错,有时候不报错)

cpp 复制代码
	//1.  没有报错
	A* p5 = (A*)malloc(sizeof(int) * 10);
	delete p5;
cpp 复制代码
	//2.  没有报错
    int* p6 = (int*)malloc(sizeof(int) * 10);
	delete p6;

上面2种情况都没有问题:

cpp 复制代码
	//3. 没有内存泄漏
	int* p7 = new int[10];
	delete p7;
cpp 复制代码
	//4.
	A* p8 = new A[10];
	delete p8;

内置类型没有问题,但是自定义类型就有问题:

分析:

int* p7 = new int[10];

A* p8 = new A[10];

一个A对象8个字节,申请了10个A对象,应该是80个字节。

多开辟了4个字节,是为了存储[ ] 中的个数,原因:

A* p8 = new A[10]; delete p8;这样写的话:

1.首先析构函数调用的次数不够,导致内存泄漏(内存泄漏不报错)。

内存泄漏不报错:

2.其次delete p8;释放的位置不对。 直接就在p8进行释放,因为没有记录个数也不会多开4个字节,但是释放内存是不能从中间释放的所以这个时候就报错了。(报错的实际原因是:释放的位置不对)

当我们将析构函数不显式写的话,再次运行下面的这行代码:

cpp 复制代码
	//~A()
	//{
	//	cout << "~A():" << this << endl;

	//}
cpp 复制代码
	A* p8 = new A[10];
    delete p8;

运行是不会报错的

为什么?

编译器进行了优化:只申请了80个字节

A类型没有显示的实现析构函数,默认的析构函数没有什么指向的资源,优化之后就没有掉调用析构函数,不用多开4个字节,释放的位置就额是对的,就不会报错。不过最好不要这样写,因为是再编译器进行优化的情况下才不会报错,不同的编译器,进行优化是不确定的,本质上还是一个错误程序

cpp 复制代码
	//~A()
	//{
	//	cout << "~A():" << this << endl;

	//}

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

这个时候是没有内存泄漏的,没调用析构函数没问题。delete p8;只释放了1个A对象?

并不是只释放了1个A对象,A* p8 = new A[10]; -> 底层是由malloc开辟的,delete p8;->底层释放空间是free,malloc一块空间free掉是没有问题的,malloc和free底层也会记录这块指针指向的空间是多大。

delete[ ] 和 new[ ] 严格来说并不会关联底层的申请空间和释放空间,关联的是调用多少次析构函数和构造函数。

总结:

new/delete、new[ ]/delete[ ]不要乱匹配,否咋就会出现各种问题。

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

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

	}

private:
	int _a;
};
cpp 复制代码
int main()
{
	//要求不能用new来开辟空间
	A* p1 = (A*)operator new(sizeof(A));

	return 0;
}

此时的 p1 只开了空间,没有初始化,没办法初始化因为成员变量是私有的,没办法显示的去初始化,调构造,但是构造都是定义对象的时候自动调用的,写一个 Init( ) 函数, Init( )函数跟A的构造函数的功能是重叠的,是可以的。还有其他方式:定位new。

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

使用格式:

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

cpp 复制代码
int main()
{
	//要求不能用new来开辟空间
	A* p1 = (A*)operator new(sizeof(A));
	//new(p1)A;//默认构造
	new(p1)A(10);//带参构造

	//构造函数是不能显式调用
	//p1->A();//不支持

	//析构函数是可以显式调用的

	p1->~A();
	operator delete(p1);

	return 0;
}

调试结果:

平时是不会使用定位new的,定位new真正的意义是什么?跟内存池相结合起来使用。

使用场景:

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

new/delete是直接像系统申请内存,定位new不是开空间,是显示调用构造函数。

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

相关推荐
勇闯逆流河3 小时前
【C++】用红黑树封装map与set
java·开发语言·数据结构·c++
山,离天三尺三3 小时前
深度拷贝详解
开发语言·c++·算法
我狸才不是赔钱货4 小时前
容器:软件世界的标准集装箱
linux·运维·c++·docker·容器
云知谷4 小时前
【嵌入式基本功】单片机嵌入式学习路线
linux·c语言·c++·单片机·嵌入式硬件
kk”5 小时前
C++ stack 和 queue
开发语言·c++
给大佬递杯卡布奇诺6 小时前
FFmpeg 基本API avcodec_send_packet函数内部调用流程分析
c++·ffmpeg·音视频
QT 小鲜肉6 小时前
【数据结构与算法基础】05. 栈详解(C++ 实战)
开发语言·数据结构·c++·笔记·学习·算法·学习方法
lingran__6 小时前
算法沉淀第七天(AtCoder Beginner Contest 428 和 小训练赛)
c++·算法
2401_840105206 小时前
P1049 装箱问题 题解(四种方法)附DP和DFS的对比
c++·算法·深度优先·动态规划