C++初階——類和對象(三)
上期內容,我們圍繞類對象模型的大小計算,成員存儲方式,this指針
,以及C++實現棧和C語言的比較,進一步認識了C++的封裝
特性。本期內容,我們開始介紹類的默認成員函數,這是類和對象板塊中最為重要,也是相對復雜的內容,讓我們一起探索!
引言
如果一個類中什么成員都沒有,簡稱為空類。空類中真的什么都沒有嗎?并不是,任何類在什么都不寫時,編譯器會自動生成以下6個默認成員函數(默認成員函數:用戶沒有顯式實現,編譯器會生成的成員函數稱為默認成員函數):
- 初始化和清理:構造函數、析構函數
- 拷貝復制:拷貝構造函數、賦值重載函數
- 取地址重載:普通對象取地址重載和const對象取地址重載函數
一、構造函數
1.背景引入
我們再來回顧一下,_year
,_month
,_day
是三個私有的成員變量,Init
和Print
是外界可以直接訪問的成員函數,在main函數中,我們先要通過類來示例化出一個對象,比如這里的Date d1;
Date d2;
Date d3;
等等,然后調用類里面的成員函數。這里基于同一個類創建了很多對象,然而在成員函數中似乎并沒有傳遞代表各個對象的形參,那么編譯器是怎么區分的呢?很簡單,由于this指針
的存在。我們以d1.Init(2025,3,10);
為例,this指針
指向當前的對象——d1
,然后在調用函數Init
時,隱式傳遞了這個指針,然后通過這個指針訪問d1中的成員變量_year、_month、_day,將它們分別賦值為2025、3、10,如圖所示:
當運行到下一個對象d2
時,this指針
指向的就是d2
了。
進一步探究
如果我們仔細觀察,不難發現這里分為了兩步走:第一步,先通過類實例化一個對象,也就是Date d1;
,然后再用具體的數值將其初始化d1.Init(2025,3,10);
,其實是有些麻煩的,我們能不能在實例化對象的同時,就將信息設置進去呢?——答案是肯定的,C++本身就是對C語言的優化,構造函數應運而生!
2.什么是構造函數?
構造函數是一個特殊的成員函數,名字與類名相同。創建類對象時由編譯器自動調用,以保證每個數據成員都有一個合適的初始值,并且在對象整個生命周期內只調用一次。
在這里,我們已經將原來的初始化函數改造成了一個構造函數,構造函數的函數名與類名相同,沒有返回值(這里的沒有返回值不是說返回一個void,寫成void Date(int year, int month, int day)
,而是什么都不要寫,直接就是Date(int year, int month, int day)
),至于為什么,這是語法規定,這是一個特殊的函數,有特殊的待遇,只有這樣寫,編譯器才知道這是構造函數。對于構造函數的調用,也是很有趣的,我們在實例化對象的同時,后面跟上了一個括號,里面就是想要在對象中填入的信息Date d1(2025, 3, 10);
,這樣一來,我們不僅實例化了一個對象,還填入了我們想要的信息,一步解決,非常方便。之前的那種兩步走,是先根據圖紙建房子,然后里面的家具還要自己來置辦;現在是把裝修也一并讓別人做好了,自己直接拎包入住,確實方便的多。
3.構造函數的特性
構造函數是特殊的成員函數,需要注意的是,構造函數雖然名稱叫構造,但是構造函數的主要任務并不是開空間創建對象,而是初始化對象。也很好理解,之前是兩步走,現在是一步走,至于減少的那一步——填入想要的信息,就是構造函數的功勞。
(1)構造函數的重載
構造函數是可以重載
的,根據傳參的不同,調用對應的構造函數,如圖所示:
這里就寫了兩個構造函數,其中一個是無參數的,這些都可以理解,但是,我們注意到,為什么在調用的時候寫的是Date d1;
而不是Date d1();
呢?我們先來看一下結果:
這里什么都沒有輸出,也就是沒有調用那個構造函數,這是為什么呢?仔細觀察我們發現,Date d1();
像是函數的聲明,Date
是返回值的類型,d1
是函數名,()表示這個函數不需要傳參。基于這種情況,編譯器不知道這里到底是在類實例化對象還是在聲明一個名為d1的函數。因此,我們對于無參的構造函數,在實例化對象時就不需要加()
了。
當然,還有一點,一開始的構造函數示例中,是提供了缺省參數
的,而且是全缺省,這里做出了改變。如果依然是全缺省會發生什么呢?如圖所示:
還是一個調用不明確
的問題:不傳參,不僅無參的函數可以調用,全缺省的也可以調用,因為它可以自動幫你把數據補上。在C++入門中已經講過這一點,這里就當是復習一下吧。
(2)默認構造函數
默認構造函數的分類
默認構造函數是無需參數即可調用的構造函數,我們既可以顯式聲明,也可以讓編譯器隱式生成。無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為是默認構造函數
,再簡單一點,不需要傳參就可以調用的構造函數都是默認構造函數:
- 無參構造函數
- 全缺省構造函數
- 編譯器默認生成的構造函數
編譯器自動生成的構造函數
如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。注意,這里是無參的,也就是說調用的時候直接寫Date d1;
即可,如圖所示:
這里的默認構造函數似乎沒什么用,對于這些變量的值,好像也沒有進行處理,依舊是隨機值
,那么默認構造函數還有它存在的價值嗎?
其實是這樣的:C++把類型分成內置類型(基本類型)和自定義類型。內置類型就是語言提供的數據類型,如:int、char、double……,自定義類型就是需要自己定義的,如struct、class……
編譯器生成的默認構造函數對內置類型不作處理,對自定義類型則是調用它的默認構造。因此,一般情況下,有內置類型成員,就需要自己寫構造函數,不能用編譯器自己生成的;全部都是自定義類型成員,可以考慮讓編譯器自己生成。
我們來舉個自定義類型的例子:
在Date類的成員變量中,不僅有內置類型,還有自定義類型變量Time _t
;那么在實例化對象時,內置類型不作處理
,自定義類型調用它的默認構造
,而Time類的默認構造我們已經自己定義了,因此會打印出對應的結果,我們也可以調試看一看:
由于Date
類中沒有自己寫構造函數,內置類型都是隨機值,而自定義類型Time _t就調用了它的默認構造,而Time的默認構造是我們自己實現的,對內置類型進行了初始化。
- C++11 中針對內置類型成員不初始化的缺陷,又打了補丁,即:內置類型成員變量在類中聲明時可以給默認值:
這里沒有自己寫構造函數,默認值給在成員變量的聲明里。
二、析構函數
析構函數是特殊的成員函數,其特征如下:
- 析構函數名是在類名前加上字符 ~。
- 無參數無返回值類型。
- 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:析構函數不能重載
- 對象生命周期結束時,C++編譯系統系統自動調用析構函數。
1.自己定義的析構函數
我們以一個棧為例,因為棧中涉及到動態內存開辟,不使用了需要及時銷毀開辟的內存空間,否則會造成內存泄漏
,有了析構函數,我們只需要在類中定義一下,在類外它就會自動調用了,代碼如下:
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc fail");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}cout << "~Stack" << endl;}
private:DataType* _array;int _capacity;int _size;
};int main()
{Stack st1;return 0;
}
運行結果如圖:
2.編譯器自動生成的析構函數
關于編譯器自動生成的析構函數
,是否會完成一些事情呢?下面的程序我們會看到,編譯器生成的默認析構函數,對自定類型成員調用它的析構函數。
如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數,比如Date
類;有資源申請時,一定要寫,否則會造成資源泄漏,比如Stack
類。
本期總結+下期預告
本期內容非常重要,詳細介紹了類的構造函數和析構函數,下期將繼續講解拷貝構造函數等相關內容!
感謝大家的關注,我們下期再見!