C++幕后故事(九)--我们来new个对象

读者如果觉得我文章还不错的,,希望可以多多支持下我,文章可以转发,但是必须保留原出处和原作者署名。为了获取更好的阅读体验请关注微信公众号。更多内容请关注我的微信公众号:cpp手艺人

image

今天我们主要学习知识点:

1.new的调用流程。
2.我们重载了new之后能干啥。
3.placement new干啥的。
4.set_new_handler是什么。

1. operator new操作符的原理

1.1 operator new 调用流程

测试代码如下:
/****************************************************************************
**
** Copyright (C) 2019 635672377@qq.com
** All rights reserved.
**
****************************************************************************/

/*
    测试对象的new、delete,在VS2017更容易观察
*/
#ifndef obj_new_delete_h
#define obj_new_delete_h

#include <new>
#include <memory>
#include <iostream>

using std::cout; 
using std::endl;

namespace obj_new_delete 
{

class Obj
{
public:
    Obj():mCount(0) { cout << "Obj ctor" << endl; }
    ~Obj() { cout << "~Obj dtor" << endl; }

private:
    int mCount;
};

void test_new_obj()
{
    Obj *obj = new Obj();
delete obj;
}
}
#endif // obj_new_delete_h 

我们在Obj *obj = new Obj();处下个断点,再打开反汇编窗口,我摘取主要的代码。
markdown的汇编代码不能高亮,看的很难受啊,还是前往公众号可以获取更好的阅读体验。

; Obj *obj = new Obj();
00DE2C47  push        4  
00DE2C49  call        operator new (0DE141Ah)  
00DE2C4E  add         esp,4  
00DE2C51  mov         dword ptr [ebp-0ECh],eax  
00DE2C57  mov         dword ptr [ebp-4],0  
00DE2C5E  cmp         dword ptr [ebp-0ECh],0  
00DE2C65  je          obj_new_delete::test_new_obj+7Ah (0DE2C7Ah)  
00DE2C67  mov         ecx,dword ptr [ebp-0ECh]  
; 调用对象的构造函数
00DE2C6D  call        obj_new_delete::Obj::Obj (0DE1456h)  

我在::operator new的汇编代码处,点击菜单“转到源代码”,就可以还原为C++代码,这个代码的源文件叫做new_scalar.cpp:

void* __CRTDECL operator new(size_t const size)
{
    for (;;)
{
    // 这个有个小技巧,控制变量的作用域,没必要让其他对象见到这个变量的作用域
        if (void* const block = malloc(size))
        {
            return block;
        }
        // _callnew内部会调用new_handler,
// 返回值为0表示new_handler类型函数为null,这样就不会调用new_handler类型函数,
// 抛出两个其中一个的异常,大小异常,内存分配异常。
        // 否则就会调用设置的new_handler类型函数
        if (_callnewh(size) == 0)
        {
            if (size == SIZE_MAX)
            {
                __scrt_throw_std_bad_array_new_length();
            }
            else
            {
                __scrt_throw_std_bad_alloc();
            }
        }
        // The new handler was successful; try to allocate again...
}
}

到这里,我们基本上能够知道operator new调用过程。

image

从代码上看operator new做了两件事:

  1. 获取到新的内存。
  2. 调用对象的构造函数(从汇编代码看,这一步是编译器插入的,但是很多书上把这一步归为operator new。)

1.2 重载new操作符

代码如下:
// 重载global new
void * operator new(size_t const size)
{
    return malloc(size);
}

// 重载global delete
void operator delete(void *head)
{
    return free(head);
}

class Obj
{
public:
    Obj():mCount(0) { cout << "Obj ctor" << endl; }
    ~Obj() { cout << "~Obj dtor" << endl; }

    // 重载局部 new
    void *operator new (size_t const size)
    {
        return static_cast<Obj *>(::operator new(size));
    }
    // 重载局部delete
    void operator delete(void *head)
    {
        return ::operator delete(head);
}

private:
    int mCount;
};

其实重载operator new的代码很简单。

