当前位置: 首页 > news >正文

【C++】C++ 内存管理

本篇文章主要讲解 C++ 中动态管理内存的方式,包括两个关键字 new 和 delete。


1  C 语言中内存管理方式

        C 语言中动态管理内存主要是通过 4  个函数来进行管理的,分别是 malloc ,calloc,realloc 以及 free 函数,而且其开辟的内存都是在堆上开辟的:

#include<stdio.h>void Print(int* arr, size_t size)
{for (int i = 0; i < size; i++){printf("%d ", arr[i]);}printf("\n");
}int main()
{//malloc,calloc,realloc 函数会返回一个开辟内存后的 void* 指针,需要进行强转int* ptr1 = (int*)malloc(sizeof(int) * 10);//开辟完之后,需要判断是否为空指针,开辟失败返回空指针if (ptr1 == NULL){perror("malloc fail!\n");exit(1);}int* ptr2 = (int*)calloc(10, sizeof(int));//开辟完之后,需要判断是否为空指针,开辟失败返回空指针if (ptr2 == NULL){perror("calloc fail!\n");exit(2);}Print(ptr1, 10); Print(ptr2, 10);int* ptr3 = (int*)realloc(ptr1, sizeof(int) * 20);//开辟完之后,需要判断是否为空指针,开辟失败返回空指针if (ptr3 == NULL){perror("realloc fail!\n");exit(3);}ptr1[0] = 10, ptr1[1] = 20;printf("ptr1:%p ptr3:%p\n", ptr1, ptr3);Print(ptr3, 20);//运行完之后不要忘记释放free(ptr2);free(ptr3);return 0;
}

输出结果:

-842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451
0 0 0 0 0 0 0 0 0 0
ptr1:00824798 ptr3:00824798
10 20 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451

从运行结果就可以看出 malloc,calloc,以及 realloc 的区别:

(1) malloc 与 realloc 不会对开辟的内存空间进行初始化,而 calloc 会对开辟的内存空间全部初始化为 0

(2) malloc 与 calloc 为开辟内存空间,而 realloc 为扩容,即在原来内存空间基础上进行扩容

(3) realloc 的扩容机制:如果原空间后面有足够的空间进行扩容,那就原地进行扩容;如果不够,那就先异地扩容,然后拷贝原空间的数据到新空间,再释放原空间。

        但是 C 语言的内存管理方式存在一定的问题:

(1) 容易忘记释放内存,造成内存泄露现象

(2) 在开辟完之后需要手动判断是否开辟成功,较为麻烦

(3) 返回的为 void* 类型的指针,需要进行强转

所以为了避免以上问题,C++ 提出了自己的内存管理方式。


2  C++ 的内存管理方式

        C++ 中采用了 new 运算符来动态开辟内存,采用 delete 来释放内存。

1) 内置类型使用 new 和 delete 来动态开辟内存

        对内置类型使用 new 和 delete 动态开辟内存时,其语法如下:

//开辟一个对象
//使用 new 关键字来开辟一个对象,new 后面为要开辟的对象类型
//new 运算符会返回对应类型的指针,不需要进行强转了
int* pa = new int(1); //可以在()中进行初始化,不初始化默认为随机值
delete pa;//开辟多个对象
//使用 new[] 来开辟多个对象,[] 中为要开辟的对象个数
int* pa = new int[10]{1, 2, 3}; //可以使用{}来进行初始化,没有初始化的默认为0
delete[] pa;

举个例子:

#include<iostream>int main()
{int* pa1 = new int;cout << *pa1 << endl;int* pa2 = new int(10);cout << *pa2 << endl;int* arr1 = new int[10]{1, 2, 3};for (int i = 0; i < 10; i++)cout << arr1[i] << ' ';cout << endl;delete pa1;delete pa2;//new[] 与 delete[] 搭配使用,不要用混delete[] arr1;return 0;
}

运行结果:

-842150451
10
1 2 3 0 0 0 0 0 0 0

        内置类型使用 new 和 delete 动态开辟内存时,没有什么特别需要注意的地方,只需要注意搭配就可以,不要用混,delete 配合 new 使用,delete[] 配合 new[] 使用


