 六大默認(rèn)成員函數(shù))
前言在 C 面向?qū)ο缶幊讨小邦惻c對象” 是核心基石而默認(rèn)成員函數(shù)則是編譯器為類自動(dòng)生成的 “隱藏助手”—— 即使你沒顯式寫它們也在默默工作。本文將從實(shí)際開發(fā)角度拆解類的 6 個(gè)默認(rèn)成員函數(shù)幫你搞懂 “為什么需要它們”“怎么用” 以及 “踩坑點(diǎn)在哪”。同時(shí)友情提示一下這一篇的深拷貝可以先不管我是學(xué)完后面的回來寫博客所以隨便實(shí)現(xiàn)了一下我在 STL 的底層實(shí)現(xiàn)的時(shí)候會(huì)具體細(xì)致地講。一、什么是默認(rèn)成員函數(shù)如果一個(gè)類中什么成員都沒寫空類編譯器會(huì)自動(dòng)生成 6 個(gè)默認(rèn)成員函數(shù)。它們的核心作用是處理對象的 “初始化、清理、拷貝、賦值、取地址” 等基礎(chǔ)操作??疹恈lass Date {};看似空實(shí)則包含以下 6 個(gè)默認(rèn)成員函數(shù)函數(shù)類型核心作用顯式實(shí)現(xiàn)場景構(gòu)造函數(shù)對象創(chuàng)建時(shí)初始化需自定義初始化邏輯如日期析構(gòu)函數(shù)對象銷毀時(shí)清理資源類內(nèi)有動(dòng)態(tài)內(nèi)存如棧、鏈表拷貝構(gòu)造函數(shù)用已有對象創(chuàng)建新對象Date d2(d1)涉及資源拷貝避免淺拷貝賦值運(yùn)算符重載對象間賦值d2 d1涉及資源賦值避免淺拷貝普通取地址重載獲取普通對象地址幾乎不用默認(rèn)實(shí)現(xiàn)足夠const取地址重載獲取const對象地址幾乎不用默認(rèn)實(shí)現(xiàn)足夠需要注意的是這 6 個(gè)中最重要的是前 4 個(gè)。最后兩個(gè)取地址重載不重要我們稍微了解一下即可。其次就是 C11 以后還會(huì)增加兩個(gè)默認(rèn)成員函數(shù)移動(dòng)構(gòu)造和移動(dòng)賦值這個(gè)我們后面再講解。默認(rèn)成員函數(shù)很重要也比較復(fù)雜我們要從兩個(gè)方面去學(xué)習(xí)第一我們不寫時(shí)編譯器默認(rèn)生成的函數(shù)行為是什么是否滿足我們的需求。第二編譯器默認(rèn)生成的函數(shù)不滿足我們的需求我們需要自己實(shí)現(xiàn)那么如何自己實(shí)現(xiàn)二、構(gòu)造函數(shù)2.1 構(gòu)造函數(shù)的定義每次創(chuàng)建對象時(shí)編譯器都會(huì)自動(dòng)調(diào)用一次構(gòu)造函數(shù) —— 它的核心不是 “創(chuàng)建對象”開空間由編譯器負(fù)責(zé)而是 “給對象的成員變量賦初始值”避免成員變量出現(xiàn)隨機(jī)值。構(gòu)造函數(shù)的本質(zhì)是要替代我們以前寫的Init函數(shù)的功能構(gòu)造函數(shù)自動(dòng)調(diào)用的特點(diǎn)就完美的替代了Init。2.2 構(gòu)造函數(shù)特性與創(chuàng)建函數(shù)名與類名相同。無返回值。返回值啥都不需要給也不需要寫void不要糾結(jié)C 規(guī)定如此構(gòu)造函數(shù)可以重載無參、帶參、全缺省都可。classDate{public:// 1. 無參構(gòu)造編譯器默認(rèn)生成的就是類似無參版本Date(){_year1900;_month1;_day1;}// 2. 帶參構(gòu)造重載版本支持自定義初始化Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};// 使用示例intmain(){Date d1;// d1實(shí)例化時(shí)自動(dòng)調(diào)用無參構(gòu)造Dated2(2024,5,20);// 調(diào)用帶參構(gòu)造// Date d3(); // 錯(cuò)誤這是聲明函數(shù)不是創(chuàng)建對象return0;}寫到這里我們已經(jīng)發(fā)現(xiàn)構(gòu)造函數(shù)的優(yōu)勢了但是還可以結(jié)合我們的缺省值這個(gè)知識(shí)點(diǎn)我們可以只寫一個(gè)來替代我們剛剛寫的那兩個(gè)構(gòu)造函數(shù)請看全缺省構(gòu)造函數(shù)Date(intyear2025,intmonth2,intday20){_yearyear;_monthmonth;_dayday;}瞬間減少了我們的代碼量。但是這個(gè)全缺省的構(gòu)造函數(shù)和無參的構(gòu)造函數(shù)不能同時(shí)出現(xiàn)因?yàn)檫@樣編譯器就不知道調(diào)用哪一個(gè)了。2.3 編譯器默認(rèn)生成的構(gòu)造函數(shù)如果類中沒有顯式定義構(gòu)造函數(shù)則 C 編譯器會(huì)自動(dòng)生成一個(gè)無參的默認(rèn)構(gòu)造函數(shù)一旦用戶顯式定義編譯器將不再生成。我們先來看看編譯器生成的classDate{public:voidprint(){cout_year-_month-_dayendl;}private:int_year;int_month;int_day;};intmain(){Date d1;// d1實(shí)例化時(shí)自動(dòng)調(diào)用默認(rèn)生成的無參構(gòu)造d1.print();return0;}我們發(fā)現(xiàn)好像編譯器生成的默認(rèn)構(gòu)造函數(shù)好像啥也沒干還是隨機(jī)值那它有啥用別急我們繼續(xù)看classTime{public:// Time的默認(rèn)構(gòu)造Time(){coutTime()被調(diào)用endl;_hour0;_minute0;_second0;}private:int_hour,_minute,_second;};classDate{public:voidprint(){cout_year-_month-_dayendl;}private:int_year;int_month;int_day;Time _t;};intmain(){Date d1;// d1實(shí)例化時(shí)自動(dòng)調(diào)用默認(rèn)生成的無參構(gòu)造d1.print();return0;}我們發(fā)現(xiàn)系統(tǒng)生成的默認(rèn)構(gòu)造函數(shù)會(huì)調(diào)用 “自定義類型成員”Time的默認(rèn)構(gòu)造函數(shù)。也就是它沒給內(nèi)置類型int/char等賦初始值但是會(huì)調(diào)用 “自定義類型成員” 的默認(rèn)構(gòu)造函數(shù)。注意再一次強(qiáng)調(diào)無參構(gòu)造、全缺省構(gòu)造、編譯器默認(rèn)生成的構(gòu)造函數(shù)都屬于 “默認(rèn)構(gòu)造函數(shù)”—— 它們的共同點(diǎn)是 “調(diào)用時(shí)不用傳參”。一個(gè)類中只能有一個(gè)默認(rèn)構(gòu)造函數(shù)否則會(huì)沖突三、析構(gòu)函數(shù)3.1 析構(gòu)函數(shù)的定義通過前面構(gòu)造函數(shù)的學(xué)習(xí)我們知道一個(gè)對象是怎么來的那一個(gè)對象又是怎么沒的呢析構(gòu)函數(shù)與構(gòu)造函數(shù)功能相反析構(gòu)函數(shù)不是完成對對象本身的銷毀C 規(guī)定對象在銷毀時(shí)生命周期結(jié)束會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)完成對象中資源的清理釋放工作。析構(gòu)函數(shù)的功能類比我們之前的Destroy功能。為什么析構(gòu)函數(shù)不是完成對對象本身的銷毀因?yàn)閷ο笫谴嬖跅挥泻瘮?shù)結(jié)束棧幀銷毀并釋放3.2 析構(gòu)函數(shù)特性與創(chuàng)建函數(shù)名 ~有點(diǎn)像取反的意思類名無參數(shù)、無返回值一個(gè)類只能有一個(gè)析構(gòu)函數(shù)不能重載若未顯式實(shí)現(xiàn)編譯器會(huì)生成默認(rèn)析構(gòu)函數(shù)。3.3 編譯器默認(rèn)析構(gòu)函數(shù)和默認(rèn)構(gòu)造類似默認(rèn)析構(gòu)函數(shù)會(huì)調(diào)用 “自定義類型成員” 的析構(gòu)函數(shù)而內(nèi)置類型成員無需清理內(nèi)存由編譯器回收classTime{public:~Time(){cout~Time()被調(diào)用endl;}private:int_hour,_minute,_second;};classDate{private:int_year1970;// 內(nèi)置類型默認(rèn)析構(gòu)不處理Time _t;// 自定義類型默認(rèn)析構(gòu)調(diào)用~Time()};intmain(){Date d;// 程序結(jié)束時(shí)輸出~Time()被調(diào)用return0;}3.4 什么時(shí)候要顯式寫析構(gòu)函數(shù)沒有空間申請的類不需要析構(gòu)函數(shù)比如只有內(nèi)置類型的因?yàn)樗鼈儧]有在堆上開辟空間都是在棧上開空間出來它們出了函數(shù)的作用域會(huì)自動(dòng)銷毀把空間還給操作系統(tǒng)。日期類classDate{public://…………………private:int_year;int_month;int_day;};日期類都是內(nèi)置類型我們就可以不用寫棧類的析構(gòu)函數(shù)typedefintDataType;classStack{public:// 構(gòu)造函數(shù)申請動(dòng)態(tài)內(nèi)存Stack(size_t capacity4){_array(DataType*)malloc(sizeof(DataType)*capacity);if(_arraynullptr){perror(malloc失敗);return;}_capacitycapacity;_size0;}// 析構(gòu)函數(shù)釋放動(dòng)態(tài)內(nèi)存必須顯式寫~Stack(){if(_array!nullptr){free(_array);// 釋放內(nèi)存_arraynullptr;// 避免野指針_capacity0;_size0;}}private:DataType*_array;// 動(dòng)態(tài)內(nèi)存指針size_t _capacity;size_t _size;};// 使用示例voidTestStack(){Stack s;// 創(chuàng)建對象調(diào)用構(gòu)造s.Push(1);s.Push(2);}// s出作用域自動(dòng)調(diào)用析構(gòu)釋放內(nèi)存因?yàn)橄駰_@種它有動(dòng)態(tài)開辟內(nèi)存在堆上的系統(tǒng)生成的它不會(huì)去清理堆上的就不夠用了我們要顯式寫析構(gòu)函數(shù)。3.5 總結(jié)對象生命周期結(jié)束系統(tǒng)會(huì)自動(dòng)調(diào)用默認(rèn)的析構(gòu)函數(shù)。有資源申請的類需要寫析構(gòu)函數(shù)。有多個(gè)對象需要析構(gòu)的時(shí)候后創(chuàng)建的先析構(gòu)。因?yàn)闂筮M(jìn)先出。四、拷貝構(gòu)造函數(shù)4.1 拷貝構(gòu)造函數(shù)的定義用一個(gè)已存在的對象創(chuàng)建新對象時(shí)如Date d2(d1)編譯器會(huì)自動(dòng)調(diào)用拷貝構(gòu)造函數(shù)。也就是說拷貝構(gòu)造是一個(gè)特殊的構(gòu)造函數(shù)。它的核心是 “復(fù)制對象的成員變量”但要注意 “淺拷貝” 的坑。4.2 拷貝構(gòu)造函數(shù)特性與創(chuàng)建是構(gòu)造函數(shù)的重載形式函數(shù)名 類名參數(shù)必須是 “類類型的引用”Date d若未顯式實(shí)現(xiàn)編譯器會(huì)生成默認(rèn)拷貝構(gòu)造淺拷貝。引用前面最好加上const限制。保護(hù)用來拷貝的那個(gè)數(shù)據(jù)Date(constDated){_yeard._year;_monthd._month;_dayd._day;}采用const引用的原因可以保護(hù)形式參數(shù)不被改變。傳值傳參拷貝構(gòu)造函數(shù)只是一個(gè)中轉(zhuǎn)。我不希望它在函數(shù)體內(nèi)對我的原來的對象造成修改。為什么參數(shù)必須是引用如果參數(shù)是傳值Date(const Date d)調(diào)用拷貝構(gòu)造時(shí)會(huì)先復(fù)制實(shí)參到形參 —— 而復(fù)制形參又需要調(diào)用拷貝構(gòu)造簡而言之傳參會(huì)調(diào)用拷貝構(gòu)造調(diào)用拷貝構(gòu)造又要調(diào)用拷貝構(gòu)造形成 “無窮遞歸”具體可以看下面的圖classDate{public:// 正確寫法參數(shù)是const引用const避免修改原對象引用避免遞歸Date(constDated){_yeard._year;_monthd._month;_dayd._day;}Date(intyear0,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}// 錯(cuò)誤寫法參數(shù)傳值引發(fā)無窮遞歸// Date(const Date d) {}private:int_year,_month,_day;};intmain(){Date d1;Dated2(d1);return0;}調(diào)用拷貝構(gòu)造函數(shù)的另外一種方法第二種調(diào)用方法實(shí)質(zhì)上等價(jià)于第一種。但是可讀性更高Date d1;Date d2d1;4.3 編譯器默認(rèn)生成拷貝構(gòu)造函數(shù)與構(gòu)造函數(shù)和析構(gòu)函數(shù)不同自動(dòng)生成的拷貝構(gòu)造函數(shù)會(huì)管內(nèi)置類型。若未顯式定義拷貝構(gòu)造編譯器會(huì)自動(dòng)生成拷貝構(gòu)造函數(shù)。自動(dòng)生成的拷貝構(gòu)造對內(nèi)置類型成員變量會(huì)完成值拷貝 / 淺拷貝對自定義類型成員變量會(huì)調(diào)用它的拷貝構(gòu)造。我們發(fā)現(xiàn)自己不寫也會(huì)完成因?yàn)榫幾g器生成的會(huì)按照字節(jié)進(jìn)行拷貝。情況一類成員均為內(nèi)置類型且不管理資源例如Date類。編譯器生成的拷貝構(gòu)造函數(shù)會(huì)完成值拷貝因此不需要我們顯式實(shí)現(xiàn)拷貝構(gòu)造。情況二類成員包含自定義類型且該類型已正確實(shí)現(xiàn)拷貝構(gòu)造函數(shù)例如Myqueue類其內(nèi)部成員主要是自定義類型Stack的對象。編譯器自動(dòng)生成的拷貝構(gòu)造函數(shù)會(huì)調(diào)用Stack的拷貝構(gòu)造函數(shù)因此也不需要我們顯式實(shí)現(xiàn)Myqueue的拷貝構(gòu)造。情況三類管理動(dòng)態(tài)資源例如Stack類其成員_a指向了資源。編譯器自動(dòng)生成的拷貝構(gòu)造函數(shù)僅完成值拷貝 / 淺拷貝僅復(fù)制指針值并不符合我們的需求因此必須手動(dòng)實(shí)現(xiàn)深拷貝即對指針?biāo)赶虻馁Y源也進(jìn)行完整拷貝。4.4 淺拷貝的坑什么時(shí)候必須顯式寫拷貝構(gòu)造像上面的情況三就要自己寫了系統(tǒng)生成的無法完成。我們先看為什么解決辦法深拷貝深拷貝的核心是 “給新對象重新申請資源”避免指針指向同一塊內(nèi)存classStack{public:// 深拷貝構(gòu)造Stack(constStacks){// 給新對象重新申請內(nèi)存_array(DataType*)malloc(sizeof(DataType)*s._capacity);if(_arraynullptr){perror(malloc失敗);return;}// 復(fù)制數(shù)據(jù)不是復(fù)制指針memcpy(_array,s._array,sizeof(DataType)*s._size);_capacitys._capacity;_sizes._size;}// 構(gòu)造、析構(gòu)同上...};五、賦值運(yùn)算符重載5.1 運(yùn)算符重載比如我們?nèi)绻容^日期類對象的大小按照我們現(xiàn)有的知識(shí)要去寫一個(gè)函數(shù)但是我們?nèi)绻胫苯颖容^呢d1d2所以有了運(yùn)算符重載。在 C 中運(yùn)算符重載是一項(xiàng)強(qiáng)大的特性它允許我們?yōu)樽远x類型賦予內(nèi)置類型般的操作體驗(yàn)極大提升了代碼的可讀性與易用性。函數(shù)名字為關(guān)鍵字operator后面接需要重載的運(yùn)算符符號(hào)。函數(shù)原型返回值類型 operator 操作符(參數(shù)列表)Dateoperator(constDated)不能通過連接其他符號(hào)來創(chuàng)建新的操作符比如operator重載操作符必須有一個(gè)類類型參數(shù)用于內(nèi)置類型的運(yùn)算符其含義不能改變例如內(nèi)置的整型 不能改變其含義作為類成員函數(shù)重載時(shí)其形參看起來比操作數(shù)數(shù)目少 1因?yàn)槌蓡T函數(shù)的第一個(gè)參數(shù)為隱藏的this.*、::、sizeof、?:、.注意以上 5 個(gè)運(yùn)算符不能重載。5.2 賦值運(yùn)算符重載的特性與創(chuàng)建5.2.1 賦值運(yùn)算符特性與創(chuàng)建返回值類型 operator(參數(shù)列表)參數(shù)類型為const 類類型引用提高效率const避免修改原對象返回值類型為類類型支持連續(xù)賦值如d3 d2 d1必須顯式檢測 “自己給自己賦值”如d1 d1避免無意義操作。classDate{public:Date(intyear1900,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}// 賦值運(yùn)算符重載Dateoperator(constDated){// 1. 檢測自己給自己賦值if(this!d){// 2. 復(fù)制成員變量_yeard._year;_monthd._month;_dayd._day;}// 3. 返回*this支持連續(xù)賦值return*this;//返回d2}private:int_year,_month,_day;};// 使用示例intmain(){Dated1(2024,5,20);Date d2,d3;d3d2d1;// 連續(xù)賦值等價(jià)于d3.operator(d2.operator(d1))return0;}5.2.2 全局函數(shù) vs 成員函數(shù)運(yùn)算符重載可以通過全局函數(shù)或類的成員函數(shù)實(shí)現(xiàn)兩者的核心區(qū)別在于參數(shù)列表的隱含性。若將operator實(shí)現(xiàn)為全局函數(shù)需要顯式接收兩個(gè)Date對象作為參數(shù)【全局函數(shù)實(shí)現(xiàn)】classDate{public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}// 成員變量需公開或用后面友元解決封裝性問題int_year,_month,_day;};// 全局函數(shù)重載 operatorbooloperator(constDated1,constDated2){returnd1._yeard2._yeard1._monthd2._monthd1._dayd2._day;}voidTest(){Dated1(2024,10,1);Dated2(2024,10,2);cout(d1d2)endl;// 調(diào)用全局 operator}缺點(diǎn)若類的成員變量是私有的全局函數(shù)無法直接訪問需通過 “友元” 聲明打破封裝或直接將重載函數(shù)寫為成員函數(shù)?!境蓡T函數(shù)實(shí)現(xiàn)】若將operator實(shí)現(xiàn)為類的成員函數(shù)編譯器會(huì)自動(dòng)將左操作數(shù)作為隱含的this指針因此參數(shù)列表只需顯式接收右操作數(shù)classDate{public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}// 成員函數(shù)重載 operator左操作數(shù)是 this 指向的對象booloperator(constDated2)const{return_yeard2._year_monthd2._month_dayd2._day;}private:int_year,_month,_day;// 成員變量可私有保證封裝性};voidTest(){Dated1(2024,10,1);Dated2(2024,10,1);cout(d1d2)endl;// 調(diào)用成員函數(shù) operator}優(yōu)點(diǎn)無需破壞封裝性且邏輯更直觀左操作數(shù)的屬性可直接訪問。5.2.3 避坑點(diǎn)賦值運(yùn)算符只能是類的成員函數(shù)不能把賦值運(yùn)算符重載成全局函數(shù) —— 因?yàn)榫幾g器會(huì)默認(rèn)生成類內(nèi)的賦值運(yùn)算符重載全局版本會(huì)和它沖突5.3 編譯器默認(rèn)生成的賦值運(yùn)算符重載和拷貝構(gòu)造一樣編譯器默認(rèn)生成的賦值運(yùn)算符重載是 “淺拷貝”—— 有動(dòng)態(tài)內(nèi)存的類必須顯式實(shí)現(xiàn)深拷貝否則會(huì)重復(fù)釋放內(nèi)存// 棧類的深拷貝賦值運(yùn)算符重載classStack{public:Stackoperator(constStacks){if(this!s){// 檢測自賦值// 1. 釋放當(dāng)前對象的舊資源free(_array);// 2. 給當(dāng)前對象重新申請資源并復(fù)制數(shù)據(jù)_array(DataType*)malloc(sizeof(DataType)*s._capacity);if(_arraynullptr){perror(malloc失敗);return*this;}memcpy(_array,s._array,sizeof(DataType)*s._size);_capacitys._capacity;_sizes._size;}return*this;}// 構(gòu)造、析構(gòu)、拷貝構(gòu)造同上...};5.3.1 不需要手動(dòng)實(shí)現(xiàn)的情況情況一類成員均為內(nèi)置類型且不管理資源例如Date類。編譯器生成的賦值運(yùn)算符會(huì)進(jìn)行值拷貝。情況二類成員包含自定義類型且該類型已正確實(shí)現(xiàn)賦值運(yùn)算符例如Myqueue類其成員為Stack對象。編譯器會(huì)遞歸調(diào)用Stack的賦值運(yùn)算符只要Stack已正確實(shí)現(xiàn)深拷貝Myqueue就無需手動(dòng)實(shí)現(xiàn)。5.3.2 必須手動(dòng)實(shí)現(xiàn)的情況類管理動(dòng)態(tài)資源如堆內(nèi)存、文件句柄等例如Stack類其成員_a指向動(dòng)態(tài)數(shù)組。編譯器生成的賦值運(yùn)算符僅進(jìn)行淺拷貝復(fù)制指針值會(huì)導(dǎo)致內(nèi)存泄漏重復(fù)釋放懸空指針等一系列問題。六、其他 2 個(gè)默認(rèn)成員函數(shù)取地址重載取地址重載和const取地址重載編譯器默認(rèn)生成的實(shí)現(xiàn)完全滿足需求 —— 返回對象的地址this指針幾乎無需自定義。七、總結(jié)函數(shù)類型顯式實(shí)現(xiàn)場景核心坑點(diǎn)構(gòu)造函數(shù)需自定義初始化如日期默認(rèn)構(gòu)造只能有一個(gè)析構(gòu)函數(shù)類內(nèi)有動(dòng)態(tài)資源如棧忘記釋放資源導(dǎo)致內(nèi)存泄漏拷貝構(gòu)造函數(shù)類內(nèi)有動(dòng)態(tài)資源避免淺拷貝參數(shù)不用引用引發(fā)無窮遞歸賦值運(yùn)算符重載類內(nèi)有動(dòng)態(tài)資源避免淺拷貝忘記檢測自賦值、返回*this取地址重載特殊需求隱藏真實(shí)地址幾乎不用自定義