淺析內存分配

源碼面前,了無祕密 ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ-- 侯捷

今天打算來總結一下C++中的內存分配的一些事情,幾乎咱們寫的每一程序都離不開內存分配這個話題,而不一樣的程序對內存分配的需求又有不一樣,尤爲在一些嵌入式開發當中,經常須要程序員自定義內存分配的細節,因此今天的話題就從C++中的newdelete開始講起。程序員

1. new 和 delete

你可能會據說過newnew operatordelete以及delete operator,其實當你聽到這些概念的時候,說的就是newdelete,他們表示的都是C++中的操做符,C++中一般使用new表達式去爲對象分配內存,他們不容許被重載cookie

  • 當咱們使用new在堆上爲對象分配一塊空間時,以下
struct Complex {
  Complex() = default; // C++11用法,讓編譯器幫咱們生成默認構造函數(ctor)
  Complex(double real, double imag) : real_{ real }, imag_{ imag } {}

private:
  double real_;
  double imag_;
};

Complex* complex = new Complex(1.0, 2.0);
Complex* array = new Complex[10];  // 若是沒有默認ctor,這裏編譯器會出錯

實際上C++默默執行了下面三步操做函數

  • 首先調用全局命名空間的operator new(或 operator new[])函數來分配一塊原始內存,注意這塊內存並未初始化,關於operator new的細節咱們下一小節再來討論;
  • 轉型,將原始指針轉化爲對象類型;
  • 調用構造函數,分配空間,返回指向該對象的指針。

上面的new表達式就被編譯轉化爲相似下面的形式:性能

// new 先分配內存,在調用構造
void* mem = operator new(sizeof(Complex));
complex = static_cast<Complex*>(mem);
complex->Complex::Complex(1.0, 2.0);   // 這裏是不可以直接調用構造函數,這裏只是演示,可是能夠借用其它的手法調用,後面第3節咱們會說到

當咱們使用delete來釋放堆上分配的空間時,實際上執行了下面兩步操做操作系統

  • 先調用析構函數;
  • 調用全局命名空間的operator delete(或 operator new[])函數來釋放內存。
delete complex;
delete[] array;

實際上他們被編譯器轉化爲:設計

Complex::~Complex(complex);  //  調用析構函數(dtor)
operator delete(complex);

2. operator new 和 operator delete

newdelete不一樣,這兩個是C++標準定義的兩個全局函數,能夠被重載(C++11標準中分別給了6個重載的版本),用來定製特定的內存分配機制。指針

  • 破冰

前面咱們說到了newdelete表達式調用了operator newoperator delete來申請內存和釋放內存,其實這兩個函數底層調用的就是咱們熟知的mallocfree兩個函數。code

  • 伊始

再開始重載咱們本身的operator newoperator delete函數以前,先帶你們看一下他們在標準庫中的接口形式對象

void* operator new (std::size_t size);
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
void* operator new (std::size_t size, void* ptr) noexcept; // placemen
void* operator new[] (std::size_t size);
void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
void* operator new[] (std::size_t size, void* ptr) noexcept; // placement

void operator delete (void* ptr) noexcept;
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
void operator delete (void* ptr, void* voidptr2) noexcept; // placement
void operator delete (void* ptr) noexcept;
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
void operator delete (void* ptr, void* voidptr2) noexcept; // placement

// C++14 之後 operator delete 多引入了下面四種形式的重載
void operator delete (void* ptr, std::size_t size) noexcept; // with size
void operator delete (void* ptr, std::size_t size, const std::nothrow_t& nothrow_constant) noexcept; // nothrow with size
void operator delete[] (void* ptr, std::size_t size) noexcept;
void operator delete[] (void* ptr, std::size_t size, const std::nothrow_t& nothrow_constant) noexcept;

當咱們重載了本身的成員operator newoperator delete以後,咱們就能夠定製本身的內存分配行爲了,咱們通常都是重載成員operator newoperator delete,千萬要特別當心重載全局命名空間的和operator newoperator delete函數,這將影響到全部的newdelete行爲,通常不建議這麼作。接口

  • 一個簡單的內存池實現

咱們經過一個簡單的內存池實現來看看如何重載這些函數。

  • 接口
// allocator.h
struct Allocator {
  void* allocate(size_t size);
  void deallocate(void* head);

private:
  struct obj {
    obj* next;    // embedded pointer
  };

  obj* free_head_;
  const size_t chunk_ {20};
};
  • 實現
void* Allocator::allocate(size_t size) {
    obj* temp;

    if (free_head_ == nullptr) {
        free_head_ = static_cast<obj*>(malloc(size * chunk_));

        temp = free_head_;
        for (int i = 0; i < chunk_ -1; ++i) {
            temp->next = (obj*)((char*)temp + size);
            temp = temp->next;
        }
        temp->next = nullptr;
    }

    temp = free_head_;
    free_head_ = temp->next;
    return temp;
}

void Allocator::deallocate(void *head) {
    obj* temp = static_cast<obj*>(head);
    temp->next = free_head_;
    free_head_ = temp;
}