2) 自定义类型使用 new 和 delete 来动态开辟内存

        自定义类型也是使用 new 和 delete 与 new[] 和 delete[] 来动态开辟内存,只不过与内置类型不一样的是自定义类型 new 和 delete 会调用自定义类型的构造函数与析构函数,如:

#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){cout << "A(int a1, int a2)" << endl;}~A(){cout << "~A()" << endl;}private:int _a1;int _a2;
};int main()
{//会调用A类的默认构造函数A* a1 = new A;//会调用A类的构造函数A* a2 = new A(2, 3);//会去调用A类的析构函数delete a1;delete a2;return 0;
}

运行结果:

A(int a1, int a2)
A(int a1, int a2)
~A()
~A()

可以看到 new 和 delete 确实会去调用 A 类的构造函数与构造函数,new[] 与 delete[] 也是一样的,new 了几个对象就会去调用几次构造函数与析构函数

#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2) :_a1(a1),_a2(a2){cout << "A(int a1, int a2)" << endl;}void Print(){cout << _a1 << ' ' << _a2 << endl;}~A(){cout << "~A()" << endl;}private:int _a1;int _a2;
};int main()
{A* a1 = new A[10];//去调用默认构造函数A* a2 = new A[10]{{1, 1}, {1, 2}, {1, 3}, {1, 4}};//也可以利用大括号进行初始化,实际上是利用了类型转换,没有初始化的会去调用默认构造函数for (int i = 0;i < 10; i++){a2[i].Print();}delete[] a1;delete[] a2;return 0;
}

运行结果:

A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
A(int a1, int a2)
1 1
1 2
1 3
1 4
1 2
1 2
1 2
1 2
1 2
1 2
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()

通过上述代码,可以看到 new[] 和 delete[] 会去调用相应次数的构造函数和析构函数,如果没有进行初始化那就调用默认构造函数(编译器自动生成、全缺省以及无参构造函数),如果没有默认构造就会报错,如果进行了初始化那就去调用相应的构造函数去进行初始化。


3) C++ 与 C 内存管理的区别

        C++ 中采用 new 和 delete 来进行内存管理,而 C 语言采用 malloc 和 free 来进行内存管理,他们具有如下区别:

(1) new 和 delete 可以在申请资源时进行初始化,而 malloc 不可以

(2) new 在申请自定义类型的资源时,会去调用其构造函数主动完成初始化,而 malloc 不可以

(3) new 在开辟完后会返回对应类型的指针,而 malloc 返回的是 void* 类型的指针,需要进行强转

(4) 如过开辟资源所需内存不够,new 会抛出异常(后面章节会进行讲解),而 malloc 则会返回 nullptr,所以用 malloc 需要判断返回值,而 new 则不需要判断返回值

(5) delete 释放资源时会去主动调用析构函数,而 free 则不会,可能会造成内存泄露情况


3  new 和 delete 的原理

        在讲解 new 和 delete 的原理之前,我们先来看一下 operator new 和 operator delete 函数。

1) operator new 与 operator delete 函数

        operator new 和 operator delete 是系统提供的两个全局函数,new 实际上是调用 operator new 来开辟空间,operator delete 实际上是调用 operator delete 来实现释放空间的。

operator new 的源码

/*
operator new 底层也是调用 malloc 来实现资源的申请的,
当 malloc 申请成功时直接返回,如果没有成功,那就执行内存不足应对措施,
如果设置了,那就继续申请内存,如果没有设置,那就会抛出 bad_alloc 异常
*/void *_CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{if (size == 0) {size = 1; // 处理 0 大小的请求}void* ptr;// 调用底层内存分配函数mallocwhile ((ptr = std::malloc(size)) == nullptr) if (_callnewh(size) == 0){//这里申请内存失败,抛出 bad_alloc 类型异常static const std::bad_alloc nomem;_RAISE(nomem);}return ptr;
}

operator delete 的源码

//delete 底层也是通过 free 来释放内存的//free 实现 -- 实际上是一个宏,调用了 _free_dbg 函数
#define free(ptr) _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 headerpHead = pHdr(pUserDate);//verify block type_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockAUse));_free_dbg(pUserData, pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK);//release other threads__END_TRY_FINALLYreturn;
}

        上述两个函数的源码不必掌握,只需要知道 operator new 底层是调用 malloc 来申请资源,而 operator delete 底层是通过 free 来释放资源即可


