正確理解C++的構造函數和析構函數
一、構造函數
首先,由於類隻是一個模板,因此我們在定義類時無法對成員變量初始化,比如下面代碼就是錯誤的:
class circle{ public: int m_L = 20; // Error:不允許使用數據成員初始值設定項 };
因此,初始化隻能發生在類創建對象的過程中,但是由於訪問權限的原因,無法在類外訪問某些成員變量,因此下面這種做法有時候是無效的:
circle C1; // 實例化一個對象 C1 C1.m_L = 20; // 通過創建的對象,來給對應變量初始化,但是如果m_L是private訪問權限,則失效
為瞭解決這個問題,讓程序員能像使用標準數據類型一樣適用對象,在類內提供瞭一個特殊的成員函數——“構造函數”,專門用於在創建對象時初始化類對象。之所以說它特殊,是因為C++已經自動為構造函數提供瞭名稱和使用語法,程序員隻需要提供方法的定義即可,即:類名(形參列表)
。具體來說,構造函數的定義如下:
class circle{ public: int m_L; public: //成員函數(方法) circle(const int a) //通過構造函數對成員變量進行賦值 { m_L = a; } }; circle C1(20); //調用格式 cout << "C1.m_L:" << C1.m_L <<endl;
看上去似乎很簡單,但是由於構造函數也是函數,因此所有C++中的形參傳遞方式,函數特性以及函數調用方法都能用於構造函數。 如前文所講,C++會自動給類添加一個空的構造函數,但是如果自己在類中實現瞭有參構造函數,編譯器便不再提供無參構造函數。舉例如下:
class circle{ public: int m_L; public: //成員函數(方法) circle(const int a) //通過構造函數對成員變量進行賦值 { m_L = a; } }; circle C1(20); //調用格式正確,能夠通過構造函數賦值 circle C2; //錯誤,自己定義瞭有參構造函數,不再提供無參構造函數
構造函數可以重載,接著上面的例子,如果重載一個空的構造函數,那麼兩個調用格式都正確:
class circle{ public: int m_L; public: //成員函數(方法) circle(){} // 空構造函數 circle(const int a) //通過構造函數對成員變量進行賦值 { m_L = a; } }; circle C1(20); //調用格式正確,能夠通過構造函數賦值 circle C2; //正確,可以通過重載的空構造函數實現初始化
構造函數的參數不僅可以是標準數據類型,也可以是類。眾所周知,在數值作為函數參數進行傳遞的時候,會重新拷貝出來一份數據作為參數傳遞用完即銷毀,這種方式不僅浪費瞭內存空間,而且無法修改原始數據。為瞭結合這兩者之間的優點,於是經常采取引用作為函數的參數。雖然引用是指針的一種特殊情況,但是指針太過於靈活,並且引用在形式上引用與普通的變量地用法並沒有什麼區別,因此使用起來更加方便。
class circle{ public: int m_L; public: //成員函數(方法) circle(){} // 空構造函數 circle(const int a) //通過構造函數對成員變量進行賦值 { m_L = a; } //引用作為函數參數傳遞,並用const修飾,節省空間的同時避免修改原數據 circle(const circle& sub_circle) { m_L = sub_circle.m_L; } }; circle C1(20); //調用格式正確,能夠通過構造函數賦值 circle C2(C1); //正確,可以通過拷貝構造函數進行初始化
二、C++類的內存模型
C++中,一個類包括:
- 成員變量:靜態成員變量和普通成員變量
- 成員函數:靜態成員函數和普通成員函數
雖然為瞭集成,我們將其寫到一個類裡面,但是隻有普通成員變量真正屬於類的對象,類的所有對象共享一份靜態成員函數,靜態成員變量和普通成員函數。畫出瞭內存模型,如下圖所示:
為瞭進一步理解,我們舉例如下:
2.1、隻定義成員函數
class person{ public: // 定義一個空的構造函數 person(int m_age, int m_ID){ } }; person p1(10, 20); cout << "p1 所占的空間為:" << sizeof(p1) << endl;
輸出結果為:
p1 所占的空間為:1
這個題目在《劍指offer》一書中也提到過,由空類實例化出來的對象所占的內存空間是1個而不是0個字節,因為編譯其給對象 p1
分配瞭一個地址,來表示不同的對象存儲在不同的地址空間,因此占用1個字節。
2.2、往空類中添加靜態成員變量
class person{ static int age; //靜態成員變量,存在全局區,不屬於類對象的一部分 static int ID; //靜態成員變量,存在全局區,不屬於類對象的一部分 public: // 定義一個空的構造函數 person(int m_age, int m_ID){ } }; person p1(10, 20); cout << "p1 所占的空間為:" << sizeof(p1) << endl;
輸出結果為:
p1 所占的空間為:1
當向類中加入瞭成員函數與靜態成員變量時,類的實例化對象仍然隻占用1個字節的空間,足以證明這些函數和變量並不是類對象的一部分。
2.3、再加入非靜態成員變量
class person { static int age; //靜態成員變量,存在全局區,不屬於類對象的一部分 static int ID; //靜態成員變量,存在全局區,不屬於類對象的一部分 int a; //非靜態成員變量,存在棧區,屬於類對象的一部分 public: person(int m_age, int m_ID){ } };
輸出結果為:
p 所占的空間為:4
因此當向類中加入瞭非靜態成員變量時,類的實例化對象占用4個字節的空間,可以說明,非靜態變量屬於類對象的一部分。綜上:同一個類所有實例化出來的對象共享同一份靜態成員變量,所以一改全改。既然同一個類的不同對象共享同一份成員函數,那麼成員函數怎麼區分該訪問哪個對象的普通成員變量呢?
三、this指針
接著上一小節的問題,this指針為上述問題提供瞭一個完美的解決方案,它指向用來調用成員函數的對象(被當作參數隱式地傳遞給成員函數),我們通過一張圖來理解它:
此外,this指針的另一個用途是當成員函數需要返回對象時,用 return *this
; 或者 return this
,這種做法能夠實現鏈式編程。比如:
p2.addPerson(p1).addPerson(p1);
首先,對象 p2
調用成員函數 addPerson(p1)
,其返回值繼續調用 addPerson(p1)
,此時返回值就必須也是 person
類型才可以,因此使用 this
指針可以完成需求。先來看第一個例子:
class person{ public: int age; person(int age) { this->age = age; // this指針區分調用者 } // 返回值為person類型,且參數加上瞭const限制,防止修改原數據 person addPerson(const person& p) { this->age += p.age; // 主要實現兩個類對象年齡的相加 return *this; // 由於返回值是person,因此返回 *this } }; person p1(20); person p2(10); person p3 = p2.addPerson(p1).addPerson(p1); cout << "p1 age:" << p1.age << endl; cout << "p2 age:" << p2.age << endl; cout << "p3 age:" << p3.age << endl;
首先,通過構造函數分別對 p1,p2
賦瞭初值,然後 p2
調用函數 addPerson(p1)
修改自身的變量 age
。**由於函數通過值傳遞的方式返回 person
類型,所以將整個 person
類型復制瞭一份返回,返回值繼續調用 addPerson(p1)
, **最後的結果賦值給瞭新的對象 p3
。所以輸出結果為:
p1 age:20
p2 age:30
p3 age:50
但是如果函數 addPerson()
修改為:
person& addPerson(const person& p) { this->age += p.age; // 主要實現兩個類對象年齡的相加 return *this; // 雖然返回值是person&,返回值的類型也是 *this } person p1(20); person p2(10); person p3 = p2.addPerson(p1).addPerson(p1); cout << "p1 age:" << p1.age << endl; cout << "p2 age:" << p2.age << endl; cout << "p3 age:" << p3.age << endl;
與上例唯一的區別就在於返回值的類型變成瞭引用,那麼每次返回的就變成瞭該對象本身,而非在值傳遞中拷貝出來的那一份數據。那麼輸出就變成瞭:
p1 age:20
p2 age:50
p3 age:50
四、析構函數
用構造函數創建對象後,程序負責跟蹤該對象,知道其過期為止。當對象過期時,程序自動調用析構函數完成清理工作。與構造函數一樣,C++默認提供瞭一個空的析構函數,定義為:~類名( )
。由於開辟在棧區的變量程序會自動釋放,因此不需要析構函數執行清理工作,但是當程序員在堆區開辟空間時,需要手動執行清理工作,這時候需要析構函數來釋放堆區內存。比如:
~person() { // 在析構函數內寫入需要執行的代碼 cout << "調用析構函數" << endl; } person p1(20); person p2(10); // 在生命周期結束後自動調用析構函數執行清理工作
輸出為:
調用析構函數
調用析構函數
以上就是正確理解C++的構造函數和析構函數的詳細內容,更多關於C++ 構造函數 析構函數的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- C++數據結構繼承的概念與菱形繼承及虛擬繼承和組合
- C++關於類結構體大小和構造順序,析構順序的測試詳解
- 超級詳細講解C++中的多態
- 聊聊c++數組名稱和sizeof的問題
- C++繼承與菱形繼承詳細介紹