源碼面前,了無祕密 ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ-- 侯捷
今天打算來總結一下C++
中的內存分配的一些事情,幾乎咱們寫的每一程序都離不開內存分配這個話題,而不一樣的程序對內存分配的需求又有不一樣,尤爲在一些嵌入式開發當中,經常須要程序員自定義內存分配的細節,因此今天的話題就從C++
中的new
和delete
開始講起。程序員
你可能會據說過new
、new operator
、delete
以及delete operator
,其實當你聽到這些概念的時候,說的就是new
和delete
,他們表示的都是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
的細節咱們下一小節再來討論;上面的new
表達式就被編譯轉化爲相似下面的形式:性能
// new 先分配內存,在調用構造 void* mem = operator new(sizeof(Complex)); complex = static_cast<Complex*>(mem); complex->Complex::Complex(1.0, 2.0); // 這裏是不可以直接調用構造函數,這裏只是演示,可是能夠借用其它的手法調用,後面第3節咱們會說到
當咱們使用delete
來釋放堆上分配的空間時,實際上執行了下面兩步操做操作系統
delete complex; delete[] array;
實際上他們被編譯器轉化爲:設計
Complex::~Complex(complex); // 調用析構函數(dtor) operator delete(complex);
與new
和delete
不一樣,這兩個是C++標準定義的兩個全局函數,能夠被重載(C++11標準中分別給了6個重載的版本),用來定製特定的內存分配機制。指針
前面咱們說到了new
和delete
表達式調用了operator new
和operator delete
來申請內存和釋放內存,其實這兩個函數底層調用的就是咱們熟知的malloc
和free
兩個函數。code
再開始重載咱們本身的operator new
和operator 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 new
和operator delete
以後,咱們就能夠定製本身的內存分配行爲了,咱們通常都是重載成員operator new
和operator delete
,千萬要特別當心重載全局命名空間的和operator new
和operator delete
函數,這將影響到全部的new
和delete
行爲,通常不建議這麼作。接口
咱們經過一個簡單的內存池實現來看看如何重載這些函數。
// 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個對象大小的內存塊。
優勢
上面的實現一個很大的不足就是,咱們將從操做系統申請的內存一直握在本身的手裏,雖然沒有發生內存泄漏,可是沒能將內存再次還給操做系統。
// 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
類型。
第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小節。
當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()`來終止程序。
C++2.0
以後引入兩個新特性,一個是= delete
,另外一個是get_new_handler
,分別簡單介紹一下。
咱們能夠在operator new
和operator 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;
用來獲取new-handler函數,若是用戶沒有設定的話或者被重置,將返回一個nullptr
。
new_handler get_new_handler() noexcept;