2) new 和 delete 原理

(1) 内置类型的 new 和 delete

        内置类型的 new 和 delete 与 malloc 和 free 基本类似,只不过 new 和 delete 会开辟与释放单个内存空间,而 new[] 和 delete[] 会开辟和释放连续的内存空间。

(2) 自定义类型的 new 和 delete

new 的原理:

a. 先调用 operator new 函数开辟资源

b. 再调用类的对应构造函数来完成对对象的构造

delete 的原理:

a. 先调用类的析构函数来完成对象资源的清理

b. 再调用 operator delete 来释放对象的空间

new[T] 的原理:

a. 首先在需要申请对象资源的内存地址前开辟四个字节,用来记录当前开辟对象的个数

b. 然后调用 operator new[] 函数,operator[] 再去调用 operator new 函数完成对 T 个对象内存空间的申请

c. 调用 T 次构造函数,完成对 T 个对象的初始化工作

delete[] 的原理:

a. 首先调用 T 次析构函数来完成对对象资源的清理工作

b. 然后调用 operator delete[] 函数,operator delete[] 再去调用 operator delete 完成对象空间的释放

        懂得了原理之后,我们就知道为什么 new[] 和 delete 不能混着用了,对于内置类型还好,不会报错,对于没写析构函数的自定义类型也不会报错(因为不会在前面开辟四个字节来记录调用用构造函数与析构函数的次数);但是一旦写了析构函数,在使用 new[] 开辟内存空间时,就需要在原本开辟资源的指针前再开辟四个字节,那么使用 delete 就会报错,因为 delete 去调用 operator delete 函数,operator delete 去调用 free,而 free 是不能在中间开始释放的,所以会报错,而且使用 delete 由于只是调用一次析构函数,也有可能造成内存泄露情况

内置类型 new[] 与 delete 混合使用:

#include<iostream>using namespace std;//内置类型 new[] 与 delete 混合使用不会出现问题
int main()
{int* ptr = new int[10];delete ptr;return 0;
}

没有析构函数的类 new[] 与 delete 混着用:

//没有析构函数,也不会报错
include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){}private:int _a1;int _a2;
};int main()
{A* ptr = new A[10];delete ptr;return 0;
}

有析构函数的类 new[] 与 delete 混着用:

#include<iostream>using namespace std;class A
{
public:A(size_t size = 4){_arr = new int[size];_size = size;cout << "A(size_t size = 4)" << endl;}~A(){delete[] _arr;_arr = nullptr;_size = 0;cout << "~A()" << endl;}private:int* _arr = nullptr;size_t _size;
};//有析构函数的自定义类型,不仅会报错,而且会造成内存泄漏情况
int main()
{A* ptr = new A[10];delete ptr;return 0;
}

报错结果:

通过结果可以看到,其确实只调用了一次析构函数,而且报错了,我们来画图分析一下报错原因:

其在内存中实际上是这样开辟内存的,而 delete 底层去调用了 free,其实 delete ptr 实际上就是 free(ptr),而 ptr 指针是不是开辟内存的起始位置,而是中间位置,free 不能从中间位置开始释放,所以就报错了。


4  定位 new

        定位 new 是用在对于已分配的原始内存空间中调用构造函数初始化对象的一种方法。在一个类中,构造函数是不能显示调用的,而析构函数却是可以的,所以如果一个对象已经分配了内存空间,但是没有初始化(比如在内存池中),这时候就不能使用 new 关键字去调用构造函数了,而是需要使用定位 new 去初始化对象。

定位 new 语法格式:

new(place_address)type 或者 new(place_address)type(initializer_list)

place_address 为一个指针,type 为该指针的自定义类型,initializer_list为该类型的初始化列表

