(注:本文是针对上篇文章中C++内存管理的两个关键字)两个关键字原理的解析,对于这两个关键字的使用并没有什么影响,如果只想得知两个关键字的使用方法,则可以直接跳过本篇文章)
目录
1. 引入:
2.operator new 与 operator delete:
2.1 基本定义以及与操作符的差异:
2.2 为什么要引入operator new和operator delete:
3. 操作符的大致动作过程:
3.1 开辟单个空间的动作过程:
3.2 开辟多个空间的动作过程:
1. 引入:
为了方便说明两个关键字的实现原理,首先引入一个简单的栈,具体代码如下:
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack( int capacity = 4)" << endl;
_a = new int[capacity];
_capacity = capacity;
_top = _capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[]_a;
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
return 0;
}
运行代码,结果显示调用了一次构造函数和一次析构函数:
对于下方给出的代码,即:
Stack* s2 = new Stack;
delete s2;
整体的运行顺序为:利用关键字开辟一个类型为自定义类型的空间,大小为字节。此后,由自定义类型的构造函数可知,再利用关键字为指针变量_开辟空间。因此,第一行代码整体开辟了两次空间。第一次是自身开辟空间,第二次是针对自定义类型会去调用自定义类型的构造函数,在构造函数中,再开辟一次空间。
对于第二行代码中的关键字。首先需要调用析构函数,析构函数的作用并非像一样释放掉开辟的空间,而是释放掉空间中的资源,也就是指针变量_指向的空间。在调用完析构函数后,再去释放空间。此处可以看出来,针对自定义类型,在释放空间时,并不能区调用。因为并不会处理指针变量_中已经开辟的空间。因此会导致内存泄漏。
由上面的例子和上篇文章引入关键字使用方法的例子可以了解,针对内置类型与并没有差异,针对自定义类型,比多了一步调用默认构造函数。对于,针对内置类型与也没有差异,针对自定义类型,多了一步在释放空间之前调用一次析构函数。所以,这两个关键字可以看作对的加强。对于这两个关键字开辟空间或者释放空间的功能的原理,是借助,完成的。需要注意,上面给出的是两个全局函数,并非运算符重载。下面将针对这两个全局函数进行解析。
2.operator new 与 operator delete:
2.1 基本定义以及与操作符的差异:
和并不是运算符重载,而是两个全局函数,对于,其运用方式与基本相同,与的调用方式也基本相同。二者与前面的操作符在运行中也有一定的差距,例如:
Stack* s2 = new Stack;
delete s2;
Stack* s3 = (Stack*)operator new(sizeof(Stack));
operator delete(s3);
运行后结果如下:
不难发现,两个全局函数只能开辟空间,并不能像操作符一样调用构造函数或者析构函数。
对于这两个全局函数,具体代码如下:
(注:对于下方给出的代码在此阶段并不需要知道具体含义,在文章的后面,需要引用其中某行代码时,会给出相应的解析)
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK);
__TRY
pHead = pHdr(pUserData);
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK);
__END_TRY_FINALLY
return;
}
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上面给定代码中的其中两行,即:
while ((p = malloc(size)) == 0)
_free_dbg( pUserData, pHead->nBlockUse );
不难看出,和这两个函数可以看作是对和这两个函数的封装。而对于为什么要对进行一次封装再使用,而不直接使用,将在下一小节进行简要说明。
2.2 为什么要引入operator new和operator delete:
若调用开辟空间失败,则一般会返回。但是,在中,面向对象的编程并不能在失败用返回值进行处理,而是需要抛异常,对进行封装,正是为了解决这个问题
(注:对于抛异常等相关内容将会在后续的文章中给出,在此阶段只需要这个概念即可)
在上面给出的代码中,虽然具体内容并不能了解清楚,但是对于下面的代码,即:
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
在介绍时就提到,当成功的开辟空间后,返回值会返回这块空间的起始地址。因此,上述代码的大体意思为:检查的返回值,如果返回值判断等于,则说明没有成功的开辟 地址,下面就进行抛异常。对于的封装大致意思也相同,此处不再过多介绍。
3. 操作符的大致动作过程:
3.1 开辟单个空间的动作过程:
前面简单介绍了两个全局函数和。本部分将介绍操作符的大致动作过程。
(注:为了清楚的了解的动作过程,需要通过汇编进行查看,本部分并不需要了解汇编代码,只是借用其中的几行来大体说明动作过程,并且针对借用的代码给出解析)
对于下面给出的代码:
Stack* s2 = new Stack;
转为汇编形式,即为:
在上面的指令中,可以找到较为熟悉的两行指令,即:
二者分别对应了操作符的两个动作,即:调用函数开辟空间,调用构造函数对空间进行初始化。 对于操作符同理,其汇编指令如下:
其中,红色框框出来的两行分别为:调用析构函数与调用释放空间。
3.2 开辟多个空间的动作过程:
给定代码如下:
Stack* s4 = new Stack[10];
delete[]s4;
将上述代码转为汇编形式,涉及到的指令如下:
可以看到,大致的运行过程是先调用指令,下一步直接会转到。其中的表示开辟空间的大小。