本文主要講述C++ new運算符和operator new, placement new之間的種種關聯,new的底層實現,以及operator new的重載和一些在內存池,STL中的應用。
一 new運算符和operator new():
new:指我們在C++裡通常用到的運算符,比如A* a = new A; 對於new來說,有new和::new之分,前者位於std
operator new():指對new的重載形式,它是一個函數,並不是運算符。對於operator new來說,分為全局重載和類重載,全局重載是void* ::operator new(size_t size),在類中重載形式 void* A::operator new(size_t size)。還要注意的是這裡的operator new()完成的操作一般只是分配內存,事實上系統默認的全局::operator new(size_t size)也只是調用malloc分配內存,並且返回一個void*指針。而構造函數的調用(如果需要)是在new運算符中完成的。
先簡單解釋一下new和operator new之間的關系:
operator new can be called explicitly as a regular function, but in C++, new is an operator with a very specific
behavior: An expression with the new operator, first calls function operator new (i.e., this function) with the size of its type specifier as first argument, and if this is successful, it then automatically initializes or constructs
the object (if needed). Finally, the expression evaluates as a pointer to the appropriate type.
比如我們寫如下代碼:
A* a = new A;
我們知道這裡分為兩步:1.分配內存,2.調用A()構造對象。事實上,分配內存這一操作就是由operator new(size_t)來完成的,如果類A重載了operator new,那麼將調用A::operator new(size_t ),如果沒有重載,就調用::operator new(size_t ),全局new操作符由C++默認提供。因此前面的兩步也就是:1.調用operator new 2.調用構造函數。這裡再一次提出來是因為後面關於這兩步會有一些變形,在關於placement new那裡會講到。先舉個簡單例子
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">//平台:Visual Stdio 2008
#include<iostream>
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
A* a = new A;
delete a;
system("pause");
return 0;
}</SPAN>
//平台:Visual Stdio 2008
#include<iostream>
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
A* a = new A;
delete a;
system("pause");
return 0;
}下面我們跟蹤一下A反匯編代碼,由於Debug版本反匯編跳轉太多,因此此處通過Release版本在A* a = new A;處設斷點反匯編:
在Release版本中,構造函數和析構函數都是直接展開的。
[plain]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px"> A* a = new A;
01301022 push 1 ;不含數據成員的類占用一字節空間,此處壓入sizeof(A)
01301024 call operator new (13013C2h) ;調用operator new(size_t size)
01301029 mov esi,eax ;返回值保存到esi
0130102B add esp,4 ;平衡棧
0130102E mov dword ptr [esp+8],esi ;
01301032 mov dword ptr [esp+14h],0
0130103A test esi,esi ;在operator new之後,檢查其返回值,如果為空(分配失敗),則不調用A()構造函數
0130103C je wmain+62h (1301062h) ;為空 跳過構造函數部分
0130103E mov eax,dword ptr [__imp_std::endl (1302038h)] ;構造函數內部,輸出字符串
01301043 mov ecx,dword ptr [__imp_std::cout (1302050h)]
01301049 push eax
0130104A push offset string "call A constructor" (1302134h)
0130104F push ecx
01301050 call std::operator<<<std::char_traits<char> > (13011F0h)
01301055 add esp,8
01301058 mov ecx,eax
0130105A call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]
01301060 jmp wmain+64h (1301064h) ;構造完成,跳過下一句
01301062 xor esi,esi ;將esi置空,這裡的esi即為new A的返回值
01301064 mov dword ptr [esp+14h],0FFFFFFFFh
delete a;
0130106C test esi,esi ;檢查a是否為空
0130106E je wmain+9Bh (130109Bh) ;如果為空,跳過析構函數和operator delete
01301070 mov edx,dword ptr [__imp_std::endl (1302038h)] ;析構函數 輸出字符串
01301076 mov eax,dword ptr [__imp_std::cout (1302050h)]
0130107B push edx
0130107C push offset string "call A destructor" (1302148h)
01301081 push eax
01301082 call std::operator<<<std::char_traits<char> > (13011F0h)
01301087 add esp,8
0130108A mov ecx,eax
0130108C call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]
01301092 push esi ;壓入a
01301093 call operator delete (13013BCh) ;調用operator delete
01301098 add esp,4
通過反匯編可以看出A* = new A包含了operator new(sizeof(A))和A()兩個步驟(當然,最後還要將值返回到a)
delete a包含了~A()和operator delete(a)兩個步驟。
</SPAN>
A* a = new A;
01301022 push 1 ;不含數據成員的類占用一字節空間,此處壓入sizeof(A)
01301024 call operator new (13013C2h) ;調用operator new(size_t size)
01301029 mov esi,eax ;返回值保存到esi
0130102B add esp,4 ;平衡棧
0130102E mov dword ptr [esp+8],esi ;
01301032 mov dword ptr [esp+14h],0
0130103A test esi,esi ;在operator new之後,檢查其返回值,如果為空(分配失敗),則不調用A()構造函數
0130103C je wmain+62h (1301062h) ;為空 跳過構造函數部分
0130103E mov eax,dword ptr [__imp_std::endl (1302038h)] ;構造函數內部,輸出字符串
01301043 mov ecx,dword ptr [__imp_std::cout (1302050h)]
01301049 push eax
0130104A push offset string "call A constructor" (1302134h)
0130104F push ecx
01301050 call std::operator<<<std::char_traits<char> > (13011F0h)
01301055 add esp,8
01301058 mov ecx,eax
0130105A call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]
01301060 jmp wmain+64h (1301064h) ;構造完成,跳過下一句
01301062 xor esi,esi ;將esi置空,這裡的esi即為new A的返回值
01301064 mov dword ptr [esp+14h],0FFFFFFFFh
delete a;
0130106C test esi,esi ;檢查a是否為空
0130106E je wmain+9Bh (130109Bh) ;如果為空,跳過析構函數和operator delete
01301070 mov edx,dword ptr [__imp_std::endl (1302038h)] ;析構函數 輸出字符串
01301076 mov eax,dword ptr [__imp_std::cout (1302050h)]
0130107B push edx
0130107C push offset string "call A destructor" (1302148h)
01301081 push eax
01301082 call std::operator<<<std::char_traits<char> > (13011F0h)
01301087 add esp,8
0130108A mov ecx,eax
0130108C call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]
01301092 push esi ;壓入a
01301093 call operator delete (13013BCh) ;調用operator delete
01301098 add esp,4
通過反匯編可以看出A* = new A包含了operator new(sizeof(A))和A()兩個步驟(當然,最後還要將值返回到a)
delete a包含了~A()和operator delete(a)兩個步驟。
二 operator new的三種形式:
operator new有三種形式:
throwing (1) void* operator new (std::size_t size) throw (std::bad_alloc);
nothrow (2) void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
placement (3) void* operator new (std::size_t size, void* ptr) throw();
(1)(2)的區別僅是是否拋出異常,當分配失敗時,前者會拋出bad_alloc異常,後者返回null,不會拋出異常。它們都分配一個固定大小的連續內存。
用法示例:
A* a = new A; //調用throwing(1)
A* a = new(std::nothrow) A; //調用nothrow(2)
(3)是placement new,它也是對operator new的一個重載,定義於<new>中,它多接收一個ptr參數,但它只是簡單地返回ptr。其在new.h下的源代碼如下:
[cpp]
<SPAN style="FONT-FAMILY: Microsoft YaHei; FONT-SIZE: 14px">#ifndef __PLACEMENT_NEW_INLINE
#define __PLACEMENT_NEW_INLINE
inline void *__cdecl operator new(size_t, void *_P)
{return (_P); }
#if _MSC_VER >= 1200
inline void __cdecl operator delete(void *, void *)
{return; }
#endif
#endif</SPAN>
#ifndef __PLACEMENT_NEW_INLINE
#define __PLACEMENT_NEW_INLINE
inline void *__cdecl operator new(size_t, void *_P)
{return (_P); }
#if _MSC_VER >= 1200
inline void __cdecl operator delete(void *, void *)
{return; }
#endif
#endif
那麼它究竟有什麼用呢?事實上,它可以實現在ptr所指地址上構建一個對象(通過調用其構造函數),這在內存池技術上有廣泛應用。
它的調用形式為:
new(p) A(); //也可用A(5)等有參構造函數。
前面說到,new運算符都會調用operator new,而這裡的operator new(size_t, void*)並沒有什麼作用,真正起作用的是new運算符的第二個步驟:在p處調用A構造函數。這裡的p可以是動態分配的內存,也可以是棧中緩沖,如char buf[100]; new(buf) A();
我們仍然可以通過一個例子來驗證:
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">#include <iostream>
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* p = (A*)::operator new(sizeof(A)); //分配
new(p) A(); //構造
p->~A(); //析構
::operator delete(p); //釋放
system("pause");
return 0;
}</SPAN>
#include <iostream>
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* p = (A*)::operator new(sizeof(A)); //分配
new(p) A(); //構造
p->~A(); //析構
::operator delete(p); //釋放
system("pause");
return 0;
}
上面的代碼將對象的分配,構造,析構和釋放分離開來,這也是new和delete運算符兩句就能完成的操作。
先直接運行可以看到程序輸出:
再分別注釋掉new(a) A();和a->~A();兩句,可以看到對應的構造和析構函數將不會被調用。
然後查看反匯編:
平台: Visual Studio 2008 Debug版
[plain]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px"> A* a = (A*)::operator new(sizeof(A)); //分配
00F9151D push 1
00F9151F call operator new (0F91208h) ;調用::operator new(size_t size)也就是throwing(1)版本
00F91524 add esp,4
00F91527 mov dword ptr [ebp-14h],eax ;返回地址放入[ebp-14h] 即為p
new(a) A(); //構造
00F9152A mov eax,dword ptr [ebp-14h]
00F9152D push eax
00F9152E push 1 ;壓入p
00F91530 call operator new (0F91280h);調用operator new(size_t, void* p)即placement(3)版本 只是簡單返回p
00F91535 add esp,8
00F91538 mov dword ptr [ebp-0E0h],eax ;將p放入[ebp-0E0h]
00F9153E mov dword ptr [ebp-4],0
00F91545 cmp dword ptr [ebp-0E0h],0 ;判斷p是否為空
00F9154C je wmain+81h (0F91561h) ;如果為空 跳過構造函數
00F9154E mov ecx,dword ptr [ebp-0E0h] ;取出p到ecx
00F91554 call A::A (0F91285h) ;調用構造函數 根據_thiscall調用約定 this指針通過ecx寄存器傳遞
00F91559 mov dword ptr [ebp-0F4h],eax ;將返回值(this指針)放入[ebp-0F4h]中
00F9155F jmp wmain+8Bh (0F9156Bh) ;跳過下一句
00F91561 mov dword ptr [ebp-0F4h],0 ;將[ebp-0F4h]置空 當前面判斷p為空時執行此語句
00F9156B mov ecx,dword ptr [ebp-0F4h] ;[ebp-0F4h]為最終構造完成後的this指針(或者為空) 放入ecx
00F91571 mov dword ptr [ebp-0ECh],ecx ;又將this放入[ebp-0ECh] 這些都是調試所用
00F91577 mov dword ptr [ebp-4],0FFFFFFFFh
a->~A(); //析構
00F9157E push 0
00F91580 mov ecx,dword ptr [ebp-14h] ;從[ebp-14h]中取出p
00F91583 call A::`scalar deleting destructor' (0F91041h) ;調用析構函數(跟蹤進去比較復雜 如果在Release下,構造析構函數都是直接展開的)
::operator delete(a); //釋放
00F91588 mov eax,dword ptr [ebp-14h] ;將p放入eax
00F9158B push eax ;壓入p
00F9158C call operator delete (0F910B9h);調用operator delete(void* )
00F91591 add esp,4 </SPAN>
A* a = (A*)::operator new(sizeof(A)); //分配
00F9151D push 1
00F9151F call operator new (0F91208h) ;調用::operator new(size_t size)也就是throwing(1)版本
00F91524 add esp,4
00F91527 mov dword ptr [ebp-14h],eax ;返回地址放入[ebp-14h] 即為p
new(a) A(); //構造
00F9152A mov eax,dword ptr [ebp-14h]
00F9152D push eax
00F9152E push 1 ;壓入p
00F91530 call operator new (0F91280h);調用operator new(size_t, void* p)即placement(3)版本 只是簡單返回p
00F91535 add esp,8
00F91538 mov dword ptr [ebp-0E0h],eax ;將p放入[ebp-0E0h]
00F9153E mov dword ptr [ebp-4],0
00F91545 cmp dword ptr [ebp-0E0h],0 ;判斷p是否為空
00F9154C je wmain+81h (0F91561h) ;如果為空 跳過構造函數
00F9154E mov ecx,dword ptr [ebp-0E0h] ;取出p到ecx
00F91554 call A::A (0F91285h) ;調用構造函數 根據_thiscall調用約定 this指針通過ecx寄存器傳遞
00F91559 mov dword ptr [ebp-0F4h],eax ;將返回值(this指針)放入[ebp-0F4h]中
00F9155F jmp wmain+8Bh (0F9156Bh) ;跳過下一句
00F91561 mov dword ptr [ebp-0F4h],0 ;將[ebp-0F4h]置空 當前面判斷p為空時執行此語句
00F9156B mov ecx,dword ptr [ebp-0F4h] ;[ebp-0F4h]為最終構造完成後的this指針(或者為空) 放入ecx
00F91571 mov dword ptr [ebp-0ECh],ecx ;又將this放入[ebp-0ECh] 這些都是調試所用
00F91577 mov dword ptr [ebp-4],0FFFFFFFFh
a->~A(); //析構
00F9157E push 0
00F91580 mov ecx,dword ptr [ebp-14h] ;從[ebp-14h]中取出p
00F91583 call A::`scalar deleting destructor' (0F91041h) ;調用析構函數(跟蹤進去比較復雜 如果在Release下,構造析構函數都是直接展開的)
::operator delete(a); //釋放
00F91588 mov eax,dword ptr [ebp-14h] ;將p放入eax
00F9158B push eax ;壓入p
00F9158C call operator delete (0F910B9h);調用operator delete(void* )
00F91591 add esp,4 從反匯編中可以看出,其實operator new調用了兩次,只不過每一次調用不同的重載函數,並且placement new的主要作用只是將p放入ecx,並且調用其構造函數。
事實上,在指定地址上構造對象還有另一種方法,即手動調用構造函數:p->A::A(); 這裡要加上A::作用域,否則編譯器會報錯:
error C2273: “函數樣式轉換”: 位於“->”運算符右邊時非法
用p->A::A();替換掉new(p) A();仍然能達到同樣的效果,反匯編:
[plain]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px"> A* a = (A*)::operator new(sizeof(A)); //分配
010614FE push 1
01061500 call operator new (1061208h)
01061505 add esp,4
01061508 mov dword ptr [a],eax
//new(a) A(); //構造
a->A::A();
0106150B mov ecx,dword ptr [a]
0106150E call operator new (1061285h)
a->~A(); //析構
01061513 push 0
01061515 mov ecx,dword ptr [a]
01061518 call A::`scalar deleting destructor' (1061041h)
::operator delete(a); //釋放
0106151D mov eax,dword ptr [a]
01061520 push eax
01061521 call operator delete (10610B9h)
01061526 add esp,4 </SPAN>
A* a = (A*)::operator new(sizeof(A)); //分配
010614FE push 1
01061500 call operator new (1061208h)
01061505 add esp,4
01061508 mov dword ptr [a],eax
//new(a) A(); //構造
a->A::A();
0106150B mov ecx,dword ptr [a]
0106150E call operator new (1061285h)
a->~A(); //析構
01061513 push 0
01061515 mov ecx,dword ptr [a]
01061518 call A::`scalar deleting destructor' (1061041h)
::operator delete(a); //釋放
0106151D mov eax,dword ptr [a]
01061520 push eax
01061521 call operator delete (10610B9h)
01061526 add esp,4 比之前的方法更加簡潔高效(不需要調用placement new)。不知道手動調用構造函數是否有違C++標准或有什麼隱晦,我在其他很多有名的內存池(包括SGI STL alloc)實現上看到都是用的placement new,而不是手動調用構造函數。
三 operator new重載:
前面簡單提到過 A* p = new A;所發生的事情:先調用operator new,如果類A重載了operator new,那麼就使用該重載版本,否則使用全局版本::operatro new(size_t size)。那麼類中可以重載operator new的哪些版本?全局operator new可以重載嗎?全局和類中重載分別會在什麼時機調用?
1.在類中重載operator new
上面提到的throwing(1)和nothrow(2)的operator new是可以被重載的,比如:
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">#include <iostream>
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
void* operator new(size_t size)
{
std::cout<<"call A::operator new"<<std::endl;
return malloc(size);
}
void* operator new(size_t size, const std::nothrow_t& nothrow_value)
{
std::cout<<"call A::operator new nothrow"<<std::endl;
return malloc(size);
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* p1 = new A;
delete p1;
A* p2 = new(std::nothrow) A;
delete p2;
system("pause");
return 0;
}</SPAN>
#include <iostream>
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
void* operator new(size_t size)
{
std::cout<<"call A::operator new"<<std::endl;
return malloc(size);
}
void* operator new(size_t size, const std::nothrow_t& nothrow_value)
{
std::cout<<"call A::operator new nothrow"<<std::endl;
return malloc(size);
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* p1 = new A;
delete p1;
A* p2 = new(std::nothrow) A;
delete p2;
system("pause");
return 0;
}
如果類A中沒有對operator new的重載,那麼new A和new(std::nothrow) A;都將會使用全局operator new(size_t size)。可將A中兩個operator new注釋掉,並且在A外添加一個全局operator new重載:
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">void* ::operator new(size_t size)
{
std::cout<<"call global operator new"<<std::endl;
return malloc(size);
}</SPAN>
void* ::operator new(size_t size)
{
std::cout<<"call global operator new"<<std::endl;
return malloc(size);
}程序輸出:
注意,這裡的重載遵循作用域覆蓋原則,即在裡向外尋找operator new的重載時,只要找到operator new()函數就不再向外查找,如果參數符合則通過,如果參數不符合則報錯,而不管全局是否還有相匹配的函數原型。比如如果這裡只將A中operator new(size_t, const std::nothrow_t&)刪除掉,就會報錯:
error C2660: “A::operator new”: 函數不接受 2 個參數。
至於placement new,它本身就是operator new的一個重載,不需也盡量不要對它進行改寫,因為它一般是搭配 new(p) A(); 工作的,它的職責只需簡單返回指針。
對operator new的重載還可以添加自定義參數,如在類A中添加
[cpp]
<SPAN style="FONT-FAMILY: Microsoft YaHei; FONT-SIZE: 14px">void* operator new(size_t size, int x, int y, int z)
{
std::cout<<"X="<<x<<" Y="<<y<<" Z="<<z<<std::endl;
return malloc(size);
}</SPAN>
void* operator new(size_t size, int x, int y, int z)
{
std::cout<<"X="<<x<<" Y="<<y<<" Z="<<z<<std::endl;
return malloc(size);
}這種重載看起來沒有什麼大作用,因為它operator new需要完成的任務只是分配內存,但是通過對這類重載的巧妙應用,可以讓它在動態分配內存調試和檢測中大展身手。這將在後面operator new重載運用技巧中,展現。
2.重載全局operator new
全局operator new的重載和在類中重載並無太大區別,當new A;時,如果類A中沒有重載operator new,那麼將調用全局operator new函數,如果沒有重載全局operator new,最後會調用默認的全局operator new。
3.類中operator new和全局operator new的調用時機
前面已經提到了在new時的調用順序,但是這裡提出來的原因是還存在一個全局的new運算符,也就是::new,這個運算符會直接調用全局operator new,並且也會調用構造函數。這可能讓人很犯迷糊,只做了解即可。這裡提到的調用時機都是指通過new運算符調用,沒有討論其他情況,比如主動調用。
四 operator new運用技巧和一些實例探索
1.operator new重載運用於調試:
前面提到如何operator new的重載是可以有自定義參數的,那麼我們如何利用自定義參數獲取更多的信息呢,這裡一個很有用的做法就是給operator new添加兩個參數:char* file, int line,這兩個參數記錄new運算符的位置,然後再在new時將文件名和行號傳入,這樣我們就能在分配內存失敗時給出提示:輸出文件名和行號。
那麼如何獲取當前語句所在文件名和行號呢,windows提供兩個宏:__FILE__和__LINE__。利用它們可以直接獲取到文件名和行號,也就是 new(__FILE__, __LINE__) 由於這些都是不變的,因此可以再定義一個宏:#define new new(__FILE__, __LINE__)。這樣我們就只需要定義這個宏,然後重載operator new即可。
源代碼如下,這裡只是簡單輸出new的文件名和行號。
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">//A.h
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
void* operator new(size_t size, const char* file, int line)
{
std::cout<<"call A::operator new on file:"<<file<<" line:"<<line<<std::endl;
return malloc(size);
return NULL;
}
};
//Test.cpp
#include <iostream>
#include "A.h"
#define new new(__FILE__, __LINE__)
int _tmain(int argc, _TCHAR* argv[])
{
A* p1 = new A;
delete p1;
A* p2 = new A;
delete p2;
system("pause");
return 0;
}</SPAN>
//A.h
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
void* operator new(size_t size, const char* file, int line)
{
std::cout<<"call A::operator new on file:"<<file<<" line:"<<line<<std::endl;
return malloc(size);
return NULL;
}
};
//Test.cpp
#include <iostream>
#include "A.h"
#define new new(__FILE__, __LINE__)
int _tmain(int argc, _TCHAR* argv[])
{
A* p1 = new A;
delete p1;
A* p2 = new A;
delete p2;
system("pause");
return 0;
}輸出:
注意:需要將類的聲明實現與new的使用隔離開來。並且將類頭文件放在宏定義之前。否則在類A中的operator new重載中的new會被宏替換,整個函數就變成了: void* operator new(__FILE__, __LINE__)(size_t size, char* file, int line)
編譯器自然會報錯。
2.內存池優化
operator new的另一個大用處就是內存池優化,內存池的一個常見策略就是分配一次性分配一塊大的內存作為內存池(buffer或pool),然後重復利用該內存塊,每次分配都從內存池中取出,釋放則將內存塊放回內存池。在我們客戶端調用的是new運算符,我們可以改寫operator new函數,讓它從內存池中取出(當內存池不夠時,再從系統堆中一次性分配一塊大的),至於構造和析構則在取出的內存上進行,然後再重載operator delete,它將內存塊放回內存池。關於內存池和operator new在參考文獻中有一篇很好的文章。這裡就不累述了。
3.STL中的new
在SGI STL源碼中,defalloc.h和stl_construct.h中提供了最簡單的空間配置器(allocator)封裝,見《STL源碼剖析》P48。它將對象的空間分配和構造分離開來,雖然在defalloc.h中僅僅是對::operator new和::operator delete的一層封裝,但是它仍然給STL容器提供了更加靈活的接口。SGI STL真正使用的並不是defalloc.h中的分配器,而是stl_alloc.h中的SGI精心打造的"雙層級配置器",它將內存池技術演繹得淋漓盡致,值得細細琢磨。順便提一下,在stl_alloc.h中並沒有使用::operator new/delete 而直接使用malloc和free。具體緣由均可參見《STL源碼剖析》。
五 delete的使用
delete的使用基本和new一致,包括operator delete的重載方式這些都相似,只不過它的參數是void*,返回值為void。但是有一點需要注意,operator delete的自定義參數重載並不能手動調用。比如
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">void* operator new(size_t size, int x)
{
cout<<" x = "<<x<<endl;
return malloc(size);
}
void operator delete(void* p, int x)
{
cout<<" x = "<<x<<endl;
free(p);
}</SPAN>
void* operator new(size_t size, int x)
{
cout<<" x = "<<x<<endl;
return malloc(size);
}
void operator delete(void* p, int x)
{
cout<<" x = "<<x<<endl;
free(p);
}
如下調用是無法通過的:
A* p = new(3) A;//Ok
delete(3) p;//error C2541: “delete”: 不能刪除不是指針的對象
那麼重載operator delete有什麼作用?如何調用?事實上以上自定義參數operator delete 只在一種情況下被調用:當new運算符拋出異常時。
可以這樣理解,只有在new運算符中,編譯器才知道你調用的operator new形式,然後它會調用對應的operator delete。一旦出了new運算符,編譯器對於你自定義的new將一無所知,因此它只會按照你指定的delete運算符形式來調用operator delete,而至於為什麼不能指定調用自定義delete(也就是只能老老實實delete p),這個就不知道了。
細心觀察的話,上面operator new用於調試的例子代碼中,由於我們沒有給出operator new對應的operator delete。在VS2008下會有如下警告:
warning C4291: “void *A::operator new(size_t,const char *,int)”: 未找到匹配的刪除運算符;如果初始化引發異常,則不會釋放內存
六 關於new和內存分配的其他
1.set_new_handler
還有一些零散的東西沒有介紹到,比如set_new_handler可以在malloc(需要調用set_new_mode(1))或operator new內存分配失敗時指定一個入口函數new_handler,這個函數完成自定義處理(繼續嘗試分配,拋出異常,或終止程序),如果new_handler返回,那麼系統將繼續嘗試分配內存,如果失敗,將繼續重復調用它,直到內存分配完畢或new_handler不再返回(拋出異常,終止)。下面這段程序完成這個測試:
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">#include <iostream>
#include <new.h>// 使用_set_new_mode和set_new_handler
void nomem_handler()
{
std::cout<<"call nomem_handler"<<std::endl;
}
int main()
{
_set_new_mode(1); //使new_handler有效
set_new_handler(nomem_handler);//指定入口函數 函數原型void f();
std::cout<<"try to alloc 2GB memory...."<<std::endl;
char* a = (char*)malloc(2*1024*1024*1024);
if(a)
std::cout<<"ok...I got it"<<std::endl;
free(a);
system("pause");
}</SPAN>
#include <iostream>
#include <new.h>// 使用_set_new_mode和set_new_handler
void nomem_handler()
{
std::cout<<"call nomem_handler"<<std::endl;
}
int main()
{
_set_new_mode(1); //使new_handler有效
set_new_handler(nomem_handler);//指定入口函數 函數原型void f();
std::cout<<"try to alloc 2GB memory...."<<std::endl;
char* a = (char*)malloc(2*1024*1024*1024);
if(a)
std::cout<<"ok...I got it"<<std::endl;
free(a);
system("pause");
}
程序運行後會一直輸出call nomem_handler 因為函數裡面只是簡單輸出,返回,系統嘗試分配失敗後,調用nomem_handler函數,由於該函數並沒有起到實際作用(讓可分配內存增大),因此返回後系統再次嘗試分配失敗,再調用nomem_handler,循環下去。
在SGI STL中的也有個仿new_handler函數:oom_malloc
2.new分配數組
A* p = new A[3];中,會直接調用全局的operator new[](size_t size),而不管A中是否有operator new[]的重載。而delete[]p卻會優先調用A::operator delete[](void*)(如果A中有重載)。另外還要注意的是,在operator new[](size_t size)中傳入的並不是sizeof(A)*3。而要在對象數組的大小上加上一個額外數據,用於編譯器區分對象數組指針和對象指針以及對象數組大小。在VS2008下這個額外數據占4個字節,一個int大小。測試代碼如下
[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">//A.h
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
void* operator new(size_t size)
{
std::cout<<"call A::operator new[] size:"<<size<<std::endl;
return malloc(size);
}
void operator delete[](void* p)
{
std::cout<<"call A::operator delete[]"<<std::endl;
free(p);
}
void operator delete(void* p)
{
free(p);
}
};</SPAN>
//A.h
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
void* operator new(size_t size)
{
std::cout<<"call A::operator new[] size:"<<size<<std::endl;
return malloc(size);
}
void operator delete[](void* p)
{
std::cout<<"call A::operator delete[]"<<std::endl;
free(p);
}
void operator delete(void* p)
{
free(p);
}
};[cpp]
<SPAN style="FONT-FAMILY: SimHei; FONT-SIZE: 14px">//Test.cpp
#include <iostream>
#include "A.h"
void* operator new[](size_t size)
{
std::cout<<"call global new[] size: "<<size<<std::endl;
return malloc(size);
}
void operator delete[](void* p)
{
std::cout<<"call global delete[] "<<std::endl;
}
int _tmain(int argc, _TCHAR* argv[])
{
std::cout<<"sizeof A "<<sizeof(A)<<std::endl;
A* p1 = new A[3];
delete []p1;
system("pause");
return 0;
}</SPAN>