🔥個人主頁:胡蘿卜3.0
🎬作者簡介:C++研發方向學習者
📖個人專欄:??《C語言》《數據結構》?《C++干貨分享》
??人生格言:不試試怎么知道自己行不行
目錄
一、類的默認成員函數
二、構造函數
三、析構函數
一、類的默認成員函數
默認成員函數就是用戶沒有顯示實現,編譯器會自動生成的成員函數成為默認成員函數。
在一個類中,我們不寫的情況下編譯器會默認生成6個默認成員函數:
在這6個中最重要的是前4個,最后兩個取地址重載不是很重要,我們稍微了解一下即可。其次就是C++11以后還會增加兩個默認成員函數:移動構造和移動賦值,這個我們后面再講解。默認成員函數很重要,也很復雜,我們要從兩個方面去學習:
- 第一:我們不寫時,編譯器默認生成的函數行為是什么,是否滿足我們的需求。
- 第二:編譯器默認生成的函數不滿足我們的需求,我們需要自己實現,那么自己如何實現?
二、構造函數
有同學看到這個標題,就想說構造函數有什么好學的,不就是給對象開辟空間嗎?不用學的,不用學的~~。
ok,構造函數雖然名稱叫構造,但是構造函數的主要任務并不是開空間創建對象(我們常使用的局部對象是棧幀創建時,空間就開好了),構造函數的主要任務是對象實例化時初始化對象。構造函數的本質是要替代我們以前Stack和Date類中寫的Init函數(初始化函數)的功能,構造函數自動調用的特點就完美的替代了Init函數。構造函數完成初始化,初始化對象,不是開辟空間,并且構造函數是一個特殊的成員函數,接下來我們來看看它到底特殊在哪里。
構造函數的特點:
- 函數名和類名相同
- 無返回值。(返回值啥都不需要給,也不需要寫void,這里不要糾結,只要記得這是C++的規定)
- 對象實例化是系統會自動調用對應的構造函數。
- 構造函數可以重載。(在一個類里面可能需要多種初始化方式)
- 如果類中沒有顯示定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯示定義構造函數,編譯器將不在生成
- 無參構造函數、全缺省構造函數、我們不寫構造是編譯器默認生成的構造函數,都叫做默認構造函數,但是這三個函數有且只有一個存在,不能同時存在。無參構造函數和全缺省構造函數雖然構成函數重載,但是調用時會存在歧義。要注意很多同學會認為默認構造函數是編譯器默認生成那個叫默認構造,實際上無參構造函數、全缺省構造函數也是默認構造,總結?下就是不傳實參就可以調用的構造就叫默認構造。
- 我們不寫,編譯器默認生成的構造,對內置類型成員變量的初始化沒有要求,也就是說是否初始化是不確定的,要看編譯器。對于自定義類型成員變量,要求調用這個成員變量的默認構造函數初始化。如果這個成員變量,沒有默認構造函數,那么就會報錯,我們要初始化這個成員變量,需要用初始化列表才能解決,初始化列表,我們下個章節再細細講解。
?
ok,我們先看構造函數的前四個特點,通過上面的四點,我們就可以寫出一個構造函數了:
class Date
{
public://不傳參Date(){_year = 1;_month = 1;_day = 1;}//傳參Date(int year,int month,int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:// 內置類型int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(2025,8,4);d2.Print();return 0;
}
有同學看到上面的代碼,就在想,怎么給出了兩個函數名都是Date的函數?這是因為構造函數是可以重載的,編譯器根據參數,匹配相應的函數。
這時候又有其他的小伙伴看到Date d2(2025,8,4);,這個構造函數在傳參,并且有括號,就在想要不要給Date d1;的后面加個括號呢?
其實這里是不需要的,原因如下:
注意:當構造函數有形參時加上括號,沒有參數不需要加上括號
那這時候就會有同學要問了,那構造函數與我們之前寫的Init函數的區別是什么?為什么不想要Init函數?
Init函數是分離的,也就是對象的定義和初始化是分離的,先定義,后初始化,這就會導致忘記初始化,C++這個機制是為了保證對象定義實例化出來就一定初始化,因為對象實例化時會自動調用對應的構造函數,也就意味著只要寫了正確的構造函數,對象實例化出來一定
接下來,我們來看一下后三點(后三點比較重要):
特點5:如果類中沒有顯示定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯示定義構造函數,編譯器將不在生成
話不多說,直接看代碼
代碼1(類中沒有顯示定義的構造函數):
class Date { public:void Print(){cout << _year << "/" << _month << "/" << _day<<endl;} private:// 內置類型int _year;int _month;int _day; }; int main() {Date d1;d1.Print();return 0; }
為什么我沒有在類中定義構造函數,編譯器默認生成的構造函數給我的d1初始化成了隨機值呢?(后面再看)?
?代碼2(類中顯示定義構造函數):
class Date { public:Date()//無參構造函數{_year = 2025;_month = 8;_day = 3;}void Print(){cout << _year << "/" << _month << "/" << _day<<endl;} private:int _year;int _month;int _day; }; int main() {Date d1;d1.Print();return 0; }
ok啊,當我們在類中顯示定義了構造函數,編譯器沒有自動生成一個無參的默認構造函數
?我們知道構造函數是默認成員函數中的一種,默認成員函數是我們不寫編譯器自動生成。那這時就有同學想說了,默認構造函數不就是我們不寫編譯器自動生成的構造函數嗎?這種想法是錯誤的
特點6:默認構造函數包括無參構造函數、全缺省構造函數、我們不寫時編譯器默認生成的構造函數
在這三個默認構造函數中,當我們寫構造函數時,有且只有一個存在,不能同時存在。無參構造函數和全缺省構造函數雖然構成函數重載,但是調用時會存在歧義。
注意的是很多同學會認為默認構造函數是編譯器默認生成的那個叫默認構,實際上無參構造、全缺省構造也是默認構造
總結:不傳實參就可以調用的構造就是默認構造
class Date { public://全缺省構造函數Date(int year=2025, int month=8, int day=1){_year = year;_month = month;_day = day;}//無參構造函數Date(){_year = 2025;_month = 8;_day = 3;}void Print(){cout << _year << "/" << _month << "/" << _day<<endl;} private:int _year;int _month;int _day; }; int main() {Date d1;d1.Print();return 0; }
注意:無參構造和全缺省構造不能同時出現
補充:
?
通過上面的學習,我們知道如何寫一個構造函數了,那默認生成的構造會怎么處理成員呢?ok,接下來我們接著看:
特點7:我們不寫,編譯器默認生成的構造,對內置類型成員變量的初始化沒有要求,也就是說是否初始化是不確定的,要看編譯器,但是對于這種模棱兩可的行為,我們要當成內置類型不被處理。
對于自定義類型成員變量,要求調用這個成員變量的默認構造函數初始化。如果這個成員變量,沒有默認構造函數,那么就會報錯,我們要初始化這個成員變量,需要用初始化列表才能解決,初始化列表,我們下個章節再細細講解。
上面這兩段話中,對內置類型的處理很好理解,這對自定義類型成員變量的處理是啥意思?
總結:絕大多數情況下,構造函數都要我們自己寫,一般寫全缺省參數構造函數?
解答一下前面的問題:
?
三、析構函數
析構函數與構造函數功能相反,析構函數不是完成對對象本身的銷毀,比如:局部對象是存在棧幀的,函數結束棧幀銷毀,他就釋放了,不需要我們管。C++規定對象在銷毀時會自動調用析構函數,完成對象中資源的清理釋放工作。
析構函數的功能可以類比我們之前Stack實現的Destroy功能,而像我們上面寫的Date就沒有Destroy,其實就是沒有資源需要釋放,所以嚴格說Date是不需要析構函數的。
我們可以簡單的理解,析構函數是對類中所申請的空間進行釋放
析構函數的特點:
- 析構函數名是在類名前加上字符 ~。
- 無參數無返回值。 (這里跟構造類似,也不需要加void)
- ?個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。
- 對象生命周期結束時,系統會自動調用析構函數。
- 跟構造函數類似,我們不寫編譯器自動生成的析構函數對內置類型成員不做處理,自定類型成員會調用他的析構函數。
- 還需要注意的是我們顯示寫析構函數,對于自定義類型成員也會調用他的析構,也就是說自定義類型成員無論什么情況都會自動調用析構函數。
- ?個局部域的多個對象,C++規定后定義的先析構。
ok,我們先來看前四點,通過前四點,我們就知道如何寫一個析構函數了:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day =day;}~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;return 0;
}
當函數結束時,系統自動調用析構函數
class Stack
{
public:Stack(int n = 4){_a = (int*)malloc(n * sizeof(int));_top = 0;_capacity = n;}~Stack(){cout << "~stack()" << endl;if (_a){free(_a);_a = nullptr;_top = 0;_capacity = 0;}}
private:int* _a;int _top;int _capacity;
};
int main()
{Stack s1;return 0;
}
特點5:跟構造函數類似,我們不寫編譯器自動生成的析構函數對內置類型成員不做處理,自定類型成員會調用他的析構函數。
上面操作對內置類型成員的處理很簡單,我們來看看對自定義類型的處理是啥意思:
?
特點6:?我們顯示寫析構函數,對于自定義類型成員也會調用他的析構,也就是說自定義類型成員無論什么情況都會自動調用析構函數。
演示代碼:
class Stack { public:Stack(int n = 4){_a = (int*)malloc(n * sizeof(int));_top = 0;_capacity = n;}~Stack(){cout << "~stack()" << endl;if (_a){free(_a);_a = nullptr;_top = 0;_capacity = 0;}} private:int* _a;int _top;int _capacity; }; class MyQueue { public:~MyQueue( ){cout << "~myqueue()" << endl;} private:Stack _pushst;Stack _popst; }; int main() {MyQueue q;return 0; }
總結:
如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數,如Date;如果默認生成的析構函數就可以用,也就不需要顯示寫析構,如MyQueue;但是有資源申請時,?定要自己寫析構,否則會造成資源泄漏,如Stack。
?