一、類的默認成員函數
默認成員函數就是用戶沒有顯示實現,不過編譯器會自動生成的成員函數,稱為默認成員函數。一個類默認成員函數一共有6個,在我們不寫的情況下,編譯器就會自動生成這6個成員函數,不過我們重點要學習的是前面四個,后面兩個了解即可。還有就是在C++11后還增加了兩個默認成員函數,移動構造和移動賦值,這個我們后續講解C++11的時候也會進行講解。
默認成員函數比較復雜,我們主要從下面兩個方面進行學習理解:
1、我們不寫的時候,編譯器默認生成的函數行為是什么?是否可以滿足我們的需求?
2、編譯器默認生成的函數不滿足我們的需求的時候,那么我們就要自己去實現,那么要如何進行? ? ? ?實現呢?
下面我們就從幾種默認成員函數來入手:
二、構造函數
構造函數是一種特殊的成員函數,它的功能類似我們在前面實現鏈表、棧、隊列等的數據結構的初始化函數一樣,其是用來實現實例化對象初始化始化對象。還要注意的是,雖然名字叫做構造函數,但是其不會去開空間,其是局部變量進行初始化的。
下面是構造函數要注意的點:
1、構造函數的函數名和類的名字一樣
2、構造函數無返回值,而且我們在定義和聲明的時候連void都不需要寫
3、對象實例化的時候系統就會自動調用構造函數
4、構造函數可以重載
下面我們通過代碼來深入學習一下:
可以看到我們上面沒有去調用這個構造函數,那么我們看看其運行結果是否會自動調用這個構造函數使得我們的成員變量初始化:
? ?可以看到我們的成員變量被初始化成功了。所以說我們在實例化對象的時候編譯器會自動調用我? ? ?們的構造函數。
? ?我們上面還提到了,我們的構造函數還可以重載,也就是說我們的構造函數可以不止一個,可以? ? ? 有多個,然后編譯器會根據我們傳的參數來進行判斷該調用那個:
? ? ?運行結果如下:
? ? ?我們可以看到當我們要調用的是需要進行傳參的構造函數,那么我們在實例化的時候,就需要? ? ? ? 在后面加上括號然后進行傳參。
? ? ?那么我們對于沒有參數的那個構造函數,我們在實例化對象的時候加個括號是否可以呢?
? ? ? 答案是不行滴:
? ? 可以看到編譯器直接就報錯了。
? ? ?構造函數的使用要求還有以下幾個:
5、如果類中沒有顯示定義構造函數,那么C++編譯器會自動生成一個無參的默認構造函數,一旦? ? ? ?顯示定義那么編譯器不再生成。
6、無參構造函數、全缺省構造函數、我們不寫構造時編譯器默認生成構造函數這三種構造函數都? ? ? ?叫默認構造函數。但是我們前面學習函數重載的時候就知道,這三個函數就只能存在一個,不? ? ? ?可以共存。無參構造函數和全缺省構造函數雖然構造函數重載,但是不傳參調用時會有歧義。
? ? ? 可以看到錯誤信息中提示到,函數的調用不明確。
7、我們不寫構造函數,那么編譯器就會自動生成構造函數,對于內置類型成員變量的初始化是不? ? ? ? 確定的,那么具體被初始化成什么就由編譯器來確定了。對于自定義類型變量要求調用這個成? ? ? ? 員變量的默認構造函數進行初始化。如果這個成員變量沒有默認構造函數,那么編譯器就會報? ? ? ? 錯嗎,那么我們要初始化這個變量就需要用到初始化列表,初始化列表我們后續再進行講解。
? ? ?在C++中將數據類型分為內置類型和自定義類型
? ? ?內置類型:int、char、double等等
? ? ?自定義類型:我們使用class和struct等關鍵字進行定義的類型
下面我們就來看看編譯器自動生成的構造函數是如何的:
?我們可以看到我們在定義類的時候我們沒有寫構造函數的,那么我們在實例化對象的時候,我們的編譯器就會自動幫我們默認構造函數,但是其具體是初始化什么內容我們就不知道了,我們上面的代碼對成員變量的初始化就沒啥有要求了。
還有一種情況就是我們的成員變量也是一個類,然后我們的成員變量有構造函數,那么我們這個類就可以不寫構造函數了,那么其就會使用成員變量那個類的構造函數。
比如我們前面學習的數據結構中,使用棧實現隊列,那么我們的隊列類中的成員變量就是兩個棧:
可以看到兩個棧確定被初始化為了4。
要是我們的棧中的構造函數,其參數修改成需要進行傳參的函數,那么就需要用來初始化列表了,
?不過我們可以知道的是,對于大部分的類,我們都需要自己去寫構造函數,因為編譯器默認的很多情況都不能滿足需求。
三、析構函數
析構函數的功能和構造函數是相反的,有點類似于我們在實現棧的時候的Destroy函數的功能,用來完成對對象中的清理釋放工作。要注意的是其不是完成對對象的銷毀,比如局部對象是有棧幀的,函數結束的時候會銷毀棧幀,那么這個局部對象也就跟著銷毀了,所以對于局部對象我們是不要理的,其也不需要析構函數。但是C++中規定了對象在銷毀時會自動調用析構函數,用于完成資源的清理。如果對于一個類沒有資源要進行釋放的,那么理論上就不需要析構函數。
下面是析構函數使用上的一些要求:
1、析構函數名是在類名前加上字符~。
2、析構函數和構造函數一樣是無參無返回值的,而且也不需要寫void。
3、一個類中只能有一個析構函數只能有一個析構函數。若未顯示定義,那么系統就會自動生成默? ? ? ?認的析構函數。
4、對象生命周期結束時,系統就會自動調用析構函數。
5、一個局部域有多個實例化對象的時候,后定義的會先進行析構。
下面我們通過代碼來感受一下:
?
可以看到st1和st2都已經按照我們給的參數進行初始化了,然后我們繼續往下運行:
?可以看到我們是先將st2的銷毀了,然后再對st1進行銷毀的。
6、和構造函數一樣,我們不寫的話,那么編譯器就會自動去生成這個函數,其不對內置類型的成? ? ? ?員變量操作,自定義類的成員變量就會進行處理。
7、還需要注意的是,我們顯示寫析構的時候,對于自定義類型的成員其也會調用它的析構函數,? ? ? ?不會受到在這個類中寫的析構函數的影響。也就是說自定義類型不論什么情況下都會去自動調? ? ? ?用析構函數。
我們前面學習數據結構的時候,我們寫了道使用棧實現隊列的題目,下面我們通過這個例子來看看,我們可以在析構函數中打印一些東西來看這個函數被調用了多少次:
首先就是,我們知道的是,我們的隊列自定義類型中,其有兩個成員變量,編譯器會其調用這個成員變量的析構函數,那么就會其棧類中調用,那么我們一共兩個棧,所以其調用兩次。
?還有就是我們要是沒有去向操作系統申請空間,那么我們的析構函數其實是可以不寫的,直接使用編譯器默認生成的析構函數即可,所以當我們需要進行空間申請的時候,一定要寫析構函數,不然就會造成內存泄漏。
四、拷貝構造函數
如果一個構造函數的第一個參數是自身類類型的引用,而且任何地方的參數都有默認值,那么此函數就叫做拷貝構造函數。
下面是拷貝構造函數使用的時候的特點:
1、拷貝構造函數是構造函數的一個重載。
2、拷貝構造函數的第一個參數必須是類類型對象的引用,使用傳值方式的話編譯器會直接報錯,? ? ? ?這是因為在語法邏輯上會導致無窮遞歸調用。拷貝構造函數也可以有多個參數,但是第一個參? ? ? ?數一定要保證是類類型對象的引用,而且后面的參數一定要有缺省值。
我們上面的代碼,將a1作為參數傳給a2的拷貝構造函數,那么a2的值就會和a1的一樣了。
還有就是我們的拷貝構造函數的參數部分可以加一個const關鍵字,因為我們不想被拷貝的對象被改變,還有就是對于被const關鍵字修飾的對象我們也可將其拷貝。
?那么為啥不可以傳值呢?這是因為傳值的話,其會導致無窮遞歸調用拷貝構造函數,那是為啥會導致這個問題呢?
這是因為我們傳值的時候,是一種淺拷貝,那么會創建一個臨時變量先將參數的進行拷貝,但是我們在創建這個臨時變量的時候,因為其也是類類型,那么其進行拷貝那么就會去調用拷貝構造函數,那么就造成了無窮遞歸。
引用傳參的話,那么其是使用的實參的別名,那么在傳參的過程中是不會產生拷貝的,其實際上就直接對這個傳入的參數的內容進行拷貝了,所以不存在淺拷貝,就直接進入到函數中了,所以不會導致無窮遞歸。
3、C++中規定了自定義類型對象進行拷貝的行為必須調用拷貝構造,所以自定義類型傳參和傳值? ? ? ?都會調用拷貝構造函數來完成。
4、要是未顯示定義拷貝構造函數,那么編譯器就會自動生成拷貝構造函數,那么自動生成的拷貝? ? ? ? 構造函數對內置類型的成員變量會完成值拷貝,即一個字節一個字節的拷貝,和我們前面學習? ? ? ? C語言的時候對字符串進行拷貝一樣,對于自定義類型的成員變量那么就會調用其拷貝構造函? ? ? ? 數。
不過對于一些比較復雜的成員變量,要是使用編譯器自動生成的拷貝構造函數,會造成不好的效果,比如我們的棧成員變量:
?
?
可以看到我們的程序就直接運行不起來了,這是因為st1初始化的時候,_a會被分配一塊空間,但是st2就是st1的淺拷貝,但是此時st2中的_arr和st1中的_arr使用的是一塊空間了。
所以這種情況下我們一定要自己去寫拷貝構造函數來實現深拷貝。
5、像我們上面的Date類中,其成員變量全是內置類型而且沒有什么指向資源,那么編譯器自動生? ? ? ? 成的拷貝構造函數就可以完成了,所以我們可以不去寫,但是像Stack類這樣的,雖然其也是? ? ? ? ? 內置類型,但是_arr其指向了資源,編譯器自動生成的拷貝構造函數完成的是值拷貝,達不到? ? ? ? 我們的需求,所以我們要自己實現一個深拷貝。還有就是很前面的使用棧實現隊列中的隊列類? ? ? ? 也是一樣,其成員變量的棧的類其有顯示的拷貝構造函數,那么我們的隊列類中可以不寫,其? ? ? ? 會去調用棧的拷貝構造函數。
? ? ? 下面有個技巧:
? ? ? ?如果一個類顯示的實現了析構函數,而且釋放了資源,那么其就需要顯示的寫拷貝構造。
6、傳值返回會產生一個臨時對象的拷貝構造,傳值引用返回,返回的是返回對象的別名,那么就沒產生拷貝。那么如果返回對象是一個當前函數的局部域的局部對象,那么函數結束就銷毀了,那么使用引用返回是有問題的,此時的引用就是野引用了。