參考博客:https://blog.csdn.net/zhong29/article/details/80930919
malloc的實現方式
malloc函數將內存空間中可用的內存塊連接成一個很長的空閒鏈表。當調用malloc函數的時候,就會在鏈表中進行遍歷,直到找到滿足用戶申請空間大小的內存塊,然後就從鏈表中把這段內存拿出來分配給用戶,剩下的接回原空閒鏈表。如果無法獲取符合大小的內存塊的時候,就會放回NULL指針,因此如果用malloc申請空間就一定要進行返回值判斷。
同時用malloc返回的指針是對齊的,爲了能夠使得void*適用於任何對象。大多數實現所分配的空間都會比申請空間來的大一些,額外的空間用來記錄管理信息——分配塊的長度、指向下一個分配塊的指針。
當用戶調用free函數的時候就把申請的內存塊重新接到空閒鏈表上。
這裏提一下realloc,realloc函數可以增減之前分配的空間的長度,如果發現當前的存儲區域的高位地址上沒有足夠的空間了,就會分配一個新的足夠大的存儲區域,將現有的內容複製過去,同時釋放原有存儲區域的內容。這裏要注意的是,因爲realloc可能會移動位置,如果用指針指向該地址,就有可能使得指針無效
new的實現
先看下new的源碼
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
p = (void *) malloc (sz);
while (p == 0)
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
p = (void *) malloc (sz);
}
return p;
}
從源碼可以看出實際上new也只是申請了內存空間。這裏涉及到一個new_handler,他的作用就是反覆申請內存空間,如果這個new_handler做不了這個工作,那麼就換新的new_handler,如果沒有new_handler能做這個工作就把一個空指針傳給handler,就可以實現異常的拋出。
總結下new_handler的工作:
- Make more memory available,使得下一次內存分配能夠成功,如刪除其他無用的內存,使得系統有更多可用的內存。
- Install a different new_handler,如果當前的new_handler分配不了這麼多內存,或者他知道另一new_handler能完成,則可以調用set_new_handler來設置另一個new_handler
- Deinstall the new_handler,就是如果沒有能分配這麼大內存的handler,就會把空指針返回。這樣就可以拋出異常
- Throw an exception,類型爲bad_alloc的異常不會被operator new捕獲,會被傳播到內存請求的地方。
那麼new的過程也可以理解了,首先調用operator new函數申請一片空間,然後改變指針類型從void* -> 某個指針類型,再進行構造函數的調用
pt=static_cast(operator new(sizeof(T)));
pt->T();
new和malloc的異同
- new和malloc都是在堆上開闢內存。malloc只開闢了空間,沒有初始化,而new不僅開闢了空間還進行了初始化。
- malloc需要傳入初始化的字節數,指定開闢空間的大小,而new就不用了,new的工作方式是就是上面代碼中顯示的,按照對象的大小獲取字節數,然後開闢空間大小。
- malloc失敗返回NULL,new失敗返回的是bad_alloc的異常,因此new對象需要異常捕獲來判斷(其實可以不用異常捕獲,但是判斷是一定需要的)
- malloc用free來釋放,new用delete來釋放,new[]—delete[]
- malloc只有一種開闢內存的方式,而new分爲普通new,nothrow的new,const new 和定位new
擴展知識1
- nothrow new
#include<new>
Manager* pManager = new (std::nothrow) Manager();
if(NULL==pManager)
{
return false;
}
//或者設置set_new_handle 自定義異常拋出函數
#include <new>
#include <iostream>
#include <stdlib.h>
using namespace std;
void __cdecl newhandler()
{
return;
}
int main()
{
set_new_handler (newhandler);
Manager * pManager = new (std::nothrow) Manager();
if(NULL == pManager)
{
//記錄日誌
return false;
}
}
- const new
暫不整理,
瞭解下常量指針和指針常量的區別
常量指針:const int *p=&a; 常量指針,這是一個指針,不過指向的內容是常量。const修飾數據類型,地址可以改,但是內容不可改int a=1,c=2; const int *b=&a; b=&c;//對的 *b=2;//報錯
指針常量:int * const p=&a; 指針常量,這是一個常量,就是指針本身是常量。因此地址不可以改,但是內容可以改。
int a=1,c=2; int * const b=&a; b=&c;//錯 *b=2;//對
- placement new
就是在指定的位置上來實現對象。
// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
從源碼來看placement new沒有申請新的空間,直接返回了第二個參數的指針。
那麼placement new的過程應該是
T p=new(alreadyExistPoint) T()
//分解步驟如下
void* p1=::operator new(sizeof(T),alreadyExistPoint);
T* p=static_cast<T*> p1;
擴展知識2
存儲空間的佈局
C程序一直由幾個部分組成
- 正文段。這裏是CPU執行的機器指令部分。通常正文段是可共享的,只讀的。
- 初始化數據段。 包含了程序中需明確的賦予初值的變量,例如除了函數外的聲明 int maxcount=999;
- 未初始化數據段。 在程序開始執行之前,內核將此段的數據賦予初值或者空指針。
- 棧。 自動變量和每次函數調用時所需要保存的信息都存放在此段中。每次函數調用時,其返回地址以及調用者的環境信息都存放在棧中。 對於遞歸函數,每次自身被調用時,就會產生一個棧幀,因此函數之間的變量集就不會互相干擾。
- 堆。 通常在堆中進行動態存儲的分配。
以上摘自UNIX環境高級編程
從上面的描述中我們可以知道棧是由編譯器自動分配和釋放的。而堆是由程序員來操作的。
如何只在堆上或者只在棧上創建對象呢。
簡單說下,只在堆上就是隻能通過new的方式來動態分配空間,因此就要想辦法禁用在編譯期間就創建對象的方式如 A a(args); 禁用的辦法就是講構造析構函數設爲protected函數,那麼編譯器就不會在編譯期間開闢棧空間給他。
class A{
protected:
A(){};
~A(){};
public:
static A* create(){
return new A();
}
void destory(){
delete this;//直接刪除傳入的this對象,成員函數能調用protected對象
}
};
接下來就是隻在棧上生成對象,很簡單就把operator new和operator delete禁用掉即可
class A{
private:
void* operator new(size_t){};
void operator delete(void* ptr){};
public:
A(){};
~A(){};
};