重载局部operator new,只需要将operator new作为类的普通成员变量就可以
重载全局operator new,只要在全局位置声明::operator new的实现函数

1.3 重载operator new有啥用?

实话说,这个重载的作用非常之大。 说一个自己经历过的项目bug。因为项目着急上线,准备发包的前夕,测试反馈说测试大量数据时,软件发生偶然性的崩溃,时间不固定。当时都准备收拾书包回家了,听到有bug,心中真的有万马奔腾感觉。没办法,只好查看生成dump文件,windbg挂上。发现是在多线程下野指针的问题,不知道谁释放了资源,又进行了二次释放。当时在晚上思维都有点迟缓了,调到了后半夜都没有解决的思路。最后想到了一招,就是重载了operator new和operator delete,在这两个函数里面记录一些标志性的信息,最后定位问题的所在。那一夜,我见到了上海5点钟的软件园灯火闪烁。
  1. 可以用来检测运用上的错误
  2. 可以提高效率,节省不必要的内存,提高回收和分配的速度(比如针对某一对象的内存池)
  3. 可以收集对内存使用的数据统计。

tips:

1.malloc(0)会返回一个正常的地址,但是这个地址不能存储任何东西。

2. placement new

2.1.什么是placement new?

> 在用户指定的内存上构建对象,这个过程不会申请新的内存,只会调用对象的构造函数即可。

看代码:

void test_placement_new()
{
    char *buff = new char[sizeof(Obj)];
    Obj *ojb = new(buff)Obj();
}

老套路,在VS 2017下转到反汇编的窗口。

          ; char *buff = new char[sizeof(Obj)];
00892558  push        4  
0089255A  call         operator new[] (0891118h)  
0089255F  add         esp,4  
          ; eax中保存了new char返回的首地址
00892562  mov         dword ptr [ebp-0E0h],eax  
00892568  mov         eax,dword ptr [ebp-0E0h]  
0089256E  mov         dword ptr [buff],eax  
          ; Obj *ojb = new(buff)Obj();
          ; 将eax中的首地址作为参数传递进去
00892571  mov         eax,dword ptr [buff]
00892574  push         eax  
          ; Obj对象的大小
00892575  push        4  
00892577  call          operator new (0891550h)  
0089257C  add         esp,8  
0089257F  mov         dword ptr [ebp-0ECh],eax  
00892585  mov         ecx,dword ptr [ebp-0ECh]  
          ; 调用Obj的构造函数
0089258B  call          obj_new_delete::Obj::Obj (0891456h)  
00892590  mov         dword ptr [ojb],eax  

再调用operator new,我单步调试进去发现调用的operator new函数原型如下:

_Ret_notnull_ _Post_writable_byte_size_(_Size) _Post_satisfies_(return == _Where)
inline void* __CRTDECL operator new(size_t _Size, _Writable_bytes_(_Size) void* _Where) noexcept
{
    (void)_Size;
    return _Where;
}
  1. 函数原型为:inline void* __CRTDECL operator new(size_t _Size, void* _Where) noexcept,返回用户传入的地址。
  2. 调用类的构造函数(从汇编角度看,构造函数代码是编译器插入的,但是很多书上把这一步归为placement new)。

2.2 placement new作用

以前刚学到这个语法的时候,我觉得这个能有啥用,谁这么无聊在同一块内存捯饬个不停。直到有天我看到vector的实现源码,才知道当年的自己想法还是很白开水的。 在STL容器中vector在申请内存的时候为了提高效率,每次申请的内存都是比实际需要的大2倍。但是这跟placement new有什么关系。

我举个例子说明:

你向vector中push了2个元素,此时vector的实际内存是为4个的。这是你再向vector中push一个元素,因为vector还有2个未用的空间,所以不需要申请内存。这样就可以在原来那块已经分配好的内存中调用元素的构造函数就可以了。而这里就恰恰用到了placement new了。

我摘抄SGI STL 3.0版本的里面的源代码供大家参考下

template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
    new (p) T1(value);
}

