:從面向?qū)ο蠡A(chǔ)到核心成員函數(shù))
在C中類和對象是面向?qū)ο缶幊蘋OP的基石。本文將從面向?qū)ο笏枷胍搿㈩惖亩x與封裝一直深入到構(gòu)造函數(shù)、析構(gòu)函數(shù)、拷貝構(gòu)造以及運算符重載等核心機(jī)制。一、 面向過程 vs 面向?qū)ο驝語言面向過程關(guān)注的是過程分析出求解問題的步驟通過函數(shù)調(diào)用逐步解決問題。例如洗衣服 - 拿盆子 - 放水 - 放衣服 - 放洗衣粉 - 揉搓 - 擰干 - 晾曬C面向?qū)ο箨P(guān)注的是對象將一件事拆分成不同的對象靠對象之間的交互完成。例如人、衣服、洗衣機(jī)、洗衣粉四個對象人只需將衣服放進(jìn)洗衣機(jī)啟動按鈕即可不需關(guān)注洗衣機(jī)內(nèi)部如何運轉(zhuǎn)。二、 類的引入與定義C語言中結(jié)構(gòu)體只能定義變量而在C中結(jié)構(gòu)體struct和類class不僅可以定義變量還可以定義函數(shù)。1. 類的兩種定義方式方式一聲明和定義全部放在類體中這種方式比較直接適合代碼量較少的情況。需要注意的是如果成員函數(shù)在類體內(nèi)部定義編譯器可能會將其視為內(nèi)聯(lián)函數(shù)inline來處理。// 全部寫在類里面 class Person { public: // 成員函數(shù)在類內(nèi)部定義 void showInfo() { cout _name - _sex - _age endl; } private: char* _name; // 姓名 char* _sex; // 性別 int _age; // 年齡 };方式二聲明放在 .h 文件定義放在 .cpp 文件在實際工程開發(fā)中更推薦使用這種方式。這能讓代碼結(jié)構(gòu)更清晰也利于分離編譯。注意在.cpp文件中定義成員函數(shù)時函數(shù)名前必須加上類名::以指明該函數(shù)屬于哪個類域。---------- person.h (頭文件類聲明) ----------class Person { public: // 成員函數(shù)聲明 void showInfo(); private: char* _name; // 姓名 char* _sex; // 性別 int _age; // 年齡 };---------- person.cpp源文件類定義----------#include person.h // 成員函數(shù)定義必須加 Person:: void Person::showInfo() { cout _name - _sex - _age endl; }建議日常練習(xí)時為了方便可以使用方式一。但在實際工作項目中要使用方式二做到聲明與實現(xiàn)分離。2.struct和class的區(qū)別在C中struct和class都可以用來定義類區(qū)別在于默認(rèn)的訪問權(quán)限struct的默認(rèn)訪問權(quán)限是public公有。class的默認(rèn)訪問權(quán)限是private私有。三、 類的訪問限定符及封裝1. 訪問限定符限定符含義特點public公有類外可以直接訪問暴露給用戶的接口protected保護(hù)類外不能直接訪問繼承體系中會用到private私有類外不能直接訪問隱藏內(nèi)部實現(xiàn)細(xì)節(jié)2. 封裝封裝的核心目的隱藏實現(xiàn)細(xì)節(jié)只暴露操作接口。將數(shù)據(jù)和操作數(shù)據(jù)的方法進(jìn)行有機(jī)結(jié)合通過訪問權(quán)限來隱藏對象的屬性和實現(xiàn)細(xì)節(jié)僅對外公開接口來和對象進(jìn)行交互。就像電腦一樣用戶只需要知道如何開機(jī)、使用鍵盤鼠標(biāo)而不需要關(guān)心CPU和顯卡的內(nèi)部工作原理。四、 類的作用域、實例化與對象大小1. 類的作用域類定義了一個新的作用域類的所有成員都在類的作用域中。在類體外定義成員時需要使用::作用域操作符指明成員屬于哪個類域。2. 類的實例化類是設(shè)計圖只定義有哪些東西不占用實際物理內(nèi)存。對象是房子根據(jù)設(shè)計圖建造出來的實體占用物理內(nèi)存。注意不能直接訪問類中的成員變量必須通過實例化后的對象來訪問。3. 對象大小的計算核心規(guī)則對象中只存儲成員變量不存儲成員函數(shù)。成員函數(shù)存放在代碼段屬于同類所有對象共享。內(nèi)存對齊規(guī)則同結(jié)構(gòu)體第一個成員在與結(jié)構(gòu)體偏移量為0的地址處。其他成員變量要對齊到某個數(shù)字對齊數(shù)的整數(shù)倍地址處。對齊數(shù) 編譯器默認(rèn)的一個對齊數(shù)與該成員大小的較小值VS中默認(rèn)8Linux中默認(rèn)4。結(jié)構(gòu)體總大小為最大對齊數(shù)所有變量類型最大者與默認(rèn)對齊參數(shù)取最小的整數(shù)倍。如果嵌套了結(jié)構(gòu)體嵌套的結(jié)構(gòu)體對齊到自己的最大對齊數(shù)的整數(shù)倍處結(jié)構(gòu)體的整體大小就是所有最大對齊數(shù)含嵌套結(jié)構(gòu)體的對齊數(shù)的整數(shù)倍。特殊情況空類class A {}; // 空類 int main() { cout sizeof(A) endl; // 輸出 1 }為什么空類是1因為編譯器需要一個獨一無二的地址來標(biāo)識這個類的不同對象所以會分配1個字節(jié)作為占位符。五、 類成員函數(shù)的 this 指針1. this 指針的本質(zhì)this指針是隱藏的成員函數(shù)形參指向當(dāng)前調(diào)用該函數(shù)的對象。在 x86 平臺上this通常通過ecx寄存器傳遞在 x64 平臺上則作為第一個參數(shù)通過rcx寄存器傳遞。在類中定義成員函數(shù)時編譯器會自動加上this指針。例如void Print()實際上是void Print(Date* const this)。2. 經(jīng)典面試題空指針調(diào)用成員函數(shù)class A { public: void Print() { cout Print() endl; } void PrintA() { cout _a endl; } private: int _a; }; int main() { A* p nullptr; p-Print(); // 正常運行因為Print()函數(shù)沒有訪問成員變量不需要解引用this p-PrintA(); // 崩潰因為PrintA()訪問了_a本質(zhì)是 this-_a對空指針解引用導(dǎo)致崩潰 return 0; }3. const 成員函數(shù)將const修飾的成員函數(shù)稱之為const成員函數(shù)。const修飾類成員函數(shù)實際修飾的是該成員函數(shù)隱含的this指針表明在該成員函數(shù)中不能對類的任何成員進(jìn)行修改。權(quán)限問題const對象只能調(diào)用const成員函數(shù)非const對象既可以調(diào)用const成員函數(shù)也可以調(diào)用非const成員函數(shù)。權(quán)限放大權(quán)限可以縮小非const對象調(diào)用const函數(shù)但不能放大const對象調(diào)用非const函數(shù)。六、 類和對象中六大默認(rèn)成員函數(shù)如果一個類中什么成員都沒有簡稱為空類??疹愔惺裁炊紱]有嗎并不是的任何一個類在我們不寫的情況下都會自動生成下面6個默認(rèn)成員函數(shù)。1. 構(gòu)造函數(shù) (Constructor)主要任務(wù)初始化對象而不是開空間創(chuàng)建對象。特性說明函數(shù)名與類名相同返回值無返回值不需要寫void調(diào)用時機(jī)對象實例化時由編譯器自動調(diào)用重載可以重載默認(rèn)生成如果類中沒有顯式定義構(gòu)造函數(shù)編譯器會自動生成一個無參的默認(rèn)構(gòu)造函數(shù)。一旦用戶顯式定義編譯器將不再生成。C11 補(bǔ)丁針對編譯器默認(rèn)生成的構(gòu)造函數(shù)對內(nèi)置類型不做處理是隨機(jī)值的問題C11允許在成員聲明時給缺省值。class Date { private: int _year 1; // 聲明時給缺省值 int _month 1; int _day 1; };2. 析構(gòu)函數(shù) (Destructor)主要任務(wù)完成對象中資源的清理工作而不是銷毀對象本身對象銷毀由編譯器完成。特性說明函數(shù)名類名前加~參數(shù)與返回值無參數(shù)無返回值重載一個類只能有一個析構(gòu)函數(shù)不能重載調(diào)用時機(jī)對象生命周期結(jié)束時C編譯系統(tǒng)自動調(diào)用默認(rèn)生成若未顯式定義系統(tǒng)會自動生成默認(rèn)的析構(gòu)函數(shù)什么時候需要自己寫析構(gòu)函數(shù)有動態(tài)申請資源malloc/new需要顯式釋放資源。沒有動態(tài)申請資源不需要寫析構(gòu)。需要釋放資源的成員都是自定義類型不需要寫析構(gòu)會自動調(diào)用成員對象的析構(gòu)。3. 拷貝構(gòu)造函數(shù) (Copy Constructor)主要任務(wù)用已存在的對象創(chuàng)建新對象。特性說明函數(shù)名與類名相同參數(shù)只有一個參數(shù)且必須是類類型對象的引用一般用const修飾。使用傳值方式會引發(fā)無窮遞歸調(diào)用。調(diào)用場景使用已存在對象創(chuàng)建新對象函數(shù)參數(shù)類型為類類型對象函數(shù)返回值類型為類類型對象。淺拷貝 vs 深拷貝淺拷貝默認(rèn)生成內(nèi)置類型成員完成值拷貝自定義類型成員會調(diào)用他的拷貝構(gòu)造。對于指針成員只拷貝指針地址會導(dǎo)致兩個對象指向同一塊內(nèi)存二次析構(gòu)互相影響。深拷貝必須自己實現(xiàn)對于Stack這種有動態(tài)資源的類必須自己實現(xiàn)拷貝構(gòu)造重新分配內(nèi)存并拷貝數(shù)據(jù)。// 深拷貝示例 Stack(const Stack st) { _a (int*)malloc(sizeof(int) * st._capacity); if (nullptr _a) return; memcpy(_a, st._a, sizeof(int) * st._top); _top st._top; _capacity st._capacity; }4. 賦值運算符重載 (operator)主要任務(wù)兩個已經(jīng)存在的對象之間進(jìn)行復(fù)制拷貝。Date d4 d2;是拷貝構(gòu)造初始化。d4 d1;是賦值運算符重載已經(jīng)存在了。格式與要點Date operator(const Date d) { if (this ! d) { // 1. 檢測是否自己給自己賦值 _year d._year; _month d._month; _day d._day; } return *this; // 2. 返回*this支持連續(xù)賦值 (d5 d4 d1) }參數(shù)const T傳遞引用提高傳參效率。返回值T返回引用提高返回效率有返回值目的是為了支持連續(xù)賦值。注意賦值運算符只能重載成類的成員函數(shù)不能重載成全局函數(shù)。默認(rèn)生成的賦值重載行為跟拷貝構(gòu)造一樣淺拷貝有動態(tài)資源的類需要自己實現(xiàn)深拷貝。5. 取地址及 const 取地址操作符重載這兩個默認(rèn)成員函數(shù)一般不用重新定義編譯器默認(rèn)會生成。Date* operator() { return this; } const Date* operator() const { return this; }七、 運算符重載進(jìn)階1. 運算符重載基本規(guī)則不能通過連接其他符號來創(chuàng)建新的操作符如operator。重載操作符必須有一個類類型參數(shù)。用于內(nèi)置類型的運算符其含義不能改變。作為類成員函數(shù)重載時其形參看起來比操作數(shù)數(shù)目少1因為成員函數(shù)的第一個參數(shù)為隱藏的this。不能重載的運算符5個.*、::、sizeof、?:、.。補(bǔ)充operator、operator()、operator[]、operator-只能重載成類的成員函數(shù)不能重載成全局函數(shù)。2. 前置 與 后置 的重載為了區(qū)分前置和后置后置重載時增加了一個int參數(shù)但這個參數(shù)并不接收具體的值僅僅是占位跟前置構(gòu)成重載。// 前置 Date operator() { *this 1; return *this; } // 后置 Date operator(int) { Date tmp *this; *this 1; return tmp; }前置返回之后的對象引用。后置返回之前的對象值。3. 流插入與流提取的重載cout d1;會轉(zhuǎn)換為operator(cout, d1);不能寫成成員函數(shù)因為成員函數(shù)的第一個參數(shù)永遠(yuǎn)是this指針即d1.operator(cout)這不符合我們的使用習(xí)慣。必須寫成全局函數(shù)并在類中聲明為友元friend。class Date { // 友元函數(shù)聲明 friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); }; // 全局定義 ostream operator(ostream out, const Date d) { out d._year 年 d._month 月 d._day 日; return out; // 支持連續(xù)輸出 }八、 總結(jié)學(xué)習(xí)類和對象核心在于理解封裝、內(nèi)存分布、以及編譯器默認(rèn)生成的六大函數(shù)的行為?;A(chǔ)區(qū)分面向過程和面向?qū)ο笳莆疹惖淖饔糜颉嵗c大小計算內(nèi)存對齊。核心工具熟練使用this指針理解const成員函數(shù)的權(quán)限縮小原則。生命周期管理深刻理解構(gòu)造函數(shù)初始化、析構(gòu)函數(shù)清理、拷貝構(gòu)造深/淺拷貝的調(diào)用時機(jī)和底層行為。運算符重載掌握賦值運算符、前后置、流插入提取的重載規(guī)則。