C++中的new和delete真的复杂吗?(上)

作者: null122 | 来源:发表于2016-05-28 22:56 被阅读973次
    C++相关笔试面试中new与malloc以及delete与free是考察被面试者C++基本功的重点,也是难倒众多C++开发
    人员的一个相关难点。所以很多人就说C++中new和delete很复杂,运行机制很隐秘,运行原理很神奇。但真的是
    这样的吗? 今天我就为大家揭盖new与delete的神秘面纱!
    

    如果想要了解内存对齐的秘密就来点这个链接吧!

    在C++中newdelete分别是关键字成员中的一员。但大家也都称呼他们为 “运算符” 这是为什么呢?答案就在他们的运行过程中揭晓!

    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 operatoroperator newplacement new
      new operator: 我们平时使用的new
      operator newnew 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还要继续调用构造函数。
      这下我们就明白了newmalloc的区别了,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转载请注明出处

    相关文章

      网友评论

      • bruce_zhou:C++太复杂了
      • T4Technology:关于c++的文章好少啊,我都弃它学java了
        T4Technology:@null122 而且现在我也后悔了,java这种封装完美的语言我总要为它jar包的bug买单
        T4Technology:@null122 我的意思是,我这两句是分开的,我放弃他学java不是因为这个。。。
        null122:@T4Technology c++经典书籍很多,只要肯下功夫,很多其他语言搞不清的问题都可以搞清楚。

      本文标题:C++中的new和delete真的复杂吗?(上)

      本文链接:https://www.haomeiwen.com/subject/judfdttx.html