// vector push_back函数
void push_back(const T& x) {
    if (finish != end_of_storage) {
      construct(finish, x);         
      ++finish;                             
    }
    else                               
      insert_aux(end(), x);         
}

2.3 placement new重载

代码如下:
class Obj
{
public:
    Obj():mCount(0) { cout << "Obj ctor" << endl; }
    ~Obj() { cout << "~Obj dtor" << endl; }

    // 重载内置版本placement new
    void *operator new(size_t size, void *address)
    {
        cout << "void *operator new(size_t size, void *address) version" 
             << endl;
        return address;
    }

    // 重载内置版本placement delete
    void operator delete(void *buff, void *address)
    {
        return ::operator delete(buff, address);
    }

    // 重载placement new
    void *operator new(size_t size, void *address, long extra)
    {
        cout << "void *operator new(size_t size, void *address, long extra) "
                " version" 
             << " extra:" << extra <<endl;
        return address;
}

    // 重载placement delete
    void operator delete(void *buff, void *address, long extra)
    {
        return ::operator delete(buff, address);
    }
private:
    int mCount;
};

void test_placement_new()
{
    char *buff = new char[sizeof(Obj)];
    // Obj *ojb = new(std::cerr)Obj();
    Obj *obj = new(buff)Obj();
    obj->~Obj();
Obj *obj_1 = new(buff, 123456)Obj();
    obj_1->~Obj();

delete buff;
    // 打印结果
    // void *operator new(size_t size, void *address) version
    // Obj ctor
    // ~Obj ctor
    // void *operator new(size_t size, void *address, long extra) version extra:123456 
    // Obj ctor
    // ~Obj ctor
}

2.4 placement new注意事项

> 1. 如果你重载了placement new,那么一定要重载对应的placement delete。因为在对象的构造函数抛出异常时,C++运行时系统需要找到对应的placement delete去释放内存。否则可能有内存泄露的风险。 > 2. 重载了placement new会隐藏正常operator new导致编译语法错误。如何避免后面我有机会写出完整的代码。 > 3. 使用了placement new,需要对内存的释放要注意,并不能直接delete。因为这块内存并不是你来申请的,你应该直接调用你自己对象的析构函数。

3.set_new_handler

3.1 .set_new_handler是什么?

> 首先它是个位于std命名空间的全局函数,参数类型为函数指针 void (*new_hander)()

3.2 set_new_handler有什么用?

> 当用户使用operator new无法返回正确的内存地址,这时C++编译器就会调用一个客户指定的错误处理函数。这个错误处理函数就是通过set_new_handler来指定的。

3.3 new_handler函数注意事项

> 1. new_handler让更多内存可以被使用。 > 2. 如果不能获取到更多有用的内存抛出异常或者直接终止程序运行。(必须要这样做,因为new包含是死循环操作)

写一个实例代码:

void new_exception_handler()
{
    cout << "memory cout..." << endl;
    abort();
}

void test_new_obj()
{
    // 测试new_set_handler
    std::new_handler old_handler = std::set_new_handler(new_exception_handler);

    while (true) {
        new int[100000000ul];
    }
    cout << "end..." << endl;
    // memory cout...
}

4.总结

看到operator new的源代码,会发现原理很简单,本质上是对malloc的封装。从汇编代码看,operator new其实返回malloc返回的内存,而构造函数的代码其实编译器插入的。当调用operator new发生异常时,C++运行时系统会负责回收内存。

operator new重载作用非常的大,其中最重要的也是最常见的就是内存池实现。
placement new在某一些场景还是非常有用的。如果重载了placement new,那么必须也需要重载placement delete。

set_new_handler就是设置一个全局的回调函数,在operator new异常情况下就会调用我们设置的new_handler类型函数。

image


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 635672377@qq.com

文章标题:C++幕后故事(九)--我们来new个对象

文章字数:2.5k

本文作者:刘世雄

发布时间:2020-01-10, 14:09:40

最后更新:2020-01-10, 06:10:34

原始链接:http://lsxcpp.com/2020/01/10/cpp-9-new-object/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录