03》類和對象(中)——從零拆解六大默認(rèn)成員函數(shù)底層邏輯)
目錄一.什么是默認(rèn)成員函數(shù)1.1六大默認(rèn)成員函數(shù)1.2為什么需要默認(rèn)成員函數(shù)二.1構(gòu)造函數(shù)2.1概念與定義2.2默認(rèn)構(gòu)造函數(shù)的局限性2.3內(nèi)置與自定義類型默認(rèn)行為二.2初始化列表與構(gòu)造函數(shù)三.析構(gòu)函數(shù)3.1析構(gòu)函數(shù)概念及定義3.2內(nèi)置類型與動態(tài)資源的析構(gòu)四.拷貝構(gòu)造函數(shù)4.1拷貝構(gòu)造函數(shù)的定義4.2深拷貝與淺拷貝五.賦值運(yùn)算符重載5.1運(yùn)算符重載operator5.2賦值運(yùn)算符重載六.取地址運(yùn)算符重載6.1const修飾對象6.2取地址重載相關(guān)專欄與源碼導(dǎo)航配套源碼 本專欄所有配套代碼已同步至 GitHub點(diǎn)擊直達(dá)系統(tǒng)專欄 《從零開始學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)》專欄直達(dá)? 《C 深度解構(gòu)》專欄直達(dá)一.什么是默認(rèn)成員函數(shù)概念定義在我們沒有顯示實(shí)現(xiàn)編譯器默認(rèn)生成的成員函數(shù)1.1六大默認(rèn)成員函數(shù)構(gòu)造函數(shù)析構(gòu)函數(shù)拷貝構(gòu)造函數(shù)賦值重載函數(shù)取地址重載函數(shù)1.2為什么需要默認(rèn)成員函數(shù)在C語言中功能的實(shí)現(xiàn)必定離不開初始化 \ 資源銷毀但是在操作中很容易忘記操作的實(shí)施但是在C中將對象的“初始化”和 “資源釋放”與生命周期強(qiáng)行綁定創(chuàng)造對象時自動初始化構(gòu)造離開作用域自動銷毀析構(gòu)。內(nèi)置類型的賦值非常明顯int a b, 對于類類型而言也需要支持對象之間的拷貝與賦值功能所以需要我們自定義一個函數(shù)來保證對象復(fù)制時的完整與安全。二.1構(gòu)造函數(shù)函數(shù)特性函數(shù)名與類名一致無返回值對象實(shí)例化時編譯器自動調(diào)用、支持重載功能定位構(gòu)造函數(shù)功能類似于語言的 Init2.1概念與定義無參構(gòu)造函數(shù)全缺省構(gòu)造函數(shù)未顯式定義自動生成的默認(rèn)構(gòu)造函數(shù)核心避坑 \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 }}核心避坑?默認(rèn)構(gòu)造函數(shù)不僅僅只包含未顯示定義時編譯器默認(rèn)生成的構(gòu)造函數(shù)上面提到的三種統(tǒng)稱為默認(rèn)構(gòu)造函數(shù)。歧義無參構(gòu)造函數(shù)和全缺省構(gòu)造函數(shù)不能同時存在因?yàn)槎嘉磦鲄⒕幾g器無法正確調(diào)用 \colorbox{#FFEAEA}{\color{#C22828}\text{歧義無參構(gòu)造函數(shù)和全缺省構(gòu)造函數(shù)不能同時存在因?yàn)槎嘉磦鲄⒕幾g器無法正確調(diào)用}}歧義無參構(gòu)造函數(shù)和全缺省構(gòu)造函數(shù)不能同時存在因?yàn)槎嘉磦鲄⒕幾g器無法正確調(diào)用?錯誤示例對比Data(intyear2022,intmonth10,intday9);// 全缺省Data();// 無參Data d1;// ? 語法允許兩者同時存在但實(shí)例化時不傳參會引發(fā)調(diào)用歧義編譯報錯2.2默認(rèn)構(gòu)造函數(shù)的局限性常見情形大多數(shù)情況下如果不顯式定義構(gòu)造函數(shù)編譯器自動生成的構(gòu)造函數(shù)無法滿足我們資源初始化的要求特殊情形經(jīng)典案例兩個棧實(shí)現(xiàn)隊(duì)列 \colorbox{#FFEAEA}{\color{#C22828}\text{經(jīng)典案例兩個棧實(shí)現(xiàn)隊(duì)列}}經(jīng)典案例兩個棧實(shí)現(xiàn)隊(duì)列?在模擬隊(duì)列Queue類中含有兩個成員變量Stack對象這里的Stack為自定義類型。實(shí)例化Queue時因?yàn)槌蓡T變量為自定義類型所以編譯器生成的默認(rèn)構(gòu)造函數(shù)會自動調(diào)用Stack內(nèi)部的默認(rèn)構(gòu)造函數(shù)來進(jìn)行初始化所以這種情況我們無需再給出構(gòu)造函數(shù)2.3內(nèi)置與自定義類型默認(rèn)行為內(nèi)置類型int, double, 指針等如果未顯示實(shí)現(xiàn)構(gòu)造函數(shù)那么編譯器會不做處理變量呈現(xiàn)的值為隨機(jī)值并非編譯器主動賦予而是直接忽略了該內(nèi)存塊的初始化內(nèi)存并非為空所以讀取到了之前的垃圾值自定義類型若自定義類型成員含有對象那么不顯示實(shí)現(xiàn)構(gòu)造函數(shù)會調(diào)用對象內(nèi)部默認(rèn)構(gòu)造函數(shù)。如果給了含參構(gòu)造函數(shù)但是沒有傳入?yún)?shù)或未指定缺省值那么編譯器會因?yàn)檎也坏綐?gòu)造函數(shù)而報錯 \colorbox{#FFEAEA}{\color{#C22828}\text{ 如果給了含參構(gòu)造函數(shù)但是沒有傳入?yún)?shù)或未指定缺省值那么編譯器會因?yàn)檎也坏綐?gòu)造函數(shù)而報錯}}如果給了含參構(gòu)造函數(shù)但是沒有傳入?yún)?shù)或未指定缺省值那么編譯器會因?yàn)檎也坏綐?gòu)造函數(shù)而報錯?//未顯式實(shí)現(xiàn)構(gòu)造函數(shù)classHy{public:voidprint(){cout_aendl;}private:int_a;};intmain(){Hy a1;a1.print();//打印結(jié)果未垃圾值return0;}二.2初始化列表與構(gòu)造函數(shù)初始化列表 \colorbox{#FFEAEA}{\color{#C22828}\text{初始化列表}}初始化列表?在類的定義中所有成員變量僅僅是聲明而真正為所有成員變量分配內(nèi)存空間是在初始化列表中構(gòu)造函數(shù)是為被定義了的成員變量進(jìn)行初始化即所有成員變量都會進(jìn)入初始化列表中被定義。public:Data(intyear,intmonth,intday):_year(year)//??????此處被正式定義,_month(month),_day(day),_a((int*)malloc(12){}private:int_year;//??????這里僅僅是聲明還未被定義int_month;int_day;int*_a;初始化列表中初始化值可以是表達(dá)式也可以是單一變量初始化列表和構(gòu)造函數(shù)的區(qū)別 \colorbox{#FFEAEA}{\color{#C22828}\text{初始化列表和構(gòu)造函數(shù)的區(qū)別}}初始化列表和構(gòu)造函數(shù)的區(qū)別?都具有初始化的功能因?yàn)槌蓡T變量都要進(jìn)入初始化列表被定義所以本質(zhì)上構(gòu)造函數(shù)花括號內(nèi)的初始化是二次賦值覆蓋初始化列表中同一成員變量只能出現(xiàn)一次變量不可重定義構(gòu)造函數(shù)中初始化可以多次出現(xiàn)覆蓋賦值核心避坑: \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑: }}核心避坑:?普通成員變量的初始化兩者都可以做到無差別但是三種特例無法只能使用初始化列表來完成初始化。const修飾被const修飾的成員變量具有常屬性只有在定義時能被初始化所以必須使用初始化列表引用變量引用變量必須在定義時就與被引用變量綁定必須初始化后續(xù)不可以改變綁定關(guān)系自定義類型無默認(rèn)構(gòu)造函數(shù)成員變量中如果存在自定義類型且給只顯式定義了帶參構(gòu)造函數(shù)編譯器不在生成無默認(rèn)構(gòu)造函數(shù)此時需要初始化列表來給自定義類型成員傳入構(gòu)造函數(shù)形參classTime{public:Time(inttime){_timetime;}private:int_time;};//利用初始化列表為自定義類型成員傳入?yún)?shù)Data(intyear,intmonth,intday):_year(year),_month(month),_day(day),_t(1)//?{}?? 注意:能使用初始化列表盡量都使用構(gòu)造函數(shù)會存在兩次操作定義 二次賦值初始化列表只有一次定義在實(shí)際使用中初始化列表中與構(gòu)造函數(shù)相輔相成并不存在對立關(guān)系初始化列表只能完成簡單賦值綁定但是復(fù)雜對象的初始化還需要判斷及校驗(yàn)C11改變 \colorbox{#FFEAEA}{\color{#C22828}\text{C11改變}}C11改變?C98中規(guī)定聲明的成員變量不可以給值但是C11中可以提供缺省值不在初始化列表中初始化的成員使用缺省值初始化??偨Y(jié) \colorbox{#FFEAEA}{\color{#C22828}\text{總結(jié)}}總結(jié)?1.所有成員變量都會進(jìn)入初始化列表2.在初始化列表中初始化的成員直接使用傳入形參3.不在初始化列表中初始化的成員a.在聲明位置有缺省值使用缺省值b.沒有缺省值x.內(nèi)置類型成員編譯器不做處理隨機(jī)值y.自定義類型成員自動調(diào)用其默認(rèn)構(gòu)造函數(shù)無默認(rèn)構(gòu)造函數(shù)則報錯三.析構(gòu)函數(shù)功能定位析構(gòu)函數(shù)類似于C語言中的Destroy函數(shù)特性沒有返回值函數(shù)名也為類名對象生命周期結(jié)束時自動調(diào)用不支持重載3.1析構(gòu)函數(shù)概念及定義//析構(gòu)函數(shù)classStack{public://構(gòu)造函數(shù)Stack(intn4){_a(int*)malloc(sizeof(int)*n);_top0;_capacityn;}//析構(gòu)函數(shù)~Stack(){free(_a);//?將申請的空間資源進(jìn)行釋放_anullptr;}private:int*_a;int_top;int_capacity;};在對象生命周期結(jié)束前return 前編譯器自動調(diào)用析構(gòu)函數(shù)完成已申請的資源釋放但并非銷毀對象本身析構(gòu)函數(shù)有且僅有一個所以不能重載 \colorbox{#FFEAEA}{\color{#C22828}\text{析構(gòu)函數(shù)有且僅有一個所以不能重載}}析構(gòu)函數(shù)有且僅有一個所以不能重載?3.2內(nèi)置類型與動態(tài)資源的析構(gòu)無資源申請時析構(gòu)函數(shù)僅對有資源申請的類進(jìn)行操作對于單純的內(nèi)置類型不做處理函數(shù)調(diào)用結(jié)束自動釋放有資源申請時通過malloc或new動態(tài)申請內(nèi)存的前提下如果不顯式實(shí)現(xiàn)析構(gòu)函數(shù)編譯器雖然會自動生成析構(gòu)函數(shù)但是無法對mlloc或new申請的堆區(qū)空間進(jìn)行釋放會造成內(nèi)存泄漏。注意一個類實(shí)例化出多個對象時析構(gòu)滿足棧的性質(zhì)先進(jìn)后出所以先構(gòu)造的后析構(gòu)、后構(gòu)造的先析構(gòu) \colorbox{#FFEAEA}{\color{#C22828}\text{注意一個類實(shí)例化出多個對象時析構(gòu)滿足棧的性質(zhì)先進(jìn)后出所以先構(gòu)造的后析構(gòu)、后構(gòu)造的先析構(gòu)}}注意一個類實(shí)例化出多個對象時析構(gòu)滿足棧的性質(zhì)先進(jìn)后出所以先構(gòu)造的后析構(gòu)、后構(gòu)造的先析構(gòu)?四.拷貝構(gòu)造函數(shù)函數(shù)特性無返回值函數(shù)名與類名一致參數(shù)必須是傳引用傳值功能定位完成自定類型的拷貝4.1拷貝構(gòu)造函數(shù)的定義定義用已經(jīng)實(shí)例化出來的對象去初始化另一個正在實(shí)例化的對象拷貝構(gòu)造函數(shù)僅針對于類類型成員所以第一個參數(shù)為類類型并且如果有其他額外參數(shù)都需要有默認(rèn)值即缺省值classData{public://構(gòu)造函數(shù)Data(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}//拷貝構(gòu)造函數(shù)Data(constDatad)//思考為什么需要引用傳參{_yeard._year;_monthd._month;_dayd._day;}private:int_year;int_month;int_day;};?? 注意拷貝構(gòu)造函數(shù)的形參必須是傳引用傳參而不能是傳值傳參否則會造成死循環(huán) 核心避坑: \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑: }}核心避坑:?如果形參為傳值傳參則外部對象需先拷貝給形參d此時d相當(dāng)于實(shí)例化出來的另一個對象觸發(fā)拷貝構(gòu)造函數(shù)再次進(jìn)入形參d的拷貝又一次調(diào)用拷貝構(gòu)造函數(shù)無休止進(jìn)行。傳引用傳參不會造成拷貝所以可以直接進(jìn)入拷貝構(gòu)造函數(shù)內(nèi)進(jìn)行拷貝傳引用傳參本質(zhì)是為了減少拷貝次數(shù)但是如果作為返回值返回一定要確定變量是否還存在若出作用域被銷毀那么就相當(dāng)于野引用傳出值不可預(yù)測4.2深拷貝與淺拷貝編譯器默認(rèn)生成的淺拷貝值拷貝有相應(yīng)指針指向的空間也是直接拷貝會造成拷貝出的對象與被拷貝對象指向同一塊內(nèi)存深拷貝則是在面對存在有指針指向資源的情況時為拷貝出的函數(shù)重新創(chuàng)建一個指向空間而非與被拷貝空間指向同一塊地址。經(jīng)典案例棧 \colorbox{#FFEAEA}{\color{#C22828}\text{經(jīng)典案例棧}}經(jīng)典案例棧?//拷貝構(gòu)造函數(shù)Stack(constStacks){_a(int*)malloc(sizeof(int)*s._capacity);//淺拷貝只能值拷貝無法完成對象的指向關(guān)系_tops._top;_capacitys._capacity;//只需拷貝有效數(shù)據(jù)個數(shù)拷貝整個capacity會有空間還未利用memcpy(_a,s._a,sizeof(int)*s._top);}未顯式實(shí)現(xiàn)拷貝構(gòu)造函數(shù) \colorbox{#FFEAEA}{\color{#C22828}\text{未顯式實(shí)現(xiàn)拷貝構(gòu)造函數(shù)}}未顯式實(shí)現(xiàn)拷貝構(gòu)造函數(shù)?intmain(){Stack s1;Stack s2s1;//或者Stack s2 (s1);couts1._aendl;couts2._aendl;return0;}從圖中可以看出如果不顯式實(shí)現(xiàn)拷貝構(gòu)造函數(shù)那么s1中的_a和s2中的_a所指向的是同一塊空間地址相同。為什么會產(chǎn)生崩潰顯現(xiàn)呢核心避坑: \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑: }}核心避坑:?未顯式定義拷貝構(gòu)造函數(shù)類內(nèi)成員變量指向同一塊空間在程序結(jié)束后自動調(diào)用析構(gòu)函數(shù)會對同一塊空間進(jìn)行兩次資源釋放Double free五.賦值運(yùn)算符重載5.1運(yùn)算符重載operator函數(shù)特性有返回值函數(shù)名為operator加運(yùn)算符如void operator( ) 不能憑空創(chuàng)造運(yùn)算符等.* :: sizeof ?: .五大運(yùn)算符不可以重載運(yùn)算符操作對象最少有一個類類型內(nèi)置類型無法進(jìn)行運(yùn)算符重載按照正常邏輯執(zhí)行重載 或 --運(yùn)算符時為了區(qū)分前置或后置定義重載函數(shù)時給后置參數(shù)多一個int變量(值無所謂只是為了區(qū)分)如operator()前置、operator(int)后置重載流運(yùn)算符 或時為了符合習(xí)慣通常定義在類外否則寫法為對象cout(不符合我們平時的習(xí)慣)核心避坑 \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 }}核心避坑?如果運(yùn)算符重載的操作對象為兩個并且重載定義在類內(nèi)則只需要傳入第二個參數(shù)第一個為隱式的this若定義在類外則需要傳入兩個參數(shù)兩個操作對象的運(yùn)算符參數(shù)的傳遞是左操作對象傳第一個參數(shù)右操作對象傳第二個參數(shù)樣例示范 \colorbox{#FFEAEA}{\color{#C22828}\text{樣例示范}}樣例示范?通過重載運(yùn)算符來判斷兩個對象的日期是否相同當(dāng)然也可以實(shí)現(xiàn)大小比較日期相減減等操作流輸出、流提取運(yùn)算符重載 \colorbox{#FFEAEA}{\color{#C22828}\text{流輸出、流提取運(yùn)算符重載}}流輸出、流提取運(yùn)算符重載?在類內(nèi)定義時第一個參數(shù)隱式傳遞this指針?biāo)灾恍枰獋魅雘stream類型參數(shù)vs默認(rèn)做操作對象傳第一個參數(shù)第二個操作對象傳入第二個參數(shù)voidoperator(/*第一參數(shù)默認(rèn)傳入this指針*/ostreamout){out_yearendl;}Datad1(2026,8,25);d1cout;//?為滿足對象傳第一this參數(shù)右對象傳第二參數(shù)在類外定義類外可以自定義傳入?yún)?shù)順序則可以符合使用習(xí)慣將cout放在操作符前voidoperator(constDatad,ostreamout){outd._yearendl;}Datad1(2026,8,25);coutd1;5.2賦值運(yùn)算符重載上面講的運(yùn)算符重載是我們對于一個操作符進(jìn)行的自定義行為而對于賦值運(yùn)算符的重載是真正的默認(rèn)成員函數(shù)功能 \colorbox{#FFEAEA}{\color{#C22828}\text{功能}}功能?用一個已經(jīng)實(shí)例化成功的對象去賦值另一個已經(jīng)實(shí)例化的對象操作對象為兩個已經(jīng)實(shí)例化的對象。核心避坑 ?? \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 ??}}核心避坑???拷貝構(gòu)造函數(shù)是用已經(jīng)實(shí)例化的對象去初始化正在實(shí)例化的對象操作對象是一個實(shí)例化完成的對象一個正在實(shí)例化的對象樣例棧 \colorbox{#FFEAEA}{\color{#C22828}\text{樣例棧}}樣例棧?voidoperator(constStackst){_a(int*)malloc(sizeof(int)*st._capacity);_topst._top;_capacityst._capacity;memcpy(_a,st._a,sizeof(int)*st._top);}如果不顯示實(shí)現(xiàn)賦值運(yùn)算符重載那么有資源申請的請款下同一塊內(nèi)存將會被兩個對象同時指向。賦值運(yùn)算符重載還支持連續(xù)賦值ru(st3 st2 st1),表達(dá)式st2 st1 賦值完成后左值st2作為返回值返回所以在賦值運(yùn)算符重載中可以將this指針解引用返回小技巧 \colorbox{#FFEAEA}{\color{#C22828}\text{小技巧}}小技巧?有析構(gòu)函數(shù)時大概率會使用拷貝構(gòu)造函數(shù)和賦值重載六.取地址運(yùn)算符重載6.1const修飾對象classData{public:Data(intyear):_year(year){}voidpinrt()const//??????const放在函數(shù)尾代表傳入?yún)?shù)為const類型{cout_yearendl;}private:int_year;};//實(shí)例化一個const對象constd1(2026);d1.print();默認(rèn)隱式傳入的this指針為Data* this,而對象被const修飾后相對應(yīng)的指針類型為const Data* 這里存在權(quán)限放大問題。解決方法: \colorbox{#FFEAEA}{\color{#C22828}\text{解決方法:}}解決方法:?C規(guī)定不可以顯式提供this指針?biāo)詿o法在括號內(nèi)完成const修飾所以將const放在函數(shù)參數(shù)列表尾6.2取地址重載提醒 \colorbox{#E53935}{\color{#FFFFFF}\text{ 提醒}}提醒?編譯器默認(rèn)生成的取地址重載會直接返回對象的實(shí)際內(nèi)存地址即this指針99%情況下無需顯式實(shí)現(xiàn)。特例 \colorbox{#FFEAEA}{\color{#C22828}\text{特例}}特例?想讓其返回nullptr或者假地址時才需要顯式實(shí)現(xiàn)Data*operator(){returnnullptr;}“保持熱愛奔赴山海保持專注雕琢底層。”Crafted with passion byHhy當(dāng)前更新時間{{ 2026 / 8 / 25 }} | 此博文系博主學(xué)習(xí)復(fù)盤歡迎指正本文專欄由 Hhy 維護(hù)。如果這篇文章曾帶給你啟發(fā)請考慮關(guān)注我的 CSDN 博客或 GitHub 倉庫以獲取最新技術(shù)分享。