C++中的new和delete真的复杂吗?(上)
C++相关笔试面试中new与malloc以及delete与free是考察被面试者C++基本功的重点,也是难倒众多C++开发
人员的一个相关难点。所以很多人就说C++中new和delete很复杂,运行机制很隐秘,运行原理很神奇。但真的是
这样的吗? 今天我就为大家揭盖new与delete的神秘面纱!
如果想要了解内存对齐的秘密就来点这个链接吧!
在C++中new
与delete
分别是关键字成员中的一员。但大家也都称呼他们为 “运算符” 这是为什么呢?答案就在他们的运行过程中揭晓!
1.new关键字
C++ Prim Plus中如此描述new运算符:
new运算符根据类型来确定需要多少字节的内存。然后它找到这样的内存,并返回它的地址。
要想了解new
运算符具体干了那些事,我们就必须分析new
的具体执行过程。
注意new的实现虽然根据不同的编译环境不同,但大体过程基本相同。
a = new int;
00361270 push 4
00361272 call dword ptr ds:[3630A8h]
00361278 add esp,4
他要去哪呢?步入跟进我们发现原来跳转到了:
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
为什么呢?这是因为C++中提到的new
,至少可能代表以下三种含义:new operator
、operator new
、placement new
。
new operator: 我们平时使用的new
。
operator new: new operator
的第一步是通过operator new
完成的。这里的new
就相当于一个运算符号,是可以重载的。
因为我们调用的是new operator
所以它调用operator new
来完成工作。
这个operator new
函数看起来样子是好可怖,但不比担心我们看下他到底做了什么?
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{ // try to allocate size bytes
//__CRTDECL 就是 __cdecl 感兴趣大家可以自行研究
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{ // report no memory
_THROW_NCEE(_XSTD bad_alloc, );
}
return (p);
}
我们仔细观察就会发现 operator new
现是调用了malloc
函数申请内存,而当申请失败也就是返回空指针时,判断 _callnewh(size)
返回值是否为0
,若为0
则抛出一个异常,非0
则继续循环执行malloc
。
_callnewh
的作用就是调用一个被称作new_handler
的函数,这里注意,在有些文章中说_callnewh
是一个new_handler
这是不正确的,_callnewh
只是调用new_handler
,作用类似与回调函数!
看清operator new
的内部结构后我们发现,在简单的数据类型情况下,原来这个函数的功能十分简单,就是以malloc
为主体,对malloc
申请失败的情况做了一下特殊的处理。
而对于new_handler
函数,在VS
中我们可以使用_set_new_handler
函数来设置。
#include <iostream>
#include <new.h>
using namespace std;
int MyNewHandler(size_t size)
{
cout << "MyNewHandler out" << endl;
return 1;
}
int main()
{
_set_new_handler(MyNewHandler);
while (true)
{
int* a = new int[10000000];
}
return 0;
}
通过以上程序可以很直观的了解new_handler
的作用情况。
2.new在复杂情况的表现:
通过上面这个例子我们知道了new
在简单类型下的具体执行过程,那么new
在复杂类型下又是怎么执行的呢?
我们看一下下面这个例子:
class MyObject
{
public:
int a;
MyObject()
{
a = 1;
}
};
int main()
{
MyObject *m = new MyObject();
return 0;
}
这是他在DEBUG模式下的汇编码:
MyObject *m = new MyObject();
003E147D push 4
003E147F call operator new (03E1190h) // 调用operator new
003E1484 add esp,4
003E1487 mov dword ptr [ebp-0E0h],eax
003E148D mov dword ptr [ebp-4],0
003E1494 cmp dword ptr [ebp-0E0h],0
003E149B je main+70h (03E14B0h)
003E149D mov ecx,dword ptr [ebp-0E0h]
003E14A3 call MyObject::MyObject (03E119Fh) // 调用构造函数!
003E14A8 mov dword ptr [ebp-0F4h],eax
003E14AE jmp main+7Ah (03E14BAh)
注意以上汇编码在Release模式下可能无法得到,因为编译器为了执行速度,在编译时会对Release代码做出特殊优化。
通过上面的代码我们就可以很直观的看出new
在复杂类型时的执行过程:先调用operator new
分配空间,再调用构造函数进行初始化。
3.new的执行过程:
现在new
的执行过程就很清楚了:
new -> operator new -> malloc
这是new的基本部分,如果内存分配成功,那么operator new就会直接返回。
而内存分配出错,也就是malloc返回指针为空:
malloc出错 -> 调用new_handler -> 若new_handler返回为0 -> 抛出异常
|
v
若new_handler返回非0 -> 继续调用malloc
若是简单类型那么new
到这里基本就结束了,但要是复杂类型,new
还要继续调用构造函数。
这下我们就明白了new
和malloc
的区别了,new
会调用malloc
进行内存分配的操作。但他和malloc
不用的是,他分配失败时会调用new_handler
,而new_handler
返回0的情况抛出异常,而malloc
只会返回一个空指针。
4.重载new运算符
前面说过大家称呼new
关键字为“运算符”,而我们知道在C++中运算符是可以重载的,那么是否意味着我们可以为我们自己的类定制一个new
运算符呢?答案是肯定的!
class MyClass
{
public:
MyClass()
{
_val = 1;
}
void * operator new(size_t size)
{
std::cout << "MyClass operator new!" << std::endl;
return ::operator new(size);
}
private:
int _val;
};
int main()
{
MyClass *m = new MyClass();
std::cin.get();
return 0;
}
上面的例子演示了一个重载operator new
的例子。
上面的例子中在调用全局的operator new
之前,我们加入了自己的特殊处理,不过要注意返回值是 void *
。
5.new[]
相对于new,new[]
多了一些步骤。
简单类型new[]
中,首先new[]
调用了operator new[]
。
void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)
{ // try to allocate count bytes for an array
return (operator new(count));
}
operator new[]
根据所需数目调用operator new
。
复杂类型中执行完operator new[]
后还会利用一个vector constructor iterator
来记录new所需的构造函数的地址等信息。
那么编译器是如何知道要new
多少个元素呢?原来在new[]
时编译器会在数组的头部也就是数组指针所指向的位置加上数组的长度,也就是一个四字节的_DWORD
。
也正是这四个字节导致我们使用new[]
创建复杂类型数组之后,无法使用delete
来释放而只能使用delete[]
来释放。
想了解delete
细节吗?那么我们下篇文章再来细说delete
!
附vector constructor iterator结构:vector_constructor_iterator_(数组首地址, 对象大小, 数组个数, 对象构造函数, 对象析构函数);(不同实现下可能会有差别)
简书●null122转载请注明出处