=========================================================================
相關代碼gitee自取:
C語言學習日記: 加油努力 (gitee.com)
?=========================================================================
接上期:
【C++初階】三、類和對象
(面向過程、class類、類的訪問限定符和封裝、類的實例化、類對象模型、this指針)
-CSDN博客
?=========================================================================
? ? ? ? ? ? ? ? ? ? ?
引入:類的六個默認成員函數
如果一個類中什么成員都沒有,簡稱為空類。
但空類中并不是什么都沒有,任何類在什么都不寫時,
編譯器會自動生成以下六個默認成員函數,
默認成員函數:用戶沒有顯式實現時,編譯器會自動生成的成員函數稱為默認成員函數? ? ? ? ? ? ? ? ? ? ?
- 初始化和清理:
構造函數(1)?-- 完成成員變量的初始化工作
析構函數(2) -- 完成一個對象結束生命周期后的資源清理工作
? ? ? ? ? ? ??- 拷貝復制:
拷貝構造函數(3) -- 使用同類對象初始化創建對象
賦值重載(4) -- 把一個對象賦值給另一個對象
? ? ? ? ? ? ? ??- 取地址重載:
主要是普通對象(5)和const對象取地址(6),這兩個很少會自己實現
? ? ? ? ? ? ? ? ?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
? ? ? ? ? ? ? ? ? ? ?
一 . 構造函數(難)
構造函數的概念和特性:
? ? ? ? ? ? ? ? ? ?
C++構造函數的概念:
還是假設有以下Date類:
//日期類: class Date { public://我們自己定義的初始化函數:void Init(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.Init(2023, 11, 16);d1.Print();Date d1;d1.Init(2023, 11, 17);d1.Print();return 0; }
- 對于以上的Date類,可以通過我們自己定義的 Init共有函數(方法)給對象設置日期,
但如果每次創建對象時都需要調用該方法初始化對象成員的話,
會有點麻煩而且可能會忘記初始化,那能否在對象創建時就自動進行初始化呢?
? ? ? ? ? ? ? ? ? ??- C++為了優化C語言需要自己初始化的情況,有了一個新概念:構造函數。
構造函數是特殊的成員函數,其名字和類名相同,
創建類類型對象時由編譯器自動調用進行對象的初始化,
以保證每個數據成員都有一合適的初始值,并且在對象整個聲明周期內只會調用一次
? ? ? ? ? ? ? ? ??- 構造函數分為有參構造函數和無參構造函數,
我們在創建對象時可以設置各成員變量初始化的值,
如果沒有設置,則對象初始化時會調用無參構造函數,
如果設置了,則對象初始化時會調用相應的有參構造函數Date類 -- 圖示:
? ? ? ? ? ? ? ? ??
主函數通過構造函數創建對象 -- 圖示:
? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ??
---------------------------------------------------------------------------------------------? ? ? ? ? ? ? ? ? ??
C++構造函數特征:
? ? ? ? ? ? ? ? ? ?
- 構造函數名和類名相同,構造函數沒有返回值,
對象實例化時編譯器會自動調用對應的構造函數
? ? ? ? ? ? ? ??- 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,
一旦用戶顯式定義構造函數,編譯器將不再自動生成構造函數,
所以如果定義了有參構造函數,最好再定義一個無參構造函數,
防止創建對象時需要無參構造函數而又無法調用到
? ? ? ? ? ? ? ??- 構造函數也是函數,可以有參數,所以也可以對其設置缺省參數,
將一個有參構造函數(初始化全部成員變量的構造函數)的
所有參數都設置一個缺省參數(全缺省構造函數),
這樣該構造函數就既實現了有參構造函數的任務,
又實現了無參構造函數的任務,因為初始化對象時如果不給初始化值,
那么有參構造函數的缺省參數就會發揮作用,實現無參構造函數的任務
這樣一個構造函數就可以替代有參和無參兩個構造函數了
? ? ? ? ? ??- 構造函數支持重載,雖然支持重載,
但如果已經定義了全缺省構造函數,已經能夠實現無參構造函數的情況下,
這時如果再定義一個無參構造函數,雖然構成了構造函數重載,
但是實際調用時是會出錯的,因為全缺省構造函數和無參構造函數的功能重復了,
編譯器就會不知道該調用哪個構造函數了
? ? ? ? ? ? ??- 無參的構造函數和全缺省的構造函數都稱為默認構造函數,
并且默認構造函數只能有一個(否則會有調用歧義)
注意:
無參構造函數、全缺省構造函數、編譯器默認生成的構造函數,
都可以認為是默認構造函數
(不傳參數還可以被調用的構造函數,都可以叫默認構造函數)全缺省構造函數 -- 圖示:
? ? ? ? ? ? ? ? ? ? ? ? ? ??
編譯器默認生成的構造函數的作用:
- C++中把類型分成了內置類型(基本類型)和自定義類型,
內置類型就是語言原生的數據類型(int、double、指針……);
自定義類型就是我們使用 class / struct / union 自己定義的類型。
關于編譯器生成的默認構造函數,該構造函數會對我們未定義的成員變量進行初始化
? ? ? ? ? ? ? ?- 不同編譯器的初始化方式不同,
VS2013中:
如果對象的成員變量為內置類型,
默認生成構造函數不會對其進行處理(為隨機值);
如果對象的成員變量為自定義類型,
默認生成構造函數則會調用該自定義類型的默認構造函數
VS2019中情況會更復雜:
如果對象的成員變量全是內置類型,
默認生成構造函數不會對其進行處理(為隨機值);
如果對象的成員變量既有內置類型又有自定義類型,
則會對其中的內置類型進行處理(int類型成員變量會被初始化為0),
對其中的自定義類型,會調用該自定義類型的默認構造函數
? ? ? ? ? ?- 所以默認生成的構造函數會根據對象的成員變量的情況來判斷是否要對其進行處理,
如果對象的成員變量為自定義類型,就調用該自定義類型的默認構造函數;
如果是內置類型,則不進行處理(為隨機值)
(會處理自定義類型,不一定處理內置類型(看編譯器),建議統一當成不會進行處理)圖示:
? ? ? ? ? ?
- 因此C++11中針對內置類型成員不初始化的缺陷,又打了一個補丁:
內置類型成員變量在類中聲明時可以給默認值
(給了默認值又有定義顯式構造函數的話,以顯式構造函數為準)圖示:
? ? ? ? ? ? ? ? ? ? ?
總結:
- 一般情況下,我們都要自己寫構造函數
? ? ? ? ? ? ?- 成員變量如果都是自定義類型,或者成員變量聲明時給了缺省值,
那就可以考慮讓編譯器自己生成構造函數
? ? ? ? ?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
? ? ? ? ? ? ?
二 . 析構函數
析構函數的概念和特性:
? ? ? ? ? ? ? ? ?
C++析構函數的概念:
? ? ? ? ? ??
- 通過前面對構造函數的了解,我們知道了一個對象是怎么來的,
可一個對象又是怎么沒的呢?如果說構造函數是我們以前寫的Init初始化函數,那么析構函數就是我們以前寫的Destroy“銷毀”函數
? ? ? ? ? ? ? ? ? ? ? ??- 析構函數和構造函數的功能相反,但析構函數不是完成對對象本身的銷毀,
局部對象銷毀工作是由編譯器完成的。
而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ??
---------------------------------------------------------------------------------------------? ? ? ? ? ? ? ? ? ??
C++析構函數的特性:
? ? ? ? ? ? ? ??
- 析構函數名 = 在類名前加上字符 “~” (按位取反符號)
? ? ? ? ? ? ? ? ? ??- 析構函數沒有返回值和函數參數
? ? ? ? ? ??- 一個類只能有一個析構函數,若沒有顯式定義,編譯器會自動生成默認的析構函數
(注:析構函數不支持重載)
? ? ? ? ? ? ? ? ??- 對象聲明周期結束時,C++編譯系統會自動調用析構函數
析構函數 -- 圖示:
? ? ? ? ? ? ? ? ? ? ? ? ? ??
編譯器默認生成的構造函數的作用:
- 默認生成的析構函數,其行為跟構造函數的類似,
針對內置類型的成員變量,析構函數不會對其進行處理;
針對自定義類型的成員變量,析構函數也會調用該自定義類型的默認析構函數
? ? ? ? ? ? ? ? ? ?- 如果類中沒有申請資源,析構函數可以不寫,直接使用編譯器生成的默認析構函數,
比如之前寫的Date日期類就可以不寫;
而如果類中有申請資源,則一定要寫析構函數,否則會導致資源(內存)泄漏,
比如Stack棧類就需要顯式定義析構函數進行資源清理圖示:
? ? ? ? ?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
? ? ? ? ? ? ?
三 . 拷貝構造函數(難)
拷貝構造函數的概念和特性:
? ? ? ? ? ? ? ??
C++拷貝構造函數的概念:
? ? ? ? ? ? ? ? ? ? ?
拷貝構造函數:
只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),
在使用已存在的類類型對象拷貝創建新對象時由編譯器自動調用圖示:
? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ??
---------------------------------------------------------------------------------------------? ? ? ? ? ? ? ? ? ??
C++拷貝構造函數的特性:
? ? ? ? ? ? ? ? ? ? ? ?
- 拷貝構造函數也是特殊的成員函數,是構造函數的一個重載形式
? ? ? ? ? ? ? ? ?- 拷貝構造函數的參數只有一個且必須是類類型對象的引用,
使用傳值方式作為其參數編譯器會直接崩潰,因為會引發無窮遞歸調用
? ? ? ? ? ? ? ? ? ? ? ?- 如果沒有顯式定義拷貝構造函數,編譯器會生成默認的拷貝構造函數。
默認的拷貝構造函數拷貝對象時會按內存存儲按字節序完成拷貝,
這種拷貝叫做淺拷貝,或者值拷貝注:
在編譯器生成的默認拷貝構造函數中,內置類型是按照字節方式直接拷貝的(值拷貝),
而自定義類型則會調用該自定義類型的拷貝構造函數完成拷貝圖示:
? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ??
- 編譯器生成的默認拷貝構造函數已經可以完成字節序的值的拷貝(值拷貝)了,
當類中沒有涉及資源申請(申請動態空間等)時,
淺拷貝已經足夠使用了,是否顯式定義拷貝構造函數都可以;
但是一旦涉及到了資源申請時,則拷貝構造函數是一定要顯式定義的,進行深拷貝???????圖示:
? ? ? ? ? ? ? ? ? ?
- 拷貝構造函數典型調用場景:
使用已存在的對象來“拷貝”創建新對象、函數參數類型為類類型對象、
函數返回值類型為類類型對象注:
為了提高效率,一般對象傳參時,盡量使用引用類型返回,
返回時根據實際場景,能用引用返回盡量使用引用返回
? ? ? ? ?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
? ? ? ? ? ? ?
四 . 賦值運算符重載
運算符重載的使用和注意事項
? ? ? ? ? ? ? ? ? ?
引言:
對于內置類型(int、double……)的數據,我們可以直接對其使用運算符,
假設我們有整型變量a和b,我們可以對其使用:
???????a == b(判斷相等)?、a > b(判斷大小),
但對自定義類型的數據而言,就不能直接對其使用運算符,
因為編譯器不知道怎么判斷我們自定義的類型,所以需要我們自己定義其判斷的規則圖示 --? 自定義類型判斷規則:
? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ??
---------------------------------------------------------------------------------------------? ? ? ? ? ? ? ? ? ??
運算符重載的使用:
- C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,
???????也具有其返回值類型、函數名字以及參數列表,
其返回值類型與參數列表和普通的函數類似
? ? ? ? ? ? ? ??- 函數名字:關鍵字operator后接需要重載的運算符符號
(如:加法運算符重載? --? operator+)?
? ? ? ? ? ? ? ? ??- 函數原型:返回值類型 operator操作符(參數列表)
圖示 --? 類外運算符重載:
? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ??
---------------------------------------------------------------------------------------------? ? ? ? ? ? ? ? ? ??
運算符重載的注意事項:
- 不能通過連接其它符號來創建新的操作符:比如operator@
? ? ? ? ? ? ??- 重載操作符必須有一個類類型參數
? ? ? ? ? ? ? ? ? ??- 用于內置類型的運算符,其含義不能改變,
例如:內置的整型+ ,不能改變其含義( + 和 += 是不一樣的 )
? ? ? ? ? ? ? ?- 重點:
作為類成員函數重載時,其形參看起來比操作數數目少一個,
因為成員函數的第一個參數為隱藏的this指針
? ? ? ? ? ? ? ??- 注意以下五個運算符不能重載:
“ .* ”? 、?“ :: ”? 、“ sizeof ”? 、“ ?: ”? 、“ . ”?????圖示 --? 類中運算符重載:
? ? ? ? ? ? ? ? ? ? ??
- 一個類要重載哪些運算符,主要看這個運算符對這個類來說有沒有意義,
有意義就可以重載,沒有意義就不要重載,
對日期類來說,日期的 +(加) *(乘) /(除) 都沒有意義,但 -(減) 是有意義的,
兩個日期相減可以計算兩日期相差了多少天;
日期+日期沒有意義,但日期+整型是有意義的,
如:d1 + 100 ,計算d1日期的100天后的日期圖示 --? 類中實現 += 和 + 運算符重載:
(注:“+=”運算符重載中要設置返回值 -- return *this ,這里忘了寫了)
? ? ? ? ? ? ? ? ? ? ?? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ??
賦值運算符(=)重載
? ? ? ? ? ? ? ??
賦值運算符 -- "=" ,賦值運算符重載就是讓自定義類型也能像內置類型一樣使用”=“
? ? ? ? ? ? ? ?
賦值運算符重載格式:
- 參數類型:const T&
const修飾參數,能夠防止賦值(拷貝)時左右值寫反了,導致改變了原對象
T& 傳參引用接收右值的“別名”,提高傳參效率
? ? ? ? ? ? ? ? ????????- 返回值類型:T&
引用返回可以提高返回的效率,設置返回值還為了支持“=”的連續賦值
? ? ? ? ? ? ? ? ?- 定義賦值運算符重載函數時,需要檢測是不是“自己給自己賦值”的情況
? ? ? ? ? ? ? ? ? ? ?- 最終返回*this(即返回被賦值對象本身),能夠符合“=”連續賦值的含義
? ? ? ? ? ? ?- 用戶沒有顯式實現時,編譯器會生成一個默認的賦值運算符重載函數,
其行為和拷貝構造函數類似:
針對內置類型成員變量:進行 值拷貝(淺拷貝)
針對自定義類型成員變量:會調用該自定義類型的 賦值運算符重載函數
注意:
如果類中沒有“資源”(Date類),賦值運算符重載函數要不要顯式定義都可以;
如果類中有“資源”(Stack類),賦值運算符重載函數必須要顯式定義
? ? ? ? ? ? ? ? ? ? ??賦值運算符只能重載成類的成員函數(只能在類中定義重載),不能重載為全局函數
原因:
賦值運算符重載函數如果不顯式實現,編譯器會生成一個默認的。
此時如果再在類外實現一個全局的賦值運算符重載函數,
就會和編譯器在類中生成的默認賦值運算符重載函數沖突了,
???????所以賦值運算符重載函數只能是類的成員函數圖示 -- 以Date類為例:
?????