C++幕後故事(九)--我們來new個對象

讀者如果覺得我文章還不錯的,文章可以轉發,但是必須保留原出處和原作者署名。

今天我們主要學習知識點:

1.new的調用流程。
2.我們重載了new之後能幹啥。
3.placement new幹啥的。
4.set_new_handler是什麼。

1. operator new操作符的原理

1.1 operator new 調用流程

測試代碼如下:
/****************************************************************************
**
** Copyright (C) 2019 [email protected]
** 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章