上面咱們定義了一個Allocator的類,將分配的內存塊經過鏈表級聯在一塊兒,默認一次申請20個對象的大小的塊,這個值根據不一樣狀況你能夠修改,或者在構造的時候傳入都行,根據實際狀況。
實現了對一塊大的內存的自我管理,申請的時候將free_head_ 指向的內存給用戶,釋放的時候將內存插入到鏈表頭部。當內存不足的時候又會從新申請20個對象大小的內存塊。

  • 優勢

    1. 獲得的每一大塊的內存都是連續的,減小了malloc函數內存的浪費,malloc函數在申請內存的時候會在返回給用戶的指針前面和後面插入一些額外的cookie信息,爲了free的時候能夠知道釋放多大的內存;想要更深刻的瞭解能夠參考effective C++第三版的條款50和51;
    2. 減小了malloc的調用次數,不過帶來的性能不會特別大,malloc的效率其實很是高。
  • 缺點

上面的實現一個很大的不足就是,咱們將從操做系統申請的內存一直握在本身的手裏,雖然沒有發生內存泄漏,可是沒能將內存再次還給操做系統。

  • 使用實例
// word.h
struct Word {
  Word () = default;
  Word (int size, int data) : size_(size), data_(data) {}

  static Allocator allocator;
  static void* operator new(size_t size) { return allocator.allocate(size); }
  static void* operator new[](size_t size) { return allocator.allocate(size); }
  static void operator delete(void* pointer) { return allocator.deallocate(pointer); }
  static void operator delete[](void* pointer) { return allocator.deallocate(pointer); }
  static void* operator new(size_t size, void* start) { return start; }  // 這是一個placement new

private:
  int size_;
  int data_;
};
Allocator Word::allocator;

咱們能夠重載不少個class member operator new(),前提是每個重載的版本第一參數必須爲size_t類型。

3. placement new 和 placement delete

  • 一個簡單的例子

第1節咱們留下了一個問題,咱們說下面的代碼是不可以直接調用構造函數的

complex->Complex::Complex(1.0, 2.0);

咱們將它稍微改寫一下,變成下面的形式就能夠調用構造函數了,其實下面的形式,就是咱們這一節要說的placement new,又叫定點new或者定位new

new(complex)Complex(1.0, 2.0);
  • 做用

它用於在給定的內存中初始化對象(不會分配內存),對於 operator new 分配的內存空間來講咱們沒法使用構造函數來構造對象。這個時候咱們可使用placement new形式來構造對象。
另外placement new容許咱們在一個特定的、預先分配的內存地址上構造對象,這個地址不只僅是堆上的內存(如上所示),也能夠是棧上分配的空間,以下

std::string str[3];
for (int i = 0; i < 3; ++i) {
  new(str+i)std::string("num is " + std::to_string(i));
  std::cout << *(str+i) << std::endl;
}

// output:
num is 0
num is 1
num is 2
  • 參考

關於placement new的詳細部分能夠參考effective C++ 第三版的條款52以及C++ Primer 第五版的19.1.2小節。

4. new_handler 和 set_new_handler

operator new分配內存失敗的時候,會拋出一個std::bad_alloc異常。在一些老的編譯器可能不會拋出異常,而是返回零,不過你能夠顯示讓編譯器不拋出異常

new(std::nothrow)int[10];
// 稱爲nothrow形式
  • 形式

C++平臺在拋出異常以前,會先調用一個函數,並且不止一次,這個函數能夠由client指定的handler,下面咱們看看new_handler的形式和設定方法

typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw(); // C++98
new_handler set_new_handler(new_handler p) noexcept(); // C++11

說明一下,set_new_handler尾端聲明的throw(),表示該函數不拋出異常,不過在C++11的時候被標記爲廢棄,改成noexcept,到C++17的時候throw()這種用法已經完全被刪除了。
C++平臺這樣的設計是爲了給用戶一個機會,在內存不足的時候調用用戶本身設定的handler,也就是由你來決定這個時候該如何抉擇。

  • 設計選擇

好的new_handler設計,通常有兩個選擇。

1. 想法設法讓更多的內存可用,釋放系統當前能夠釋放的空閒資源;
2. 調用`abort()`或`exit()`來終止程序。

5 . 補充

C++2.0以後引入兩個新特性,一個是= delete,另外一個是get_new_handler,分別簡單介紹一下。

  • = delete

咱們能夠在operator newoperator delete函數尾部加上= delete,用來表示刪除這個函數,不容許使用者調用。

// word.h
struct Word {
  Word () = default;
  static void* operator new(size_t size) = delete;
  static void* operator new[](size_t size) = delete;
  static void operator delete(void* pointer) = delete;
  static void operator delete[](void* pointer) = delete;
};

// 下面四條語句都會compile error
Word* word = new Word();
Word* words = new Word[3];
delete word;
delete[] words;
  • get_new_handler

用來獲取new-handler函數,若是用戶沒有設定的話或者被重置,將返回一個nullptr

new_handler get_new_handler() noexcept;
相關文章
相關標籤/搜索