#include<iostream>using namespace std;class A
{
public:A(int a = 10):_a(a){cout << "A(int a = 10)" << endl;}~A(){cout << "~A()" << endl;}private:int _a;
};int main()
{A* ptr1 = (A*)malloc(sizeof(A));//ptr->A();//不能显示调用构造函数//使用定位 new 来初始化已经分配内存空间的对象new(ptr1)A;//调用A的默认构造A* ptr2 = (A*)malloc(sizeof(A));new(ptr2)A(11);//还可以传入参数ptr1->~A();//可以显示调用析构函数ptr2->~A();free(ptr1);free(ptr2);return 0;
}

运行结果:

A(int a = 10)
A(int a = 10)
~A()
~A()

文章转载自:

http://tc7cRlnr.kntbk.cn
http://mnDH3FpL.kntbk.cn
http://1Zxvjhpd.kntbk.cn
http://Zm0qf54n.kntbk.cn
http://cpYQCczE.kntbk.cn
http://bUZi8vU6.kntbk.cn
http://CgvbHJ77.kntbk.cn
http://s297Y7Rq.kntbk.cn
http://YESxqmaR.kntbk.cn
http://znEPONsE.kntbk.cn
http://y7gpQgoG.kntbk.cn
http://PzD08rfK.kntbk.cn
http://9CXF2goT.kntbk.cn
http://aLZiAluG.kntbk.cn
http://yR6JriDt.kntbk.cn
http://VZLTWVSY.kntbk.cn
http://WGsVztb2.kntbk.cn
http://yurh1SUc.kntbk.cn
http://W6oyQLul.kntbk.cn
http://EJrb4zDE.kntbk.cn
http://Ix4wFXMm.kntbk.cn
http://3TVvzGoh.kntbk.cn
http://UkiKG2cr.kntbk.cn
http://vYXioMvB.kntbk.cn
http://WmOgFT1B.kntbk.cn
http://m5OaFKSI.kntbk.cn
http://R2cnPaCc.kntbk.cn
http://Ff2Z7WcL.kntbk.cn
http://REcxWnIM.kntbk.cn
http://KqodD8x5.kntbk.cn
http://www.dtcms.com/a/377444.html

相关文章:

  • C++ STL之list的使用
  • Midjourney绘画创作入门操作创作(宣传创意)
  • 【数据库约束】
  • 小白成长之路-centos7部署ceph存储
  • python学习进阶之面向对象(二)
  • 【芯片设计-信号完整性 SI 学习 1.1.1 -- Unit Interval,比特周期】
  • sudo apt update sudo apt upgrade -y 两个命令的作用
  • 每日算法刷题Day68:9.10:leetcode 最短路6道题,用时2h30min
  • apache详细讲解(apache介绍+apache配置实验+apache实现https网站)
  • 一些常用的激活函数及绘图
  • 第3节-使用表格数据-数据库设计
  • 同步时钟系统在体育场游泳馆的应用
  • QT里获取UUID当做唯一文件名称
  • 【Python】pytorch数据操作
  • iOS应用启动深度解析:dyld动态链接器的工作机制与优化实践
  • [硬件电路-175]:multisim中如何给让光电二极管产生光电流?
  • 小巧精准,安全无忧:安科瑞ADL200N-CT/D16-WF防逆流电表守护阳台光伏
  • NLP(自然语言处理, Natural Language Processing)
  • 【竞赛系列】机器学习实操项目07——全球城市计算AI挑战赛(baseline、时间序列分析、地铁流量预测)
  • 华为昇腾CANN开发实战:算子自定义与模型压缩技术指南
  • Java 多线程(二)
  • TCGA(The Cancer Genome Atlas)数据库是癌症基因组学研究的重要资源,包含了多种癌症类型的基因组、转录组、表观基因组和临床数据
  • 单片机与PLC:定义、异同及替代可能性解析
  • 金融知识:投资和融资
  • 重学前端013 --- 响应式网页设计 CSS网格布局
  • hCaptcha 图像识别 API 对接说明
  • 大模型应用开发八股
  • Linux进程概念(上):进程基本概念和进程状态
  • 汽车EPAS ECU功能安全建模分析:Gamma框架+深度概率编程落地ISO 26262(含寿命预测案例)
  • 深入解析:ES6 中 class 与普通构造器的区别