C++打怪升级(七)- 动态内存管理

C++打怪升级(七)- 动态内存管理_第1张图片

前言

本节继续C++的学习,让我们来看看C++的动态内存管理吧!


推荐阅读

《深入理解计算机系统》- 虚拟内存
—深入理解计算机系统pdf

引子

动态内存管理我们在C语言中就是重要的部分,我们应该不会对其陌生。
在C语言中有关动态内存管理的函数有malloc()、calloc()、realloc()、free()
其中malloc、calloc、realloc是向堆区申请内存的函数,free是释放在堆区申请的内存空间的函数;

malloc函数
向堆申请以字节为单位的内存空间,并且申请的空间中初始值是随机值;

#include 
#include 

int main() {
	//向堆申请4个整型的空间
	int* p1 = (int*)malloc(sizeof(int) * 4);
	//检查空间是否申请成功,申请失败返回空指针
	if (!p1) {
		perror("malloc fail");
		return -1;
	}
	//释放指针p1指向的空间
	free(p1);
	//p1置NULL,防止对野指针的使用
	p1 = NULL;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第2张图片


calloc函数
向堆申请以字节为单位的内存空间,并且对申请的内存空间每一个位bit都初始化为0;

int main() {
	//向堆申请4个整型的空间
	int* p2 = (int*)calloc(4, sizeof(int));
	//检查空间是否申请成功,申请失败返回空指针
	if (!p2) {
		perror("malloc fail");
		return -1;
	}
	//释放指针p1指向的空间
	free(p2);
	//p1置NULL,防止对野指针的使用
	p2 = NULL;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第3张图片


realloc函数
对已经开辟的内存空间进行扩容,扩容成功返回扩容后内存起始地址,扩容失败返回空指针
扩容又分为原地扩和异地扩:
当原来开辟空间之后有足够的的空闲空间,进行原地扩容;
当原来开辟的空间之后没有足够的空间,进行异地扩容,在堆中随机寻找一块足够的空间并把原来空间内容拷贝到新空间,释放旧空间,函数返回新空间的起始地址;

int main() {
	//向堆申请4个整型的空间
	int* p1 = (int*)malloc(sizeof(int) * 4);
	//检查空间是否申请成功,申请失败返回空指针
	if (!p1) {
		perror("malloc fail");
		return -1;
	}
	//对已经开辟的内存空间进行扩容,分为两种情况
	//原地扩和异地扩
	//这里不直接使用p1指针接收realloc返回值,
	//因为p1有具体的指向,如果申请失败realloc返回空指针,
	//导致p1被置为空指针,导致原来指向内存空间找不到了
	int* tmp = (int*)realloc(p1, sizeof(int) * 8);
	//检查空间是否申请成功,申请失败返回空指针
	if (!tmp) {
		perror("realloc fail");
		return -1;
	}
	p1 = tmp;

	//释放指针p1指向的空间
	free(p1);
	//p1置NULL,防止对野指针的使用
	p1 = NULL;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第4张图片


C++由C而来,C++兼容C语言,C++中也可以直接使用C语言中有关动态内存开辟空间的函数;不过C++中一般不会直接使用原生的C语言中的malloc/calloc/realloc/free函数,C++中为了更好地支持面向对象,引入了有关动态内存的新概念:new和delete


C/C++进程内存的分布

在我们写的C/C++程序运行起来时,操作系统会为我们的程序建立一个进程,而每一个进程都有自己的虚拟地址空间,这里要介绍的就是C/C++程序对应进程中虚拟地址空间的划分。
C++打怪升级(七)- 动态内存管理_第5张图片

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

C++的动态内存管理

new

newdelete是C++中新引入的关键字,同时也是运算符,这一点与C语言中malloc等函数不同;
new格式

类型* 指针变量名 = new 类型

申请空间

使用new申请1个整型内存空间

int* p1 = new int;
cout << "*p1= " << * p1 << endl;
*p1 = 10;
cout << "*p1= " << *p1 << endl;

C++打怪升级(七)- 动态内存管理_第6张图片


使用new申请多个个整型内存空间

int* p1 = new int[5];

C++打怪升级(七)- 动态内存管理_第7张图片


初始化

int* p1 = new int(20);
	cout << "*p1= " << *p1 << endl;

C++打怪升级(七)- 动态内存管理_第8张图片


int* p1 = new int[5]{ 1,2 };
int* p2 = new int[5]{ 1,2,3,4,5 };

C++打怪升级(七)- 动态内存管理_第9张图片


delete 释放申请的空间

格式:

delete 指向动态申请空间的指针

int* p1 = new int(20);
cout << "*p1= " << *p1 << endl;
delete p1;

C++打怪升级(七)- 动态内存管理_第10张图片


int* p1 = new int[5]{ 1,2 };
delete[] p1;
p1 = nullptr;

C++打怪升级(七)- 动态内存管理_第11张图片


new和delete对于自定义类型

对于内置类型,new申请内存空间和delete释放空间相对于malloc申请空间和free释放空间基本没有区别,只是new和delete用法更加简洁和方便;
new和delete的真正不同的用处是相对于自定义类型来说的;

new

  1. 完成内存空间的申请;
  2. 调用类的构造函数进行初始化
class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
	}
private:
	int _a;
};
int main() {

	A* p1 = new A;
	A* p2 = (A*)malloc(sizeof(A));
	return 0;
}

C++打怪升级(七)- 动态内存管理_第12张图片
C++打怪升级(七)- 动态内存管理_第13张图片


delete

  1. 调用类的析构函数完成类对象资源清理工作;
  2. 释放申请的空间。
class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
	}
