C++内存管理

目录

1.C/C++内存分布

2.C语言中动态内存管理方式

3.C++中动态内存管理

3.1 new/delete的用法

1.操作内置类型

2.操作自定义类型

3.2 new和delete操作内置类型和自定义类型的区别

3.3 相比malloc/free的区别、优点

4.operator new与operator delete函数

4.1 operator new和operator delete函数

4.2 为什么要设计operator new和operator delete函数?

5.new和delete的实现原理

5.1 内置类型

5.2 自定义类型

1.new/delete原理

new的原理:

delete的原理:

2.new[]和delete[]原理

new T[N]的原理:

delete[]的原理:

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

6.1 场景引入

6.2 定位new的用法

7.常见面试题

7.1 malloc/free和new/delete的区别

7.2 内存泄漏

1.什么是内存泄漏,内存泄漏的危害

2.内存泄漏分类

3.如何检测内存泄漏

4.如何避免内存泄漏

5.总结


 

1.C/C++内存分布

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收

  3. 数据段(静态区):存放全局变量、静态数据(static修饰的变量)。程序结束后由系统释放。

  4. 代码段:存放函数体的二进制代码以及只读常量(比如字符串常量)。

2.C语言中动态内存管理方式

malloc、calloc、realloc、free

使用详见:动态内存管理

3.C++中动态内存管理

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

3.1 new/delete的用法

1.操作内置类型

  1. 动态申请一个int类型的空间

    int* ptr1 = new int;

  2. 动态申请一个int类型的空间并初始化

    int* ptr2 = new int(10);

  3. 动态申请3个int类型的空间

    int* ptr3 = new int[3];

  4. 动态申请3个int类型的空间并初始化

    int* ptr4 = new int[3] {1, 2, 3};

释放空间:delete和delete[]

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

2.操作自定义类型

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

  1. 动态申请一个A类型的空间

    A* p5 = new A;

  2. 动态申请3个A类型的空间

    A* p6 = new A[10];

释放空间:

delete ptr5;
delete[] ptr6;

总结:

  • 申请和释放单个元素的空间,使用new和delete操作符。

  • 申请和释放连续的空间,使用new[]和delete[]。

  • 一定要匹配起来使用,new[]出来的不能delete,可能会出错 。

3.2 new和delete操作内置类型和自定义类型的区别

new/delete针对内置类型和malloc/free没有区别,但是针对自定义类型就比malloc/free强上一大截,new/delete就是为了自定义类型而设计的。

C语言的malloc、free操作自定义类型有什么缺陷?

  1. C语言中malloc一个自定义类型时,这个自定义类型是不会初始化的,还需要我们手动初始化,也就是调用一个Init函数。

  2. C语言中free一个自定义类型时,这个自定义类型的资源也不会被清理,也需要我们提前手动清理,调用Destory函数。

但是new/delete就不一样,他们会这么做:

  1. new:开空间+调用自定义类型的构造函数

  2. delete:调用自定义类型的析构函数+释放空间

注意先后顺序。

3.3 相比malloc/free的区别、优点

共同点:都是从堆上申请空间,并且需要用户手动释放。

本质上的区别:malloc/free是函数,new/delete是操作符。

用法上的区别(也可以是优点):

  1. malloc需要手动计算空间大小并传递;new只需在其后跟上空间的类型即可;如果是多个对象,[]中指定对象个数即可。

  2. malloc的返回值为void*, 在使用时必须强转;new不需要,因为new后跟的是空间的类型。

  3. malloc申请空间失败时,返回的是NULL,因此使用时必须判空;new不需要,new申请失败时底层会抛异常,我们只需要捕获异常。

  4. malloc申请的空间不会初始化;new可以初始化。

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

4.operator new与operator delete函数

4.1 operator new和operator delete函数

  1. new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。

    注意:operator new和operator delete并不是对new/delete操作符的重载。

  2. new操作符在底层调用operator new全局函数来申请空间,delete操作符在底层调用operator delete全局函数来释放空间

    注意:单单是申请空间和释放空间,没有构造和析构。

  3. operator new和operator delete是对malloc和free的封装,而且他们的用法和malloc/free是一样的;

    所以new/delete操作符底层上就是调用malloc/free函数。

operator new和operator delete全局函数的使用:

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

private:
	int _a;
};

int main()
{
	A* p1 = (A*)operator new(sizeof(A)); 
	operator delete(p1);

	return 0;
}

4.2 为什么要设计operator new和operator delete函数?

为什么要设计operator new和operator delete函数?new/delete不能直接用malloc/free函数吗?

原因:

  1. operator new和operator delete设计出来不是给我们用的,而是给new/delete操作符申请/释放空间用的。

  2. 申请空间失败时,new会抛异常;这是因为operator new申请空间失败时会抛异常。

  3. 所以operator new和operator delete封装malloc/free就是给new/delete操作符处理失败抛异常问题的。

补充:如何捕获new所抛出的异常?(32位)

