http://www.cublog.cn/u2/79570/showart_2084600.html
1 類、對象和內(nèi)存
1.1 通過內(nèi)存看對象
我 們先回顧一下類和對象的定義,類是定義同一類所有實例變量和方法的藍圖或原型;對象是類的實例化。從內(nèi)存的角度可以對這兩個定義這樣理解,類刻畫了實例的 內(nèi)存布局,確定實例中每個數(shù)據(jù)成員在一塊連續(xù)內(nèi)存中的位置、大小以及對內(nèi)存的解讀方式;對象就是系統(tǒng)根據(jù)類刻畫的內(nèi)存布局去分配的內(nèi)存。除了實例變量和方 法,類也可以定義類變量和類方法,這是我們通常所說的靜態(tài)變量和靜態(tài)函數(shù),它們不屬于某個具體的對象,而是屬于整個類,所以不會影響對象的內(nèi)存布局和內(nèi)存 大小。通過以上的討論我們可以知道:對象本質(zhì)上就是一塊連續(xù)的內(nèi)存,對象的類型(類)就是對這塊內(nèi)存的解讀方式。在c++中我們可以通過四個類型轉(zhuǎn)換運算符改變對象的類型,這種轉(zhuǎn)換改變的是內(nèi)存的解讀方式,不會修改內(nèi)存中的值。修改對象內(nèi)存值的合法途徑是通過成員函數(shù)/友元函數(shù)修改對象的數(shù)據(jù)成員。通過成員函數(shù)修改對象的值是c++語 言保證對象安全的一種機制,但這種機制不是強制的,你可以通過暴力的非法手段避開這個機制(比如你可以取得對象的起始地址,然后根據(jù)對象的內(nèi)存布局任意修 改內(nèi)存的值),除了極其特殊情況,這種人為非法手段都應(yīng)當被禁止,因為這種暴力代碼難于理解、不便移植、極易出錯;另外程序在運行過程中由于代碼中的某些 缺陷也會非法修改對象內(nèi)存的值,這是我們程序中許多疑難bug的根源。所以正確的編寫類,理解對象在內(nèi)存的運行特點,合理的控制對象的創(chuàng)建和銷毀是一個程序穩(wěn)定運行的基本保證。
1.2 不同內(nèi)存區(qū)域的對象
在C++中,對象通常存放在三個內(nèi)存區(qū)域:棧、堆、全局/靜態(tài)數(shù)據(jù)區(qū);相對應(yīng)的,在這三個區(qū)域中的對象就被稱為棧對象、堆對象、全局/靜態(tài)對象。
全局/靜 態(tài)數(shù)據(jù)區(qū):全局對象和靜態(tài)對象存放在該區(qū),在該內(nèi)存區(qū)的對象一旦創(chuàng)建后直到進程結(jié)束才會釋放。在其生存期內(nèi)可以被多個線程訪問,它可以做為多線程通信的一 種方式,所以對于全局對象和靜態(tài)對象要考慮線程安全,特別是對于函數(shù)中的局部靜態(tài)變量,容易忘記它的線程安全性。全局對象和一些靜態(tài)對象有一個特點:這些 對象的初始化操作先于main函數(shù)的執(zhí)行,而且這些對象(可能分布在不同的源文件中)初始化順序沒有規(guī)定,所以在它們的初始化中不要啟動線程,同時它們的初始化操作也不應(yīng)有依賴關(guān)系。
堆:堆對象是通過new/malloc在堆中動態(tài)分配內(nèi)存,通過delete/free釋放內(nèi)存的對象。我們可對這種對象的創(chuàng)建和銷毀進行精確控制。堆對象在c++中的使用非常廣泛,不同線程間、函數(shù)間的對象共享可以使用堆對象,大型對象一般也使用堆對象(??臻g是有限的),特別是虛函數(shù)多態(tài)性一般是由堆對象實現(xiàn)的。使用堆對象也有一些缺點:1.需要程序員管理生存周期,忘記釋放會有內(nèi)存泄露,多次釋放可能造成程序崩潰,通過智能指針可以避免這個問題;2.堆對象的時間效率和空間效率沒有棧對象高,堆對象一般通過某種搜索算法在堆中找到合適大小的內(nèi)存,比較耗時間,另外從堆中分配的內(nèi)存大小會比實際申請的內(nèi)存大幾個字節(jié),比較耗空間,尤其是對于小型對象這種損耗是非常大的;3.頻繁使用new/delete 堆對象會造成大量的內(nèi)存碎片,內(nèi)存得不到充分的使用。對于2,3兩個問題可以通過內(nèi)存一次分配,多次使用的方法解決,更好的方法是根據(jù)業(yè)務(wù)特點實現(xiàn)特定的內(nèi)存池。
棧:棧 對象是自生自滅型對象,程序員無需對其生存周期進行管理。一般,臨時對象、函數(shù)內(nèi)的局部對象都是棧對象。使用棧對象是高效的,因為它不需要進行內(nèi)存搜索只 進行棧頂指針的移動。另外棧對象是線程安全的,因為不同的線程有自己的棧內(nèi)存。當然,棧的空間是有限的,所以使用中要防止棧溢出的出現(xiàn),通常大型對象、大 型數(shù)組、遞歸函數(shù)都要慎用棧對象。
2 C++對象的創(chuàng)建和銷毀
C++類有四個基本函數(shù):構(gòu)造函數(shù)、析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)、賦值運算符重載函數(shù),這四個函數(shù)管理著C++對象的創(chuàng)建和銷毀,正確而完整地實現(xiàn)這些函數(shù)是C++對象安全運行必要保證。
2.1 構(gòu)造/析構(gòu)
創(chuàng)建一個對象有兩個步驟:1.在內(nèi)存中分配sizeof(CAnyType) 字節(jié)數(shù)的內(nèi)存;2.調(diào)用合適構(gòu)造函數(shù)初始化分配的內(nèi)存。C++對象的大小由三個因素決定:1.各個數(shù)據(jù)成員的大?。?.由字節(jié)對齊產(chǎn)生的填充空間的大?。?.為支持虛機制編譯器添加的一個指針,大小是四個字節(jié),虛機制指針有兩種:1.支持虛函數(shù)的虛表指針,2.支持虛繼承的虛基類指針。虛繼承時,派生類只保存一份被繼承的基類的實體,比如下面例子的菱形繼承關(guān)系中,類D中只有一份類A的實體。另外,類A是一個空類,但sizeof(A)大小不是0,而是1,這是因為需要用這一個字節(jié)來唯一標識類A在內(nèi)存中的不同對象。
Class A{};
Class B : virtual public A {};
Class C : virtual public A {};
Class D:public B, public C {}
由 上面討論知道,類中除了有編程人員寫的數(shù)據(jù)成員,有時還有一些由編譯器為支持虛機制而偷偷給你添加的成員,這些成員我們在代碼中不會直接用到,但有可能被 我們的代碼非法修改。比如不恰當?shù)臉?gòu)造函數(shù)會修改虛機制指針的值,在寫構(gòu)造函數(shù)時我們經(jīng)常使用如下的代碼對整個對象進行初始化:
memset(this, 0, sizeof(*this));
這種初始化方式只能在類不涉及虛機制的情況下使用,否則它會修改虛機制指針,使類對象的行為無定義。
銷毀一個對象也有兩個步驟:1.調(diào)用析構(gòu)函數(shù);2.向系統(tǒng)歸還內(nèi)存。析構(gòu)函數(shù)的作用是釋放對象申請的資源。析構(gòu)函數(shù)通常是由系統(tǒng)自動調(diào)用的,在以下幾種情況下系統(tǒng)會調(diào)用析構(gòu)函數(shù):1.棧對象生命周期結(jié)束時:包括離開作用域、函數(shù)正常return(不考慮NRV優(yōu)化)、函數(shù)異常throw;2.堆對象進行delete操作時;3.全局對象在進程結(jié)束時。析構(gòu)函數(shù)只在一種情況下需要被顯式的調(diào)用,那就是用placement new構(gòu)建的對象。當類里包含虛函數(shù)的時候我們應(yīng)該聲明虛析構(gòu)函數(shù),虛析構(gòu)函數(shù)的作用是:當你delete一個指向派生類對象的基類指針時保證派生類的析構(gòu)函數(shù)被正確調(diào)用。有許多資源泄露的問題就是因為沒有正確使用虛析構(gòu)函數(shù)造成的,這種資源泄露有兩種:1.派生類里直接分配的資源;2.派生類里的成員對象分配的資源。尤其是第二類,隱蔽性非常高。
構(gòu) 造和析構(gòu)是一組被成對調(diào)用的函數(shù),特別是對于棧對象,調(diào)用是由系統(tǒng)自動完成的,所以我們可以利用這一特性將一些需要成對出現(xiàn)的操作分別封裝在構(gòu)造和析構(gòu)函 數(shù)里由系統(tǒng)自動完成,這樣可以避免由于編程時的遺漏而忘記進行某種操作。比如資源的申請和釋放,多線程中的加鎖和解鎖都可以利用棧對象的這一特性進行自動 管理。
2.2 拷貝/賦值
拷貝構(gòu)造函數(shù)、賦值運算符重載函數(shù)是一對孿生兄弟,通常一個類如果需要顯式寫拷貝構(gòu)造函數(shù),那么它也需要顯式寫賦值運算符重載函數(shù)??截悩?gòu)造函數(shù)的功能是用已存在的對象構(gòu)造一個新的對象,賦值運算符重載函數(shù)的功能是用已存在的對象替換一個已存在的對象。看下面幾條語句:
string str1 = “string test”; //調(diào)用帶參數(shù)的構(gòu)造函數(shù)
string str2(str1); //調(diào)用拷貝構(gòu)造函數(shù)
string str3 = str1; //調(diào)用拷貝構(gòu)造函數(shù)
string str4; //調(diào)用默認構(gòu)造函數(shù)
str4 = str3; //調(diào)用賦值運算符重載函數(shù)
拷貝構(gòu)造函數(shù)、賦值運算符重載函數(shù)原型如下:
class string
{
private:
char* m_pStr;
int m_nSize;
public:
string (); //默認構(gòu)造函數(shù)
string (const char* pStr); //帶參數(shù)的構(gòu)造函數(shù)
~ string ();
string (const string & cOther); //拷貝構(gòu)造函數(shù)
string & operator=(const string & cOther); //賦值運算符重載函數(shù)
}
這兩個函數(shù)的參數(shù)類型都是const string &,我們知道對于c++對 象通常以常引用作函數(shù)的參數(shù),這樣可以提高參數(shù)的傳遞效率,以對象作為函數(shù)參數(shù)時會調(diào)用拷貝構(gòu)造函數(shù)生成一個臨時對象供函數(shù)使用,效率較低??截悩?gòu)造函數(shù) 是一個很特殊的函數(shù),對于其他函數(shù)用對象作為函數(shù)參數(shù)頂多是效率的損失,但對拷貝構(gòu)造函數(shù)用對象作為函數(shù)參數(shù)就會形成無限遞歸調(diào)用,所以拷貝構(gòu)造必須以常 引用作為參數(shù)。
拷貝構(gòu)造函數(shù)在c++編譯器中有缺省的實現(xiàn),實現(xiàn)的方式是按位對內(nèi)存進行拷貝(memcpy(this, & cOther, sizeof(string)),如果缺省的實現(xiàn)滿足我們的要求那就不需要顯式的去實現(xiàn)這個函數(shù),否則就必須實現(xiàn),判斷是否滿足位拷貝語義的依據(jù)是類的成員數(shù)據(jù)中是否需要動態(tài)分配其他資源,比如上面的string類,成員m_pStr需要從堆中分配內(nèi)存來存放具體的字符串,這塊堆內(nèi)存是位拷貝語義無法正確管理的,所以在string對象進行拷貝/賦值時編程人員需要負責管理這塊內(nèi)存。通常在三種情況下會調(diào)用拷貝構(gòu)造函數(shù),1. 一個對象以值傳遞的方式傳入函數(shù);2. 一個對象以值傳遞的方式從函數(shù)返回;3. 一個對象需要通過另外一個對象進行初始化。如果你確保對類對象的使用不會出現(xiàn)以上三種情況,那就說明你根本不需要拷貝構(gòu)造函數(shù),直接將拷貝構(gòu)造函數(shù)私有化是最安全的選擇。從以上的討論我們知道,對于拷貝構(gòu)造函數(shù)有三種處理策略(對于賦值運算符重載函數(shù)同樣適用):1.什么都不寫,按缺省的處理;2.顯式寫拷貝構(gòu)造;3.將拷貝構(gòu)造私有化。在寫一個類前,我們必須分析類自身的實現(xiàn)方式以及對類對象的使用方式,明確選擇一種策略,如果你放棄選擇你就為將來可能出現(xiàn)的bug埋下一個伏筆。
上面討論了拷貝/賦值函數(shù)的選擇策略,下面看看它們具體的實現(xiàn)方式。拷貝構(gòu)造函數(shù)的功能由一個對象構(gòu)造一個新的對象,只要一個Copy操作就可以完成。賦值運算符重載函數(shù)的功能是由一個對象替換一個已存在的對象,完成這個功能需要三個操作:自賦值檢查、Clear原有對象、Copy新對象。如string類的實現(xiàn):
class string
{
private:
char* m_pStr;
int m_nSize;
public:
string (); //默認構(gòu)造
string (const char* pStr); //帶參數(shù)的構(gòu)造函數(shù)
~ string ();
string (const string & cOther) //實現(xiàn)拷貝構(gòu)造函數(shù)
{
Copy(cOther);
}
string & operator=(const string & cOther) //實現(xiàn)賦值運算符重載函數(shù)
{
if (this != & cOther)
{
Clear();
Copy(cOther);
}
return *this;
}
private:
void Copy(const string & cOther)
{
m_pStr = new char [cOthre. m_nSize+1];
strcpy(m_pStr, cOther. m_pStr);
m_nSize = cOther.m_nSize;
}
void Clear()
{
if (m_pStr != NULL)
{
delete[] m_pStr;
m_pStr = NULL;
}
m_nSize = 0;
}
}
string類中這兩個函數(shù)的實現(xiàn)模式可以在其他類中直接套用,只需要改動Copy和Clear()函數(shù)即可。
3 總結(jié)
作為c++程序員每天都要和類、對象以及內(nèi)存打交道,寫一個類實現(xiàn)某項功能不難,但要實現(xiàn)一個健壯的、可重用的、易擴展的類就不是很容易了。很多時候我們寫一個類時用的還是c的思維,對類的四個基本函數(shù)考慮的不夠周到仔細,對類對象在不同內(nèi)存區(qū)域運行特點理解不夠,容易產(chǎn)生一些低級的bug,而且對后續(xù)的代碼維護擴展也帶來難度。本文中對這些內(nèi)容做了基本的介紹,希望對大家有些幫助。
浙公網(wǎng)安備 33010602011771號