private:
	int _a;
};
int main() {

	A* p1 = new A;
	A* p2 = (A*)malloc(sizeof(A));
	delete p1;
	free(p2);
	return 0;
}

C++打怪升级(七)- 动态内存管理_第14张图片
C++打怪升级(七)- 动态内存管理_第15张图片


new和delete的注意事项

new申请的空间和delete释放的空间要严格匹配,否则可能会出现意想不到的错误;

错误举例:

class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
	}
private:
	int _a;
};

new了一个对象,delete多个对象

int* p1 = new int;
delete[] p1;
A* p1 = new A;
delete[] p1;//

new了多个对象,delete一个对象

A* p2 = new A[10];
delete p2;

new出来的对象,使用free()释放

A* p1 = new A;
free(p1);
A* p1 = new A[10];
free(p1);

以上这些行为到底会发生什么报错、异常、正常运行,不同编译器的处理方式不一定相同;
我们在使用new和delete时应该匹配使用,这样才能避免可能的错误。


new的底层

我们知道new操作符包含的操作:

对内存空间的申请;
如果是自定义类型,则还会调用自定义类型的构造函数进行初始化;

C++引入了new,但也不是凭空而来,new实际上是对malloc()的封装;

operatoe new 函数

operator new介绍

new的底层申请空间是通过调用operator new函数实现的;

operator new函数一览
函数接受一个无符号整型,作为待申请空间的大小
申请成功返回起始空间的地址;
申请失败返回nullptr;

void * operator new(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()申请空间,如果malloc()申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常;
对于自定义类型,operator new()不会调用构造函数,而是由其他函数调用;


显式使用operator new

operator new的使用与malloc相似

内置类型

//operator new
int main() {

	int* p1 = new int;
	int* p2 = (int*)operator new(sizeof(int));
	int* p3 = (int*)malloc(sizeof(int));
	return 0;
}

C++打怪升级(七)- 动态内存管理_第16张图片
C++打怪升级(七)- 动态内存管理_第17张图片
C++打怪升级(七)- 动态内存管理_第18张图片


自定义类型

  1. 调用operator new函数申请空间;
  2. 在申请的空间上执行构造函数,完成对象的构造;
class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
	}
private:
	int _a;
};
int main() {

	A* p1 = new A;
	A* p2 = (A*)operator new(sizeof(A));
	A* p3 = (A*)malloc(sizeof(A));
	return 0;
}

C++打怪升级(七)- 动态内存管理_第19张图片
C++打怪升级(七)- 动态内存管理_第20张图片


operator new[]函数

operator new[]函数介绍

operator new[] 申请多个对象时调用;
operator new[]函数底层会调用operator new函数,operator new函数又调用malloc函数;

void* operator new[](size_t size){
    return operator new(size);
}

显式使用operator new[]

内置类型

//operator new[]
int main() {

	int* p1 = new int[4];
	int* p2 = (int*)operator new[](sizeof(int)*4);
	int* p3 = (int*)malloc(sizeof(int) * 4);
	return 0;
}

C++打怪升级(七)- 动态内存管理_第21张图片
C++打怪升级(七)- 动态内存管理_第22张图片
C++打怪升级(七)- 动态内存管理_第23张图片


自定义类型

  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对
    象空间的申请;
  2. 在申请的空间上执行N次构造函数 ;
class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
	}
private:
	int _a;
};
int main() {

	A* p1 = new A[4];
	A* p2 = (A*)operator new[](sizeof(A) * 4);
	A* p3 = (A*)malloc(sizeof(A) * 4);
	return 0;
}

C++打怪升级(七)- 动态内存管理_第24张图片
C++打怪升级(七)- 动态内存管理_第25张图片


delete的底层