int main()
{
	try
	{
		char* p1 = new char[1024 * 1024 * 1024];
		cout << (void*)p1 << endl;

		char* p2 = new char[1024 * 1024 * 1024];
		cout << (void*)p2 << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

5.new和delete的实现原理

5.1 内置类型

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

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

5.2 自定义类型

1.new/delete原理

new的原理:
  1. 调用operator new函数申请空间。

  2. 在申请的空间上执行构造函数,完成对象的初始化。

查看new的汇编代码,也是这么做的:

delete的原理:
  1. 在空间上执行析构函数,完成对象中资源的清理工作。

  2. 调用operator delete函数释放对象的空间。

查看delete的汇编代码,也是这么做的:

2.new[]和delete[]原理

  1. new在底层调用operator new全局函数来申请空间,delete在底层调用operator delete全局函数来释放空间

  2. new[]在底层调用operator new[]全局函数来申请空间,delete[]在底层调用operator delete[]全局函数来释放空间

    operator new[]又会调用operator new,operator delete[]也会调用operator delete。(有点多此一举,但可能是有其他问题的考虑,或者语法设计者有强迫症......)

new T[N]的原理:
  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。

    operator new[] -> operator new -> malloc

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

delete[]的原理:
  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。

  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。

    operator delete[] -> operator delete -> free

补充:

new[]中的[]会指明开多少个对象的空间,所以底层的构造函数才会知道调用多少次,但是delete[]中没有指明,那他在底层上怎么知道调用多少次析构函数呢?析构函数少一次调用可能会出现内存泄漏,怎么解决的?

我们拿一个Stack类举例:

class Stack
{
public:
	//...
private:
	int _a;
	int _size;
	int _capacity;
};

int main()
{
	Stack* p = new Stack[10];
	//...
	delete[] p;

	return 0;
}
‍
  1. new[]底层调用operator new[],operator new[]再调用operator new,operator new再调用malloc,那么malloc会开辟多少字节的空间 ,120字节?

    其实并不止120字节,operator new[]开空间时会在首地址往前多开4个字节,而这4个字节就是用来保存对象个数的。但是malloc返回的指针p并不指向往前偏移4字节后的位置,还是指向首对象的位置。

  2. delete[]底层调用operator delete[],operator delete[]再调用operator delete,operator delete再调用free。

    free并不是释放p指针指向的位置,而是往前偏移4个字节,读取里面的对象个数,得知调用多少次的析构,然后带着这4个字节整体释放。

  3. 如果new[]出来的空间我们用delete释放会有什么后果?

    • 首先delete底层上只调用一次析构,其他对象没有析构可能导致内存泄漏;

    • delete底层调用的free,而p指针并不是首地址,所以会free掉一块空间的一部分,导致程序崩溃。

​​

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

6.1 场景引入

我们知道:

  1. 当实例化一个对象或者在堆上new/new[]出对象空间时,会自动调用类的构造函数。

  2. 当对象被销毁或者delete/delete[]对象空间时,会自动调用类的析构函数。

那么构造函数和析构函数可以显示调用吗 ?

先说结论:构造函数不可以,析构函数可以。

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

private:
	int _a;
};

int main()
{
	A* p1 = (A*)operator new(sizeof(A));//使用operator new申请空间
	//p1->A();//但是构造函数不可以显示调用

	p1->~A();//析构函数可以显示调用
	operator delete(p1);//使用operator delete释放空间

	return 0;
}

你可能会疑问,申请空间和调用构造一个new操作符就能完成,为什么要分开来写?常规情况下确实不用这么写,但是特殊场景下还有有把这两个工作分开的需求的。比如内存池(这里不做详细介绍)。

那么我们怎么显示调用构造函数?使用定位new!

6.2 定位new的用法

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

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

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

示例:

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

private:
	int _a;
};

int main()
{
	A* p1 = (A*)operator new(sizeof(A));//使用operator new申请空间
	//p1->A();//但是构造函数不可以显示调用
	new(p1) A(1);//显示调用构造函数初始化为1

	p1->~A();//析构函数可以显示调用
	operator delete(p1);//使用operator delete释放空间

	return 0;
}

7.常见面试题

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

7.2 内存泄漏

1.什么是内存泄漏,内存泄漏的危害

  1. 什么是内存泄漏:

    内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

  2. 内存泄漏的危害:

    长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

  3. 两种情况可能导致内存泄漏:

    1. 内存申请了忘记释放

    2. 异常安全问题

2.内存泄漏分类

  1. 堆内存泄漏(Heap leak) 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

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

3.如何检测内存泄漏

在vs下,可以使用windows操作系统提供的 _CrtDumpMemoryLeaks() 函数进行简单检测,但是该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。

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

4.如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。

    ps:这个只是理想状态,如果碰上异常时,就算注意释放了,还是可能会出问题。

  2. 采用RAII思想或者智能指针来管理资源。

  3. 公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

5.总结

内存泄漏非常常见,解决方案分为两种:

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

2、事后查错型。如泄漏检测工具。

  • 38
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值