目录
operator new[]和operator delete[]
前言
要了解c++的内存管理,我们首先要知道的就是c/c++中程序内存区域划分,而之后的内容需要了解c语言中的动态内存管理方式,由于c++是c语言的衍生,那么c语言的内存管理方式肯定多多少少被继承到了c++中,并且c语言动态内存管理内容还是蛮多的,这篇文章没办法展开来说,如果不了解的可以去看看其他博主的博客或者之后我单独写一篇博客,再然后我们将正式开启c++内存管理的介绍,并且介绍关于c++内存管理的两个函数(new/delete),在了解了这两个函数之后,我们会深入到具体实现中了解它的实现原理,再然后就会处理的是一些关于new和delete的特殊的玩法!
上述即为本章的行文脉络!
c/c++中程序内存区域划分
首先,第一个问题:为什么c/c++要把内存区域进行划分
原因是因为不同的数据有着不同的存储需求,就好比一个学校,分为教学区完成的是教学的需求,又分为住宿区完成的是住的需求,又有饭堂完成的是吃的需求。。。。
同样的,数据也是如此
如:有些数据是用来临时用的,有些数据是用于动态使用的,有些数据是整个程序运行期间都要存在的,有些数据是不能修改的。
为了满足这些存储需求,所以程序内存分为了几大块区域:分别是内核空间,栈,内存映射段,堆,数据段/静态区,代码段/常量区
如图所示:
其中,我们常用的是四个区:栈、堆、数据段/静态区、代码段/常量区
栈:用于存储临时变量或者局部对象这种出了作用域就销毁的数据
堆:这里的空间一般是通过申请空间来获得的,比较常用的地方是一些常用的数据结构、有些算法需要动态开辟空间,并且这个空间用完是要手动释放(c语言中)。
数据段:存储的是一些整个程序运行期间都要存在的数据,例如全局数据和静态数据
代码段:存储的主要是两种,一种是不能进行修改的数据,如只读常量等,一种是可执行代码,这种代码就是我们常说的二进制代码
接下来为了熟悉一下这几个区,我们做下练习题,篇幅有限,整理的都是一些易混淆的问题,思考如下问题
1.ch存储在哪?*ch存储在哪?(栈、堆、静态区、常量区)
2.pCh存储在哪?*pCh存储在哪?(栈、堆、静态区、常量区)
3.ptr存储在哪?*ptr存储在哪?(栈、堆、静态区、常量区)
#include<iostream>
int main()
{
char ch[] = "abcd";
const char* pCh = "abcd";
int* ptr = (int*)malloc(sizeof(int) * 3);
return 0;
}
代码解析:
1.ch存储在栈上,*ch存储在栈上
原因:
ch表示的是一个数组,这个数组是在函数体内部定义的临时变量,临时变量存储在栈上
*ch表示的是这个数组中存储的数据,有人可能会说,它存储的是字符串呀,字符串常量不是存储在常量区吗?话是没错,但这整个逻辑应该是这样的,ch在定义的时候从常量区中找到"abcd"这个字符串常量并且把它拷贝在栈上,而ch指向的是它拷贝完以后在栈上的数据,若假设它直接指向常量区的字符串常量,那么它的值是无法改变的,但我们现在是可以改变的
2.pCh存储在栈上,*pCh存储在常量区
原因:
pCh存储在栈上,这个好理解,它同样是定义的一个临时变量嘛
*pCh存储在常量区,指针存储的是一个地址,他与数组不同,数组中存储的是数据元素,所以编译器为了安全考虑,生成一个拷贝的常量字符串,但指针必须是我指哪里你就要给我指向哪里,你不能说拷贝了一个数据然后跟我说这就是我要指向的数据,每个存储单元它的位置都是唯一的
3.ptr存储在栈上,*ptr存储在堆上
原因:
pCh存储在栈上是因为它是在函数体内部定义的临时变量,临时变量是存储在栈上的
*pCh存储在堆上,实际上*pCh表示的是它指向的数据,之前了解到我们动态申请的空间是在堆上的空间,那么它的数据也就存储在堆上的,你总不能说我申请了堆上的空间,堆上空间我不用我跑栈上存储数据吧?这就类似你交了住宿费以后学校把你的房间安排在饭堂,这就有点扯了!
c++函数之new的使用方法
new是c++中给出的一个新的动态内存管理的方式,它的功能是动态开辟空间
第一个场景:对任意类型动态开辟一个类型大小的空间
在c语言中,我们一般是用如下调用来解决这个场景的问题
int main()
{
int* p = (int*)malloc(sizeof(int));
return 0;
}
接下来简要介绍一下以上malloc函数
它在库中的声明:void* malloc (size_t size);
返回值:void*,c语言中为了兼容不同类型对它的调用,一般设置为void*,这种方式比较方便进行强制类型转换,所以说malloc函数的返回值一般手动进行强转
参数:size,指的是要开辟空间的字节大小
功能:在堆中开辟一个size大小的空间,这个空间可以给用户进行使用,但用完这个空间要记得释放,它不会自动释放,会造成内存泄漏
在c++中,新的玩法是用new函数(也支持c语言的玩法),还是在这个场景,我们看看new的调用
#include <iostream>
int main()
{
int* p = new int;
return 0;
}
这个场景下new的调用:new 类型
第二个场景:对任意类型动态开辟多个类型大小的空间
c语言的玩法比较简单,其实跟第一个场景是一样的,只是计算出的size不同而已,就不过多介绍了
c++新的玩法如下:
#include <iostream>
int main()
{
int *p = new int[10];
return 0;
}
这个场景下new的调用:new 类型[元素个数]
第三个场景:在第一、二场景下还需要对数据初始化
上述两个场景虽然会开辟出空间,但不会对内置类型数据给一个处理,若要对内置类型数据进行处理,c++给出以下玩法
#include <iostream>
int main()
{
int* p1 = new int(1);//初始化成1
int* p2 = new int[3]{1,2,3};//初始化成1,2,3
int* p3 = new int[5]{1,2,3};//初始化成1,2,3,0,0
return 0;
}
开辟一个数据类型空间:new 类型(值)
开辟多个数据类型空间:new 类型[元素个数]{值列表};
注意的是这里初始化有以下特点:
1.单个元素,我们显示给值的元素内置类型就按我们显示给的值初始化,自定义类型用我们给的值调用对应构造函数构造一个类型对象
2.多个元素,我们没有给全元素值,它就会按照顺序把显示给的先初始化,其他没有显示给的内置类型给默认值,自定义类型调用默认构造函数
3.多个元素,我们一个值都没给,内置类型不处理,自定义类型调用默认构造函数
c++函数之delete的使用方法
delete是c++中为了释放开辟的空间所给的一个新的方式,它的功能是释放动态开辟的空间
第一个场景:对任意开辟一个类型大小空间进行释放
在c语言中,我们一般是用如下调用来解决这个场景的问题
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int));
free(p);
return 0;
}
free函数是比较好理解的,我们进行一下简要介绍
free函数的声明:void free (void* ptr);
参数:指向要释放空间的指针
功能:释放指针所指的动态开辟的空间
在c++中,新的玩法是用delete函数,还是在这个场景,我们看看delete的调用
#include <iostream>
int main()
{
int* p = new int;
delete p;
return 0;
}
在这个场景下,delete调用格式:delete 指针
第二个场景:对任意开辟多个类型大小空间进行释放
c语言中,不管释放一个还是多个都是一摸一样的调用,都是free释放指针所指空间即可,不过多介绍
在c++中,对这个场景的新的玩法如下:
#include <iostream>
int main()
{
int* p = new int[3];
delete[] p;
return 0;
}
在这个场景下,delete的调用格式为:delete[] 指针
c++中为什么提供要提供new/delete
若假设不提供new和delete,我们思考一下以下这个代码会怎么运行
#include <iostream>
class A
{
public:
A(int a = 0)
{
_a = a;
std::cout << _a << std::endl;
}
private:
int _a;
};
int main()
{
A* pA =(A*)malloc(sizeof(A));
return 0;
}
运行结果是一个打印也没有执行,换句话来说,c语言中是没有构造函数的概念的,malloc申请空间就真的只是申请空间,构造函数完成的是对象的初始化,若一个对象的初始化都不能完成,这显然是不合理的,这也就是c++提供new的最大意义,除了这个以外,相比于malloc,new的调用也比较简洁方便
而当有了new以后,上述代码调用new就完美解决了malloc申请空间的弊端
如下代码:打印了一行(调用了一次构造)
class A
{
public:
A(int a = 0)
{
_a = a;
std::cout <<"A(int a = 0)" << std::endl;
}
private:
int _a;
};
int main()
{
A* pA =new A;
return 0;
}
同理,free函数不能解决c++中的既要释放空间并且还要调用析构函数的要求,delete就出现了,delete会在释放空间的基础上,调用自定义类型对象的析构函数
当然malloc与new的区别还体现在:malloc失败会返回空指针,所以开完空间后需要判断下是否失败,new失败会抛异常,异常需要捕获(后面专门出一期来说下异常)
总结一下:
1.对于内置类型数据,new/delete与malloc/free没有什么非常大的区别,用法不同
2.对于自定义类型,new/delete在malloc/free的基础上还会调用构造/析构函数
3.malloc失败会返回空指针,new失败会抛异常
c++之new和delete实现原理
其实之前一直都有说过,c++是c语言的继承和发展,结合到这一章节也是同理
我们通过以上学习发现,c语言的malloc和free和c++的new和delete的区别主要如下
对于new和malloc:
1.new在malloc的基础上进行了失败抛异常处理,2.new自动调用了对应构造函数
2.delete在free的基础上会自动调用对象析构函数
那么我们要了解new和delete的实现原理只要解决了在malloc和free的基础上衍生出来的功能,是不是就实现了呢?如果是你,你会怎么实现new和delete?
首先,我肯定不会再需要重新写一个申请空间的函数,有复用我就复用呗,再根据我需要的条件进行封装一个新函数是不是就行了呢?同样的,当时的人也是这么想的,new和delete是在malloc和free的基础上进行的封装得出来的
operator new和operator delete
在c++中,定义了两个全局的函数即operator new和operator delete(不是运算符重载)
这两个函数是对malloc/free进行了一个封装,怎么证明这一点?
我们看如下图为vs下找到的operator new的定义(operator delete同理)
红框内的操作证明了这一点,实际上通过如上代码我们也能看出operator new/operator delete没有调用构造/析构函数,当然operator delete没有异常一说,实际上就是为了与operator new匹配进行设计出来的
第二点,红框下面的代码什么意思
我们可以理解为就是失败后抛异常
operator new[]和operator delete[]
实际上,当我们需要new/delete多个数据元素时,会自动调用这两个函数,这两个函数是对operator new和operator delete进行的一层封装,也就是在调用operator new[]和operator delete[]后转到operator new/operator delete进行处理,再转到malloc/free进行处理,并且当malloc时会一次开辟出多个元素大小的空间
new和delete实现原理关系图及其解析
我们这里以new为例,delete实际上是与new对应的
申请单个元素空间时:
new:首先调用operator new,再在调用完operator new的基础上,自动调用类型的构造函数
operator new:首先调用malloc,再在调用完malloc的基础上,检查是否有异常
delete:首先调用operator delete,再在调用完operator delete的基础上,自动调用类型析构函数
operator delete:首先调用free,这是一个为了与operator new进行配套设计出来的一个函数
申请多个元素空间时:
new:首先调用operator new[],再在调用完operator new[]的基础上,自动调用类型的构造函数
operator new[]:调用operator new
operator new:首先调用malloc,再在调用完malloc的基础上,检查是否有异常
delete:首先调用operator delete,再在调用完operator delete的基础上,自动调用类型析构函数
operator delete[]:调用operator delete
operator delete:首先调用free,这是一个为了与operator new进行配套设计出来的一个函数
补充的一个问题
当我们调用operator new[]的时候,我们会发现其实实际存储是比sizeof(对象)*个数大四个字节的,这四个字节是存储在第一个元素对象之前的(但返回的指针是指向第一个元素的位置),它存储的是你new出来的元素个数,目的是为了当我们delete的时候能知道他要调用多少个析构函数,但注意:operator new[]/operator delete[]才会存储/获取个数,operator new/operator delete不会
如下代码为什么会崩溃
#include <iostream>
class A
{
public:
A(int a = 0)
{
_a = a;
std::cout <<"A(int a = 0)" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
private:
int _a;
};
int main()
{
A* pA = new A[10];
delete pA;
return 0;
}
解析:上述代码中new了10个对象,那么根据我们所说operator new[]在第一个元素之前存储了元素对象个数,但特别要注意的是,operator new[]返回的是第一个元素的位置,此时我们调用的是delete,就会从第一个元素的位置开始释放,但我们是多开辟了四个字节呀,正确的释放位置应该是存储元素个数的位置开始释放,free不允许从你开辟的空间中间开始释放,也就导致了崩溃
如下代码为什么不会崩溃(vs2019Debug下运行)
#include <iostream>
class A
{
public:
A(int a = 0)
{
_a = a;
std::cout << "A(int a = 0)" << std::endl;
}
private:
int _a;
};
int main()
{
A* pA = new A[10];
delete pA;
return 0;
}
其实都是编译器的优化导致的,如果编译器不进行优化那么这个代码会崩溃,编译器看到你的成员都是内置类型,且没有析构函数,编译器就觉得析构函数直接不用调了,编译器在operator new[]的时候直接就不存储元素个数了,此时我没有多开那四个字节的空间,那么第一个元素对象就是我开辟空间的起点,operator new[]返回第一个元素对象的地址,释放的时候就不会出现从中间开始释放的情况!(不同编译器优化还不同)
通过以上例子,能说明的一个问题是:new和delete以及new[]和delete[]一定要配套使用,如果你只new了一个对象那么就用delete,如果你new了多个对象,就用delete[]
特殊玩法:定位new
前面我们说过,new会自动调用构造函数,但有着这么一个场景,我不希望new来自动调用构造函数,我就想显式调用构造函数可不可以呢?可以的,定位new解决的就是这么个问题
这里说明下c++中虽然无法显式调用构造函数,但可以通过指针显式调用析构函数
如下代码:
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
int main()
{
A* p = (A*)malloc(sizeof(A));
new(p)A(1);//定位new
return 0;
}
运行结果:成功运行并把p所指对象的成员赋值成了1
定位new的格式:new(指针)类型(值)
上面说了定位new是怎么调用的以及它的功能是什么,接下来我们需要了解定位new的使用场景
在实际开发中有一种场景,这种场景就是我们有时候需要频繁申请一两个对象的内存空间,但我们需要知道频繁向堆申请这么一个小内存空间,效率其实是非常慢的,对这种问题,有些人就提出了内存池的概念,即每次向堆申请稍微多一点的空间,放在内存池放着,这样我们就不用频繁的向堆申请空间了
好比父母给生活费的时候,如果是一天给你打一天的生活费,那么每个月30天你都需要向父母要,这是一件非常麻烦的事情,于是你跟你的父母说一次给一个月的生活费,对比起来这样子就比较方便了!内存池与这种场景是非常像的!
但内存池向堆申请空间,申请的空间肯定是不会进行初始化的,但后面我们需要用这个空间创建对象的时候就得显式调用构造函数了,此时定位new的价值也就出来了
标签:调用,int,c++,operator,new,delete From: https://blog.csdn.net/m0_73904148/article/details/137413048