delete底层可以分为两部分

  1. 如果是自定义类型,先调用自定义类型的析构函数,清理资源;
  2. 释放申请的空间;

operator delete函数

operator delete介绍

delete调用operator delete函数,operator delete函数又调用free函数;
如果是自定义类型,delete将先调用自定义类型的析构函数,再调用operator delete函数;

为什么说operator delete函数调用了free函数呢?
因为operator delete函数调用了_free_dbg()函数而free()函数实际是由_free_dbg()函数宏实现的;

#define free(p) _free_dbg(p, _NORMAL_BLOCK)
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

内置类型

//operator delete
int main() {

	int* p1 = (int*)operator new(sizeof(int));

	operator delete(p1);
    p1 = nullptr;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第26张图片
C++打怪升级(七)- 动态内存管理_第27张图片


这里直接使用delete运算符也可以对operator new函数申请的空间进行释放;
这是因为,delete的底层会调用operator delete,而operator delete再调用free,这与operator delete直接调用free相比只是多了一层转换;

//operator delete
int main() {

	int* p1 = (int*)operator new(sizeof(int));

	delete p1;
	p1 = nullptr;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第28张图片
C++打怪升级(七)- 动态内存管理_第29张图片


自定义类型

class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
		_a = 0;
	}
private:
	int _a;
};
int main() {

	A* p1 = (A*)operator new(sizeof(A));
	operator delete(p1);
	p1 = nullptr;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第30张图片
C++打怪升级(七)- 动态内存管理_第31张图片


这里也可以直接使用delete运算符

int main() {

	A* p1 = (A*)operator new(sizeof(A));
	delete p1;
	p1 = nullptr;
	return 0;
}

operator delete[]函数

operator delete[]函数介绍

operator delete[]将调用operator delete函数

void operator delete (void* ptr) noexcept;

显式使用operator delete[]

内置类型

int main() {

	int* p1 = (int*)operator new[](sizeof(int)*4);
	operator delete[](p1);
    //或delete p1;
	p1 = nullptr;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第32张图片
C++打怪升级(七)- 动态内存管理_第33张图片
C++打怪升级(七)- 动态内存管理_第34张图片


自定义类型

class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
	}
private:
	int _a;
};
int main() {

	A* p1 = (A*)operator new[](sizeof(A) * 4);
	operator delete[](p1);
    //delete p1;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第35张图片
C++打怪升级(七)- 动态内存管理_第36张图片


关于delete底层实现的一些简单分析

为什么说申请内存和释放内存的方式要严格匹配呢?
我们知道如果不匹配可能会引发意想不到的情况,这与编译器有关;
new是创建一个新对象,delete也释放一个对象(如果是自定义类型还会调用析构函数);
new[]是创建一个对象数组;我们当然知道我们自己创建的对象数组的大小,对于delete[]并不知道对象数组的大小,只知道对象数组的起始地址;
那么编译器如何知道delete[]要释放的对象个数呢?
一种方式是,再开始创建对象数组时new []并不是创建了我们指定的大小,而是在对象数组前且紧邻对象数组又额外开辟了一小块空间用于记录对象数组的大小;
C++打怪升级(七)- 动态内存管理_第37张图片
这样,在delete []时,我们释放表面上的内存空间,实际上编译器依据表面上对象数组起始地址再往前偏移一个确定大小的空间,实际从偏移后的位置释放申请的对象数组空间。
对于有显式析构函数的自定义类型来说,这也是其调用析构函数次数的依据;

class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
	}
