C++踩坑實戰之構造和析構函數
前言
我是練習時長一年的 C++ 個人練習生,喜歡野指針、模板報錯和未定義行為(undefined behavior)。之前在寫設計模式的『工廠模式』時,一腳踩到瞭構造、繼承和 new 組合起來的坑,現在也有時間來整理一下瞭。
構造函數
眾所周知:在創建對象時,防止有些成員沒有被初始化導致不必要的錯誤,在創建對象的時候自動調用構造函數(無聲明類型),完成成員的初始化。即:
Class c // 隱式,默認構造函數 Class c = Class() // 顯示,默認構造函數 Class c = Class("name") // 顯示,非默認構造函數 Class* c = new Class // 隱式,默認構造函數
- 構造函數執行前,對象不存在
- 構造函數創建對象後,對象不能調用構造函數
- 類中如果不定義構造函數,編譯器提供有默認的構造函數,無參數,也不執行任何額外的語句
- 如果提供非默認構造函數,沒有默認構造函數將會出錯。所以要定義一個不接受任何參數的構造函數,並為成員定義合理的值
- 一般而言,默認的構造函數是用來對所有類成員做隱式初始化的
- 自己定義的構造函數一般用使用列表初始化來初始化參數
- 通過構造函數對成員賦值,要優於通過函數為成員賦值
using namespace std; class Stone { private: int weight{0}; double radius{0.0}; public: Stone() { cout << "Class Stone was created by default creator" << endl; }; Stone(int w, double r) : weight{w}, radius{r} { cout << "Class Stone was created by custom creator" << endl; } void showInfo() { cout << "Weight: " << this->weight << ", Radius: " << this->radius << endl; } }; int main (){ // 隱式,成員有默認值 Stone s1; s1.showInfo(); // 顯式,通過列表初始化,為成員賦值 Stone s2 = Stone(12, 3.3); s2.showInfo(); return 0; }
通過構造函數實現的類型轉換
觀察以下的代碼,我們發現 Stone s2;s2 = 3.3; 這樣將一個 double 類型的數據賦值給類類型並沒有出錯,這是隱式類型轉換,從參數類型到類類型。
using namespace std; class Stone { private: int weight{0}; double radius{0.0}; public: Stone() { cout << this << endl; cout << "Class Stone was created by default creator" << endl; }; // 都關閉 Stone(double r) : radius{r} { cout << this << endl; cout << "Class Stone was created by parameter radius" << endl; } Stone(int w) : weight{w} { cout << this << endl; cout << "Class Stone was created by parameter weight" << endl; } void showInfo() { cout << "Weight: " << this->weight << ", Radius: " << this->radius << endl; } }; int main (){ Stone s2; s2 = 3.3; s2.showInfo(); return 0; }
這是因為:接受一個參數的構造函數允許使用賦值語法來為對象賦值。s2=3.3 會創建 Stock(double) 臨時對象,臨時對象初始化後,逐成員賦值的方式復制到對象中,在幾個構造函數中加入瞭 cout << this 的語句,由對象的地址不同,可以判斷該賦值語句額外生成瞭臨時對象。
為瞭防止隱式轉換帶來的危險,可以使用關鍵字 explicit 關閉這一特性,這樣就得顯式完成參數類型到類類型的轉換:s = Stock(1.3);不過,得保證沒有二義性。
using namespace std; class Stone { private: int weight{0}; double radius{0.0}; public: Stone() { cout << this << endl; cout << "Class Stone was created by default creator" << endl; }; // 都關閉 explicit Stone(double r) : radius{r} { cout << this << endl; cout << "Class Stone was created by parameter radius" << endl; } explicit Stone(int w) : weight{w} { cout << this << endl; cout << "Class Stone was created by parameter weight" << endl; } void showInfo() { cout << "Weight: " << this->weight << ", Radius: " << this->radius << endl; } }; int main (){ Stone s2; s2 = Stone(3); s2.showInfo(); return 0; }
上述代碼中,如果 Stone(int w) 沒有被關閉,那麼 s2=3.3 將調用這一構造函數。所以構造函數建議都加上 explicit 聲明。
派生類的構造函數
派生類要註意的是:派生類被構造之前,通過調用一個基類的構造函數,創建基類完成基類數據成員的初始化;也就是說,基類對象在程序進入派生類構造函數之前被創建。那麼,可以通過初始化列表傳遞給基類參數,不傳遞的話,調用基類的默認的構造函數,如下述程序中的:Gem(){}:Stone()。
using namespace std; class Stone { private: int weight{0}; double radius{0.0}; public: Stone() { cout << "This object was in address: " << this << endl; }; Stone(int w, double r) : weight{2}, radius{r} {}; void showInfo() { cout << "Weight: " << this->weight << ", Radius: " << this->radius; } int getWeight(){ return this->weight; } auto getRadius() -> double { return this->radius; } }; class Gem : public Stone { private: double price; public: Gem(){}; Gem(double p, int w, double r) : Stone(w, r), price{p} {}; void show() { cout << "Weight: " << this->getWeight() << ", Radius" << this->getRadius(); } }; int main (){ Gem g1; // call default Gem g2 = Gem(1300, 1, 2.3); // call custom // g.setWeight(130); g2.show(); return 0; }
- 首先創建基類對象
- 派生類通過初始化列表(隻能用在構造函數)將基類信息傳遞給基類的構造函數
- 派生類構造函數可以為派生類初始化新的成員
析構函數
對象過期時,程序會調用對象的析構函數完成一些清理工作,如釋放變量開辟的空間等。如構造函數使用瞭 new 來申請空間,析構就需要 delete 來釋放空間。如果沒有特別聲明析構函數,編譯器會為類提供默認的析構函數,在對象作用域到期、被刪除時自動被調用。
如 stock1 = Stock(),這種就申請瞭一個臨時變量,變量消失時會調用析構函數。此外,這種局部變量放在棧區,先入後出,也就是,最後被申請的變量最先被釋放。
using namespace std; class Stone { private: int weight{0}; double radius{0.0}; public: Stone() { cout << "This object was in address: " << this << endl; }; ~Stone() { cout << this << " Object was deleted." << endl; } }; int main (){ { Stone s1; Stone s2; } return 0; }
繼承中的析構函數
繼承類比較容易理解,畢竟都學過面向對象。公有繼承的時候,基類的公有成員也是派生類的共有成員;私有成員也是派生類的一部分,不過需要共有或保護方法來訪問。但是但是但是,派生類和基類的析構函數之間,也是一個坑。在繼承中:
- 如果一個方法不是虛方法,那麼將根據引用類型或指針類型選擇執行的方法
- 如果一個方法是虛方法,將根據指針或引用指向對象的類型選擇執行的方法
在繼承中,對象的銷毀順序和創建相反。創建時先創建基類,而後創建子類;銷毀時,先調用子類的析構函數,而後自動調用基類的析構函數。因此,對於基類而言,建議將析構函數寫成虛方法。如果析構不是虛方法,對於以下情況,隻有基類的析構被調用;如果析構是虛方法,子類、基類的析構方法都被調用。可以嘗試刪除下述代碼的 virtual 來觀察結果:
using namespace std; class Stone { private: int weight{0}; double radius{0.0}; public: Stone() { cout << "This object was in address: " << this << endl; }; Stone(int w, double r) : weight{2}, radius{r} {}; void showInfo() { cout << "Weight: " << this->weight << ", Radius: " << this->radius; } int getWeight(){ return this->weight; } auto getRadius() -> double { return this->radius; } virtual ~Stone() { cout << "Stone class was deleted." << endl; } }; class Gem : public Stone { private: double price; public: Gem() {}; Gem(double p, int w, double r) : Stone(w, r), price{p} {}; void show() { cout << "Weight: " << this->getWeight() << ", Radius" << this->getRadius(); } ~Gem() { cout << "Gem class was deleted." << endl; } }; int main (){ Stone* s1 = new Gem(2.3, 2, 3.2); delete s1; // Gem* g1 = new Gem(2.3, 2, 1.2); // delete g1; return 0; }
應用
大概常見的坑在上面都記錄好瞭,來看一段我寫的危險的程序(我大概抽象瞭一下),覆蓋瞭:野指針和為定義行為:
using namespace std; class A { private: int* a; public: int* create() { a = new int(); return a; } ~A(){ delete a; } }; int main () { A a; int* b = a.create(); delete b; return 0; }
- 每次調用 create 都會 new 一次,但隻 delete 瞭一次。
- 如果沒有調用 create 直接析構,未定義行為
- 如果 b 持有瞭 a.create() 的指針,然後 a 提前析構,那麼 b 是野指針
- delete b 是沒必要的。這樣會 double free,也是未定義行為
- 上述代碼沒有區分類裡面 new 且 返回的東西要在哪刪除合適
- 可以讓類來管理這一個 new,修改一下 create 的實現或者幹脆在構造 new,在析構 delete
總結
到此這篇關於C++踩坑實戰之構造和析構函數的文章就介紹到這瞭,更多相關C++構造和析構函數內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!