private:
	int _a;
};
int main() {
	A* p = new A[7];

	delete[] p;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第38张图片
解释delete p异常的可能原因

int main() {
	A* p = new A[7];

	delete p;
	return 0;
}

内存泄漏,对象数组起始地址之前还有额外的空间未被释放;

把类A的显式析构函数去掉就不报错了:
delete不需要调用显式的析构函数,在申请对象数组时就没有开辟额外的空间记录对象数组的元素个数,释放对象数组也不需要再往前偏移了,使用delete和delete[]没有区别了;

class A {
public:
	A(int a = 1) :_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
private:
	int _a;
};
int main() {
	A* p = new A[4];
	delete p;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第39张图片


C++异常

我们知道new运算符向堆申请一块内存空间,但是申请空间是有失败的情况的,new失败了会发生什么呢?
malloc/calloc/realloc失败返回空指针不同,new失败了是抛出一个异常,而非返回空指针;

int main() {
	//new失败,抛异常
	try {
		while (1) {
			//一次申请1G内存
			char* p1 = new char[1024 * 1024 * 1024];
			cout << (void*)p1 << endl;
		}
	}
	catch (exception& e) {
		cout << e.what() << endl;
	}
	return 0;
}

C++打怪升级(七)- 动态内存管理_第40张图片


内存泄漏

概念

内存泄漏指因为疏忽或错误造成程序没有释放已经不再使用的内存的情况。
所以说内存泄漏不是内存在物理上的消失内存还在那里,而是因为设计错误,失去了对分配给应用程序的内存的控制指针丢了,造成了内存的浪费。


分类

堆内存泄漏(Heap leak)

堆内存指的是程序执行中通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 释放。
如果申请的内存使用完后没有被释放,那么这部分内存就无法再次被申请使用,将导致堆内存泄露;

系统资源泄漏

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


内存泄漏危害

对于我们写的短时间运行的程序,内存泄露影响一般比较小,因为每次程序重启内存会被强制回收;
而对于长时间运行的程序或设备:操作系统/服务器等,内存泄露危害很大;

如果内存泄漏比较明显,短时间内我们就可以察觉到,这样的内存泄漏一般不会造成大的影响,我们能够及时排查;
而对于轻微的内存泄漏,就是头疼的事情,我们一般很难在初期发现这样的内存泄漏,往往等到发现时时间已经过去想当久了,这可能导致运行了很长时间的系统或设备卡顿甚至突然死机,这对于多人使用的服务器来说影响巨大,损失也往往是巨大的;

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


规避内存泄漏

事先预防

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间匹配的去释放,但是如果碰上异常时,就算注意释放了,还是可能会出问题;
  • 采用RAII思想或者智能指针来管理资源;

事后查错

使用内存泄漏工具检测


内存泄露检测推荐

_CrtDumpMemoryLeaks() 函数

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

int main() {
	int* p1 = new int[4];

	_CrtDumpMemoryLeaks();
	return 0;
}

C++打怪升级(七)- 动态内存管理_第41张图片


第三方工具检测

Windows

VLD工具 - (Visual LeakDetector)内存泄露库

VLD官网 https://kinddragon.github.io/vld/
VLD教程推荐 http://t.csdn.cn/6AKkI

得到内存泄漏点的调用堆栈和泄露内存的完整数据;


Linux

valgrind - 功能非常强大

valgrind官网 https://valgrind.org/
教程推荐 http://t.csdn.cn/ix463


定位new表达式(placement-new)

概念

在已分配原始内存空间中调用构造函数初始化一个对象;
也就是已经申请的内存空间malloc/calloc/realloc/operator new/operator new[],但是还未调用构造函数,可以使用定位new表达式来调用构造函数。

语法

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


用法

class A {
public:
	A(int a = 1) 
		:_a(a) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
        _a = 0;
	}
private:
	int _a;
};

//定位new
//为已申请的空间调用构造函数
int main() {

	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A(2);//定位new
	p1->~A();
	free(p1);
	//或delete p1;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第42张图片


int main() {
	A* p1 = (A*)operator new(sizeof(A));
	new(p1)A(2);//定位new
	p1->~A();
	operator delete(p1);
	p1 = nullptr;
	return 0;
}

C++打怪升级(七)- 动态内存管理_第43张图片


构造函数有多个参数时,可以传多个实参;

class A {
public:
	A(int a = 1,int b = 1, int c = 1) :_a(a),_b(b),_c(c) {
		cout << "构造函数: A(int a)" << endl;
	}
	~A() {
		cout << "析构函数: ~A()" << endl;
		_a = _b = _c = 0;
	}
private:
	int _a;
	int _b;
	int _c;
};
int main() {
	A* p1 = (A*)operator new(sizeof(A));
	new(p1)A(2, 2, 2);
    //new(p1)A{ 2,2,2 };
	p1->~A();
	operator delete(p1);
	p1 = nullptr;
	return 0;
}

new/delete与malloc/free

相同点

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

不同

  1. mallocfree是函数;newdelete是操作符(也是C++新增的关键字)
  2. malloc申请的空间不会初始化;new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递;new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数
  4. malloc的返回值为void*, 使用时必须强转;new后跟的空间的类型可以直接得到空间类型,不强转
  5. malloc申请空间失败时,返回的是NULL,使用前必须判空;new失败则是抛出异常,可以由另一部分捕获
  6. 对于自定义类型对象空间的申请,malloc/free只开辟空间和释放空间,不会调用构造函数与析构函数(没有初始化);new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

后记

本节主要介绍了C++中的动态内存管理方式:new/deletenew[]/delete[]的使用和底层的原理;同时内存泄漏是动态内存经常会遇到的问题,我们也不需要过多担心,小心使用动态内存+内存泄漏检测或以后的智能指针可以解决绝大部分问题。
下次再见!


E N D END END

你可能感兴趣的:(C++之打怪升级,c++,c语言,开发语言)