![類和對(duì)象 [ 完結(jié) ]](http://pic.xiahunao.cn/yaotu/類和對(duì)象 [ 完結(jié) ])
深入理解構(gòu)造函數(shù)初始化列表與隱式類型轉(zhuǎn)換在C中構(gòu)造函數(shù)負(fù)責(zé)對(duì)象的初始化。除了在函數(shù)體內(nèi)賦值還有一種更核心、更高效的初始化方式——初始化列表。同時(shí)C支持的單參和多參構(gòu)造函數(shù)的隱式類型轉(zhuǎn)換也為代碼編寫帶來了便利。本文將系統(tǒng)梳理這些知識(shí)點(diǎn)幫助您徹底掌握。一、初始化列表構(gòu)造函數(shù)的“真正初始化戰(zhàn)場(chǎng)”1. 什么是初始化列表初始化列表是構(gòu)造函數(shù)定義中位于函數(shù)體之前的一段以冒號(hào)開頭后跟逗號(hào)分隔的成員變量列表每個(gè)成員變量后面用括號(hào)給出初始值或表達(dá)式。class Date { public: // 初始化列表以冒號(hào)開始逗號(hào)分隔 Date(int year, int month, int day) : _year(year), _month(month), _day(day) { // 函數(shù)體這里已經(jīng)是賦值而非初始化 } private: int _year, _month, _day; };關(guān)鍵理解初始化列表是成員變量定義并初始化的地方。每個(gè)成員變量在初始化列表中只能出現(xiàn)一次??梢哉J(rèn)為無論你是否在初始化列表中顯式寫出某個(gè)成員它都會(huì)“走一遍”初始化列表——如果沒寫編譯器會(huì)嘗試用默認(rèn)方式初始化它。2. 必須在初始化列表中初始化的三種成員有些成員變量必須在初始化列表中顯式初始化否則編譯報(bào)錯(cuò)引用成員變量int ref;引用必須在定義時(shí)綁定一個(gè)對(duì)象不能先定義再賦值。const成員變量const int cst;const變量也必須在定義時(shí)初始化不能后續(xù)賦值。沒有默認(rèn)構(gòu)造函數(shù)的類類型成員MyClass obj;如果該類型沒有提供無參或全缺省的默認(rèn)構(gòu)造函數(shù)編譯器無法在初始化列表中自動(dòng)構(gòu)造它必須由你顯式傳參。class MyClass { public: MyClass(int x) {} // 只有帶參構(gòu)造無默認(rèn)構(gòu)造 }; class Test { private: int ref; // 引用 const int cst; // const變量 MyClass obj; // 無默認(rèn)構(gòu)造的類類型 public: // 必須使用初始化列表初始化上述三種成員 Test(int r, int c, int m) : ref(r), cst(c), obj(m) { // 函數(shù)體內(nèi)不能初始化這三種成員 } };3. 初始化列表與函數(shù)體的配合初始化列表和函數(shù)體并非互斥它們可以打配合初始化列表負(fù)責(zé)定義和初始化函數(shù)體負(fù)責(zé)進(jìn)一步的邏輯處理如錯(cuò)誤檢查、復(fù)雜賦值。class Test { private: int* _ptr; public: Date() : _ptr((int*)malloc(12)) { // 初始化列表分配內(nèi)存 // 函數(shù)體檢查并初始化內(nèi)存 if (_ptr nullptr) { perror(malloc fail); } else { memset(_ptr, 0, 12); } } };4. C11的成員聲明缺省值C11允許在成員變量聲明時(shí)直接給缺省值private: int _year 1; // 聲明時(shí)的缺省值 int _month 1; int _day 1;這個(gè)缺省值的作用是當(dāng)該成員在初始化列表中沒有被顯式初始化時(shí)編譯器會(huì)使用這個(gè)缺省值來初始化它。可以理解為缺省值是提供給初始化列表的“備選方案”。注意對(duì)于引用、const、無默認(rèn)構(gòu)造的類類型成員即使在聲明時(shí)給了缺省值也必須在初始化列表中顯式初始化否則依然編譯報(bào)錯(cuò)。因?yàn)榫幾g器認(rèn)為缺省值是“賦值邏輯”而非“初始化邏輯”而這三者必須通過初始化來綁定或構(gòu)造。5. 初始化列表的初始化順序重要初始化列表的執(zhí)行順序不是按照列表中的書寫順序而是嚴(yán)格按照成員變量在類中聲明的順序。這是因?yàn)槌蓡T變量在內(nèi)存中的布局由聲明順序決定初始化必須遵循該順序。class A { public: A(int a) : _a1(a), _a2(_a1) {} // 看起來先_a1后_a2 void Print() { cout _a1 _a2 endl; } private: int _a2 2; // 聲明_a2在前 int _a1 2; // 聲明_a1在后 }; int main() { A aa(1); aa.Print(); // 輸出結(jié)果1 隨機(jī)值_a2先初始化此時(shí)_a1未初始化 }如上例雖然初始化列表先寫_a1(a)但由于_a2在類中先聲明所以先初始化_a2值為未初始化的_a1再初始化_a1值為1。建議聲明順序與初始化列表順序保持一致以避免此類混淆。6. 總結(jié)初始化的完整規(guī)則每個(gè)成員變量都會(huì)走初始化列表顯式在初始化列表中直接用給定的值/表達(dá)式初始化。未在初始化列表中如果聲明處有缺省值使用缺省值。如果聲明處無缺省值內(nèi)置類型是否初始化取決于編譯器通常不處理為隨機(jī)值。自定義類型調(diào)用其默認(rèn)構(gòu)造函數(shù)無參/全缺省若沒有則編譯報(bào)錯(cuò)。引用、const、無默認(rèn)構(gòu)造的自定義類型必須在初始化列表中顯式初始化。最佳實(shí)踐盡量使用初始化列表來初始化所有成員這更高效也更能避免遺漏。二、構(gòu)造函數(shù)的隱式類型轉(zhuǎn)換C允許通過隱式類型轉(zhuǎn)換用其他類型的值直接構(gòu)造對(duì)象這極大地簡(jiǎn)化了代碼。1. 單參數(shù)構(gòu)造函數(shù)的隱式轉(zhuǎn)換當(dāng)構(gòu)造函數(shù)只有一個(gè)參數(shù)時(shí)或有缺省值使得可單參調(diào)用可以用賦值語法進(jìn)行隱式轉(zhuǎn)換class A { public: A(int a 0) { _a1 a; } // 單參數(shù)/全缺省構(gòu)造 private: int _a1; }; int main() { A aa1(1); // 直接構(gòu)造 A aa2 2; // 隱式類型轉(zhuǎn)換先用2構(gòu)造臨時(shí)A對(duì)象再拷貝構(gòu)造aa2編譯器通常會(huì)優(yōu)化為直接構(gòu)造 return 0; }實(shí)際上A aa2 2;等價(jià)于A aa2 A(2);會(huì)生成一個(gè)臨時(shí)對(duì)象但編譯器會(huì)優(yōu)化掉中間的臨時(shí)對(duì)象直接構(gòu)造。常性臨時(shí)對(duì)象生成的臨時(shí)對(duì)象具有常性const因此如果用引用綁定它必須加constcppconst A raa 2; // 正確const引用綁定臨時(shí)對(duì)象 // A raa 2; // 錯(cuò)誤非常量引用不能綁定臨時(shí)對(duì)象2. 多參數(shù)構(gòu)造函數(shù)的隱式轉(zhuǎn)換C11起C11開始支持多參數(shù)的隱式類型轉(zhuǎn)換使用花括號(hào)列表cppclass A { public: A(int a1, int a2) { _a1 a1; _a2 a2; } private: int _a1, _a2; }; int main() { A aa3 {1, 1}; // 多參數(shù)隱式轉(zhuǎn)換 const A raa4 {2, 2}; // const引用綁定臨時(shí)對(duì)象 return 0; }3. 實(shí)際應(yīng)用場(chǎng)景函數(shù)傳參隱式類型轉(zhuǎn)換在函數(shù)傳參時(shí)非常方便cppclass Stack { public: void Push(const A aa) { // 參數(shù)為const引用可以接收臨時(shí)對(duì)象 // ... } }; int main() { Stack st; A aa(3); st.Push(aa); // 傳已有對(duì)象 st.Push(3); // 隱式轉(zhuǎn)換3 - 臨時(shí)A對(duì)象再傳給Push st.Push({1,2}); // 多參數(shù)隱式轉(zhuǎn)換 return 0; }4. 禁止隱式類型轉(zhuǎn)換explicit關(guān)鍵字如果不想讓編譯器進(jìn)行這種隱式轉(zhuǎn)換可以在構(gòu)造函數(shù)前加explicit關(guān)鍵字cppclass A { public: explicit A(int a 0) { _a1 a; } }; int main() { A aa1(1); // 正確直接構(gòu)造 // A aa2 2; // 錯(cuò)誤禁止隱式轉(zhuǎn)換 return 0; }三、補(bǔ)充默認(rèn)構(gòu)造函數(shù)辨析默認(rèn)構(gòu)造函數(shù)是指可以無參調(diào)用的構(gòu)造函數(shù)包括編譯器自動(dòng)生成的默認(rèn)構(gòu)造函數(shù)如果用戶未定義任何構(gòu)造函數(shù)。用戶定義的無參構(gòu)造函數(shù)。用戶定義的全缺省構(gòu)造函數(shù)所有參數(shù)都有默認(rèn)值。以下代碼中的Date(int year, month, day)不是默認(rèn)構(gòu)造函數(shù)因?yàn)樗仨殏魅齻€(gè)參數(shù)cppclass Date { public: Date(int year, int month, int day) { /*...*/ } // 非默認(rèn)構(gòu)造 }; int main() { Date d1(2024, 7, 14); // 正確 Date d2; // 錯(cuò)誤沒有匹配的默認(rèn)構(gòu)造函數(shù) return 0; }如果希望Date有默認(rèn)構(gòu)造可以定義全缺省構(gòu)造函數(shù)或讓編譯器自動(dòng)生成需注意內(nèi)置類型默認(rèn)不初始化。四、析構(gòu)順序小貼士成員析構(gòu)順序與初始化順序聲明順序相反即先聲明的后析構(gòu)。多個(gè)對(duì)象析構(gòu)后創(chuàng)建的對(duì)象先析構(gòu)棧的后進(jìn)先出特性。以下是針對(duì) “必須在初始化列表中初始化的成員變量” 的示例#include iostream using namespace std; // 示例類沒有默認(rèn)構(gòu)造函數(shù)只定義了帶參構(gòu)造 class MyClass { public: // 只有帶參數(shù)的構(gòu)造函數(shù)無默認(rèn)構(gòu)造函數(shù) MyClass(int x) { cout MyClass 帶參構(gòu)造函數(shù)被調(diào)用x x endl; } }; class Test { private: int ref; // 引用成員變量 const int cst; // const成員變量 MyClass obj; // 沒有默認(rèn)構(gòu)造函數(shù)的類類型變量 public: // 必須使用初始化列表初始化上述三種成員變量 Test(int r, int c, int m) : ref(r), cst(c), obj(m) { // 注意以下操作都是錯(cuò)誤的不能在函數(shù)體內(nèi)初始化這三種變量 // ref r; // 錯(cuò)誤引用必須在定義時(shí)初始化 // cst c; // 錯(cuò)誤const變量必須在定義時(shí)初始化 // obj MyClass(m); // 錯(cuò)誤MyClass沒有默認(rèn)構(gòu)造函數(shù)此處會(huì)先調(diào)用默認(rèn)構(gòu)造再賦值導(dǎo)致編譯失敗 } }; int main() { int a 10; Test t(a, 20, 30); return 0; }說明引用成員變量ref引用必須在定義時(shí)立即綁定對(duì)象無法先定義再賦值因此必須在初始化列表中初始化。const 成員變量cstconst 變量一旦定義就不能修改必須在定義時(shí)初始化因此必須在初始化列表中指定初始值。沒有默認(rèn)構(gòu)造函數(shù)的類類型變量obj當(dāng)類中沒有默認(rèn)構(gòu)造函數(shù)時(shí)創(chuàng)建對(duì)象必須傳入?yún)?shù)。如果不在初始化列表中初始化編譯器會(huì)嘗試調(diào)用其默認(rèn)構(gòu)造函數(shù)導(dǎo)致編譯報(bào)錯(cuò)。#includeiostream using namespace std; class A { public: //如果不想讓類型轉(zhuǎn)換發(fā)生可以在構(gòu)造函數(shù)前加explicit函數(shù) /*explicit A(int a 0) { _a1 a; }*///多參數(shù)也是如此 A(int a 0)//單參數(shù)可以 { _a1 a; } A(int a1, int a2) { _a1 a1; _a2 a2; } A(const A aa) { _a1 aa._a1; } void Print() { cout _a1 _a2 endl; } private: int _a1 1; int _a2 2; }; class Stack { public: void Push(const A aa)//臨時(shí)對(duì)象具有常性要加const { //... } private: A _arr[10]; int _top; }; int main() { A aa1(1);//調(diào)用構(gòu)造 aa1.Print(); // 隱式類型轉(zhuǎn)換類型轉(zhuǎn)換會(huì)生成中間變量 // 2先構(gòu)造?個(gè)A的臨時(shí)對(duì)象再?這個(gè)臨時(shí)對(duì)象拷?構(gòu)造aa2 // 編譯器遇到連續(xù)構(gòu)造拷?構(gòu)造-優(yōu)化為直接構(gòu)造 //調(diào)試觀察 A aa2 2; aa2.Print(); A raa1 aa2; //A aa2 2;可以驗(yàn)證 //并不是直接構(gòu)造會(huì)生成臨時(shí)變量具有常性 const A aa2 2; //真正用意 Stack st; A aa3(3); st.Push(aa3); //寫起來更簡(jiǎn)單消耗是一樣的 st.Push(3); //多參數(shù) //A aa4 1, 1;//不支持 //但是C11后才支持多參數(shù)轉(zhuǎn)換 A aa4 { 1, 1 }; const A raa5 { 2,2 }; st.Push(aa4); st.Push({ 2,2 }); return 0; }注意初始化列表是他定義的地方那么每個(gè)成員都會(huì)去走初始化列表哪怕是不寫因?yàn)槭嵌x初始化的地方還有一類自定義類型EGclass Time { public: Time(int hour 1) :_hour(hour) { cout Time() endl; } private: int _hour; };不在初始化列表中定義 不寫也走只調(diào)用Time的默認(rèn)構(gòu)造函數(shù)無參/全缺省如果Time沒有默認(rèn)構(gòu)造調(diào)不到只能自己傳愛傳什么傳什么無默認(rèn)構(gòu)造class Time { public: Time(int hour) :_hour(hour) { cout Time() endl; } private: int _hour; }; class Date { public: Date(int xx, int year, int month, int day) : _year(year) , _month(month) , _day(day) , _n(1) , _ref(xx) , _t(1) {} void Print()const { cout _year 年 _month 月 _day 日 endl; } private: //聲明 int _year; int _month; int _day; const int _n; int _ref; Time _t; };初始化列表和函數(shù)體內(nèi)可以混著賦值EG初始化列表和函數(shù)體可以打配合Date() :_ptr((int*)malloc(12)) { //初始化列表和函數(shù)體內(nèi)可以混著賦值 if (_ptr nullptr) { perror(malloc fail); } else { memset(_ptr, 0, 12); } }C11 ?持在成員變量聲明的位置給缺省值這個(gè)缺省值主要是給沒有顯?在初始化列表初始化的成員使?的注意const 成員變量、引用成員變量、沒有默認(rèn)構(gòu)造的類類型變量即使在聲明時(shí)給了缺省值也必須在初始化列表中顯式初始化否則依然會(huì)編譯報(bào)錯(cuò)。編譯器會(huì)認(rèn)為缺省值是 “賦值邏輯”而非 “初始化邏輯”注定義是有開空間下列是聲明C11及后private: //聲明,后面為缺省值-初始化列表用的 int _year 1; int _month 1; int _day 1;;可以通過缺省函數(shù)理解只不過是給初始化類表用的結(jié)合下圖理解盡量使?初始化列表初始化因?yàn)槟切┠悴辉诔跏蓟斜沓跏蓟某蓡T也會(huì)?初始化列表如果這個(gè)成員在聲明位置給了缺省值初始化列表會(huì)?這個(gè)缺省值初始化。如果你沒有給缺省值對(duì)于沒有顯?在初始化列表初始化的內(nèi)置類型成員是否初始化取決于編譯器C 并沒有規(guī)定。對(duì)于沒有顯?在初始化列表初始化的?定義類型成員會(huì)調(diào)?這個(gè)成員類型的默認(rèn)構(gòu)造函數(shù)如果沒有默認(rèn)構(gòu)造會(huì)編譯錯(cuò)誤注意與默認(rèn)構(gòu)造函數(shù)區(qū)分可以無參調(diào)用全缺省無參的才是默認(rèn)構(gòu)造以下并不是#includeiostream using namespace std; class Date { public: Date(int year, int month, int day) : _year(year) , _month(month) //, _day(day)對(duì)于內(nèi)置類型是不做處理的可能是個(gè)隨機(jī)值 { } void Print()const { cout _year 年 _month 月 _day 日 endl; } private: //聲明,后面為缺省值-初始化列表用的 int _year 1; int _month 1; int _day 1;; }; int main() { Date d1(2024, 7, 14); d1.Print(); Date d2;//會(huì)報(bào)錯(cuò)所以并不是默認(rèn)構(gòu)造 return 0; }因此讓編譯器自動(dòng)生成默認(rèn)構(gòu)造便可行還有不僅僅可以給以上這種缺省值private: int _year 1; int _month 1; int _day 1;; int* _ptr (int*)malloc(12); Time _t 1;初始化列表中按照成員變量在類中聲明順序進(jìn)?初始化跟成員在初始化列表出現(xiàn)的先后順序?關(guān)。建議聲明順序和初始化列表順序保持?致成員析構(gòu)順序與聲明順序相反先構(gòu)造的 m1 后析構(gòu)后構(gòu)造的 m3 先析構(gòu)多個(gè)對(duì)象析構(gòu)時(shí)后創(chuàng)建的先析構(gòu)t2 比 t1 后創(chuàng)建t2 先析構(gòu)。例題下面程序的運(yùn)行結(jié)果是什么DA. 輸出 1 1B. 輸出 2 2C. 編譯報(bào)錯(cuò)D. 輸出 1 隨機(jī)值E. 輸出 1 2F. 輸出 2 1#includeiostream using namespace std; class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout _a1 _a2 endl; } private: int _a2 2; int _a1 2; }; int main() { A aa(1); aa.Print(); }因?yàn)槎x是先_a2后_a1,先對(duì)_a2進(jìn)行初始化這時(shí)候_a1的缺省值還沒發(fā)現(xiàn)給_a2的初始化就是編譯器自己決定的為_a1的隨機(jī)值將_a1的值給了_a2,再到_a1,將1的值給了_a1所以answer是D(調(diào)試很快發(fā)現(xiàn))總結(jié)盡可能寫初始化列表每個(gè)成員都要走初始化列表每個(gè)構(gòu)造函數(shù)都有初始化列表在初始化列表初始化的成員直接用顯示寫沒有在初始化列表的成員不顯示寫a. 聲明的地方有缺省值用缺省值b. 沒有缺省值內(nèi)置類型不確定看編譯器大概率是隨機(jī)值自定義類型調(diào)用默認(rèn)構(gòu)造沒有默認(rèn)構(gòu)造就編譯報(bào)錯(cuò)引用const沒有默認(rèn)構(gòu)造的自定義必須在初始化列表初始化不要把初始化列表和默認(rèn)構(gòu)造扯上關(guān)系初始化列表中按照成員變量在類中聲明順序在內(nèi)存中存放的順序進(jìn)行初始化聲明中的缺省值是給初始化列表用的缺省值是走初始化列表的2.類型轉(zhuǎn)換C ?持內(nèi)置類型隱式類型轉(zhuǎn)換為類類型對(duì)象需要有相關(guān)內(nèi)置類型為參數(shù)的構(gòu)造函數(shù)構(gòu)造函數(shù)前?加explicit就不再?持隱式類型轉(zhuǎn)換以下是展示 C 中內(nèi)置類型隱式轉(zhuǎn)換為類類型以及使用explicit關(guān)鍵字禁止隱式轉(zhuǎn)換的代碼示例#include iostream using namespace std; class MyInt { private: int value; public: // 普通構(gòu)造函數(shù)允許隱式轉(zhuǎn)換 MyInt(int v) : value(v) { cout MyInt 構(gòu)造函數(shù)被調(diào)用value v endl; } int getValue() const { return value; } }; class ExplicitInt { private: int value; public: // 用 explicit 修飾的構(gòu)造函數(shù)禁止隱式轉(zhuǎn)換 explicit ExplicitInt(int v) : value(v) { cout ExplicitInt 構(gòu)造函數(shù)被調(diào)用value v endl; } int getValue() const { return value; } }; // 測(cè)試函數(shù)參數(shù)為 MyInt 類型 void printMyInt(MyInt m) { cout printMyInt: m.getValue() endl; } // 測(cè)試函數(shù)參數(shù)為 ExplicitInt 類型 void printExplicitInt(ExplicitInt e) { cout printExplicitInt: e.getValue() endl; } int main() { // 1. 測(cè)試普通構(gòu)造函數(shù)允許隱式轉(zhuǎn)換 MyInt m1 10; // 隱式轉(zhuǎn)換int → MyInt調(diào)用 MyInt(10) printMyInt(20); // 隱式轉(zhuǎn)換int → MyInt調(diào)用 MyInt(20) // 2. 測(cè)試 explicit 構(gòu)造函數(shù)禁止隱式轉(zhuǎn)換 ExplicitInt e1(30); // 直接初始化允許 // ExplicitInt e2 40; // 錯(cuò)誤不允許隱式轉(zhuǎn)換編譯報(bào)錯(cuò) // printExplicitInt(50); // 錯(cuò)誤不允許隱式轉(zhuǎn)換編譯報(bào)錯(cuò) // 顯式轉(zhuǎn)換是允許的 ExplicitInt e3 ExplicitInt(40); // 顯式構(gòu)造 printExplicitInt(ExplicitInt(50)); // 顯式構(gòu)造后傳遞參數(shù) return 0; }代碼說明MyInt類其構(gòu)造函數(shù)未加explicit允許int類型隱式轉(zhuǎn)換為MyInt對(duì)象如MyInt m1 10或printMyInt(20)。ExplicitInt類其構(gòu)造函數(shù)加了explicit禁止隱式轉(zhuǎn)換ExplicitInt e2 40會(huì)編譯報(bào)錯(cuò)不允許隱式轉(zhuǎn)換printExplicitInt(50)會(huì)編譯報(bào)錯(cuò)不允許隱式轉(zhuǎn)換但顯式轉(zhuǎn)換如ExplicitInt(40)仍然允許。運(yùn)行結(jié)果MyInt 構(gòu)造函數(shù)被調(diào)用value 10 MyInt 構(gòu)造函數(shù)被調(diào)用value 20 printMyInt: 20 ExplicitInt 構(gòu)造函數(shù)被調(diào)用value 30 ExplicitInt 構(gòu)造函數(shù)被調(diào)用value 40 ExplicitInt 構(gòu)造函數(shù)被調(diào)用value 50 printExplicitInt: 50通過explicit關(guān)鍵字可以避免意外的隱式類型轉(zhuǎn)換增強(qiáng)代碼的可讀性和安全性。C 中 “內(nèi)置類型隱式轉(zhuǎn)換為類類型” 的原理本質(zhì)是編譯器自動(dòng)調(diào)用單參數(shù)構(gòu)造函數(shù)或可接收單個(gè)參數(shù)的構(gòu)造函數(shù)將內(nèi)置類型值轉(zhuǎn)換為目標(biāo)類的臨時(shí)對(duì)象。具體過程可拆解為 3 步當(dāng)需要一個(gè)類類型對(duì)象如函數(shù)參數(shù)、賦值操作的右側(cè)但實(shí)際提供的是一個(gè)內(nèi)置類型值時(shí)編譯器會(huì)檢查該類是否有以該內(nèi)置類型為參數(shù)的構(gòu)造函數(shù)可以是單參數(shù)構(gòu)造或其他參數(shù)有默認(rèn)值的多參數(shù)構(gòu)造如MyClass(int a, int b0)可接收單個(gè)int。若存在這樣的構(gòu)造函數(shù)編譯器會(huì)自動(dòng)調(diào)用該構(gòu)造函數(shù)創(chuàng)建一個(gè)臨時(shí)的類對(duì)象用內(nèi)置類型值初始化這個(gè)臨時(shí)對(duì)象。這個(gè)臨時(shí)對(duì)象會(huì)被用來參與后續(xù)操作如傳遞給函數(shù)參數(shù)、賦值給類對(duì)象等。3.static成員?static修飾的成員變量稱之為靜態(tài)成員變量靜態(tài)成員變量?定要在類外進(jìn)?初始化內(nèi)只能對(duì)靜態(tài)成員變量進(jìn)行聲明需用static關(guān)鍵字不能直接初始化。必須在類外單獨(dú)初始化格式為類型 類名::靜態(tài)成員變量名 初始值;不加static關(guān)鍵字。例外情況對(duì)于靜態(tài)常量成員變量static constC11 允許在類內(nèi)直接初始化但仍可在類外初始化以類外為準(zhǔn)。對(duì)于模板類的靜態(tài)成員變量初始化規(guī)則略有不同可在類內(nèi)或類外初始化視具體場(chǎng)景而定。示例代碼class Test { public: // 類內(nèi)聲明靜態(tài)成員變量不可直接初始化除 static const 外 static int a; // 正確聲明 static const int b 10; // 正確靜態(tài)常量可在類內(nèi)初始化C11 起 // static int c 20; // 錯(cuò)誤非 const 靜態(tài)成員不能在類內(nèi)初始化 }; // 類外初始化靜態(tài)成員變量必須做 int Test::a 0; // 正確類外初始化 // const int Test::b 30; // 可選若類內(nèi)已初始化類外可再次初始化覆蓋類內(nèi)值 int main() { return 0; }個(gè)人問題提出static 加上了 const那干嘛不直接使用const也就是這兩個(gè)有什么區(qū)別static const和普通const成員變量的核心區(qū)別在于作用域和存儲(chǔ)方式二者適用場(chǎng)景不同不能簡(jiǎn)單替代static const成員變量屬于類級(jí)別的常量所有對(duì)象共享同一份數(shù)據(jù)存儲(chǔ)在全局?jǐn)?shù)據(jù)區(qū)不占用對(duì)象內(nèi)存。可通過類名::成員名直接訪問無需創(chuàng)建對(duì)象。常用于表示類的公共常量如類的版本號(hào)、默認(rèn)配置等。class Config { public: static const int MAX_SIZE 1024; // 所有對(duì)象共享此常量 }; // 使用時(shí)無需創(chuàng)建對(duì)象 int main() { cout Config::MAX_SIZE endl; // 直接訪問 return 0; }普通const成員變量屬于對(duì)象級(jí)別的常量每個(gè)對(duì)象都有獨(dú)立的副本存儲(chǔ)在對(duì)象內(nèi)存中。必須通過對(duì)象訪問且初始化后當(dāng)前對(duì)象內(nèi)不可修改但不同對(duì)象可有不同初始值。常用于表示對(duì)象獨(dú)有的常量屬性如學(xué)生的學(xué)號(hào)、身份證號(hào)等。class Student { private: const int id; // 每個(gè)學(xué)生有唯一且不可修改的學(xué)號(hào) public: Student(int i) : id(i) {} // 必須在初始化列表中賦值 }; int main() { Student s1(1001); // id1001對(duì)象1的常量 Student s2(1002); // id1002對(duì)象2的常量 return 0; }若常量屬于類本身所有對(duì)象共享且邏輯一致用static const。若常量屬于每個(gè)對(duì)象不同對(duì)象可能有不同值用普通const。二者的設(shè)計(jì)初衷不同不能相互替代。靜態(tài)成員變量為當(dāng)前類的所有對(duì)象所共享不屬于某個(gè)具體的對(duì)象不存在對(duì)象中存放在靜態(tài)區(qū)?static修飾的成員函數(shù)稱之為靜態(tài)成員函數(shù)靜態(tài)成員函數(shù)沒有this 指針如果_scount是私有的那么指定類域的方式就行不通了這時(shí)候就可以提供一個(gè)靜態(tài)的成員函數(shù)static int GetACount() { return _scount; } // 靜態(tài)成員函數(shù)中 return _scount 的本質(zhì)是 return 類名::_scount編譯器會(huì)自動(dòng)補(bǔ)充類域而非依賴 this 指針的對(duì)象成員訪問。 // 如果 _scount 不是 static 修飾的普通成員變量那么在靜態(tài)成員函數(shù)中直接訪問 _scount 會(huì)編譯報(bào)錯(cuò)。靜態(tài)成員函數(shù)中可以訪問其他的靜態(tài)成員但是不能訪問非靜態(tài)的因?yàn)闆]有 this 指針static int GetACount() { _a;//報(bào)錯(cuò)//不能訪問?靜態(tài)的因?yàn)闆]有this指針 return _scount; }非靜態(tài)的成員函數(shù)可以訪問任意的靜態(tài)成員變量和靜態(tài)成員函數(shù)void func() { cout _scount endl; cout GetACount() endl; }突破類域就可以訪問靜態(tài)成員可以通過類名靜態(tài)成員或者對(duì)象.靜態(tài)成員來訪問靜態(tài)成員變量和靜態(tài)成員函數(shù)cout A::GetACount() endl; cout a1.GetACount() endl;靜態(tài)成員也是類的成員受public、protected、private訪問限定符的限制(類里面定義的靜態(tài)成員變量某種程度上可以理解為靜態(tài)的全局只是受類域的限制也就是他的生命周期是全局的且不屬于某個(gè)對(duì)象嚴(yán)格來說只能在所在的類里面使用類外面也行只是受訪問限定符的限制)靜態(tài)成員變量不能在聲明位置給缺省值初始化因?yàn)槿笔≈凳墙o構(gòu)造函數(shù)初始化列表的靜態(tài)成員變量不屬于某個(gè)對(duì)象不?構(gòu)造函數(shù)初始化列表4.友元友元提供了?種突破類訪問限定符封裝的?式友元分為友元函數(shù)和友元類在函數(shù)聲明或者類聲明的前?加friend并且把友元聲明放到?個(gè)類的??外部友元函數(shù)可訪問類的私有和保護(hù)成員友元函數(shù)僅僅是?種聲明它不是類的成員函數(shù)友元函數(shù)可以在類定義的任何地?聲明不受類訪問限定符限制?個(gè)函數(shù)可以是多個(gè)類的友元函數(shù)我不僅僅和你還要和別人做朋友#includeiostream using namespace std; // 前置聲明不然則A的友元函數(shù)聲明編譯器不認(rèn)識(shí)B//-----注意點(diǎn) class B;//-----解決點(diǎn) class A { // 友元聲明 friend void func(const A aa, const B bb);//-----問題點(diǎn) private: int _a1 1; int _a2 2; }; class B { // 友元聲明 friend void func(const A aa, const B bb); private: int _b1 3; int _b2 4; }; void func(const A aa, const B bb) { cout aa._a1 endl;//~~~~~~~~~~~~~~~~~~~~~~~~~ cout bb._b1 endl;//一個(gè)函數(shù)成為多個(gè)類的友元 } int main() { A aa; B bb; func(aa, bb); return 0; }友元類中的成員函數(shù)都可以是另?個(gè)類的友元函數(shù)都可以訪問另?個(gè)類中的私有和保護(hù)成員#includeiostream using namespace std; class A { // 友元聲明 friend class B;//把B定義成A的友元類-----解決點(diǎn)---B的成員函數(shù)都成為了A的友元函數(shù) private: int _a1 1; int _a2 2; }; class B { public://B要一直訪問A類的私有保護(hù)成員-----問題點(diǎn) void func1(const A aa) { cout aa._a1 endl; cout _b1 endl; } void func2(const A aa) { cout aa._a2 endl; cout _b2 endl; } private: int _b1 3; int _b2 4; }; int main() { A aa; B bb; bb.func1(aa); bb.func1(aa); return 0; }友元類的關(guān)系是單向的不具有交換性?如A類是B類的友元但是B類不是A類的友元友元類關(guān)系不能傳遞如果A是B的友元 B是C的友元但是A不一定是C的友元有時(shí)提供了便利。但是友元會(huì)增加耦合度破壞了封裝所以友元不宜多?個(gè)人問題提出基類中聲明友元是不是直接可以訪問其子類相反呢基類中聲明的友元與子類的訪問關(guān)系以及子類中聲明的友元與基類的訪問關(guān)系遵循以下規(guī)則1. 基類的友元不能直接訪問子類的私有 / 保護(hù)成員基類的友元僅能訪問基類自身的私有 / 保護(hù)成員無法自動(dòng)訪問子類的私有 / 保護(hù)成員即使子類繼承了基類。子類的成員包括從基類繼承的成員中只有子類自己聲明為public的成員才能被外部訪問私有 / 保護(hù)成員受子類自身的訪問限定符保護(hù)與基類的友元無關(guān)。示例class Base { friend void FriendFunc(Base b); // 友元函數(shù)聲明 protected: int _base_val 10; }; class Derived : public Base { private: int _derived_val 20; // 子類私有成員 }; void FriendFunc(Base b) { // 可以訪問基類的保護(hù)成員 cout b._base_val endl; // 若傳入子類對(duì)象仍無法訪問子類的私有成員 Derived d (Derived)b; // 強(qiáng)制轉(zhuǎn)換為子類引用 // cout d._derived_val endl; // 錯(cuò)誤無法訪問子類私有成員 }2. 子類的友元不能直接訪問基類的私有 / 保護(hù)成員子類的友元僅能訪問子類自身的私有 / 保護(hù)成員無法訪問基類的私有 / 保護(hù)成員除非基類主動(dòng)將該友元聲明為自己的友元。基類的私有 / 保護(hù)成員僅對(duì)基類自身和基類聲明的友元開放與子類的友元無關(guān)。示例class Base { private: int _base_val 10; // 基類私有成員 }; class Derived : public Base { friend void FriendFunc(Derived d); // 子類友元聲明 private: int _derived_val 20; }; void FriendFunc(Derived d) { // 可以訪問子類的私有成員 cout d._derived_val endl; // 無法訪問基類的私有成員即使子類繼承了基類 // cout d._base_val endl; // 錯(cuò)誤無法訪問基類私有成員 }總結(jié)友元關(guān)系是單向且局限于聲明它的類與繼承關(guān)系無關(guān)?;惖挠言?→ 只能訪問基類的私有 / 保護(hù)成員無權(quán)訪問子類的私有 / 保護(hù)成員。子類的友元 → 只能訪問子類的私有 / 保護(hù)成員無權(quán)訪問基類的私有 / 保護(hù)成員。若需要跨類訪問必須在對(duì)應(yīng)的類中單獨(dú)聲明友元如基類想讓子類的友元訪問自己需在基類中額外聲明該友元。5.內(nèi)部類如果一個(gè)類定義在另一個(gè)類的內(nèi)部這個(gè)內(nèi)部類就叫做內(nèi)部類。內(nèi)部類是一個(gè)獨(dú)立的類跟定義在全局相比它僅僅只是受外部類類域限制和訪問限定符限制所以外部類定義的對(duì)象中不包含內(nèi)部類內(nèi)類不是不屬外類的成員內(nèi)部類默認(rèn)是外部類的友元類我是你的朋友所以我可以訪問你的私有內(nèi)部類本質(zhì)也是一種封裝當(dāng) A 類跟 B 類緊密關(guān)聯(lián)A 類實(shí)現(xiàn)出來主要就是給 B 類使用那么可以考慮把 A 類設(shè)計(jì)為 B 的內(nèi)部類如果放到 private/protected 位置那么 A 類就是 B 類的專屬內(nèi)部類其他地方都用不了#includeiostream using namespace std; class A { private: static int _k; int _h 1; public: class B // B默認(rèn)就是A的友元 { public: void foo(const A a) { cout _k endl; //OK cout a._h endl; //OK } private: int _b 1; }; }; int A::_k 1; int main() { cout sizeof(A) endl;//4:實(shí)際上只有一個(gè)_h,沒帶_b//B不是A的成員 A::B b;//指定B是屬于A的類域受訪問限定符限制 A aa; b.foo(aa); return 0; }6.匿名對(duì)象用“類型 (實(shí)參)”定義出來的對(duì)象叫做匿名對(duì)象相比之前用“類型 對(duì)象名 (實(shí)參)”定義的對(duì)象后者被稱為有名對(duì)象匿名對(duì)象的生命周期只在當(dāng)前一行一般用于臨時(shí)定義一個(gè)對(duì)象并立即使用比如調(diào)用成員函數(shù)、傳遞參數(shù)用完后下一行就會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)匿名對(duì)象的核心作用就是簡(jiǎn)化代碼、提升便捷性避免為臨時(shí)使用的對(duì)象額外定義一個(gè)對(duì)象名。 代碼示例匿名對(duì)象 vs 有名對(duì)象#include iostream using namespace std; class MyClass { public: MyClass(int val) : _val(val) { cout 構(gòu)造函數(shù)val _val endl; } ~MyClass() { cout 析構(gòu)函數(shù)val _val endl; } void ShowVal() { cout 當(dāng)前值 _val endl; } private: int _val; }; int main() { // 1. 有名對(duì)象需要定義對(duì)象名生命周期到作用域結(jié)束main函數(shù)結(jié)束 cout 有名對(duì)象 endl; MyClass obj(10); // 有名對(duì)象對(duì)象名obj obj.ShowVal(); // 通過對(duì)象名調(diào)用成員函數(shù) // 2. 匿名對(duì)象無對(duì)象名生命周期僅當(dāng)前行 cout \n 匿名對(duì)象 endl; MyClass(20).ShowVal(); // 匿名對(duì)象直接調(diào)用成員函數(shù)一行內(nèi)用完即析構(gòu) cout \n main函數(shù)即將結(jié)束 endl; return 0; } 運(yùn)行結(jié)果與分析 有名對(duì)象 構(gòu)造函數(shù)val 10 當(dāng)前值10 匿名對(duì)象 構(gòu)造函數(shù)val 20 當(dāng)前值20 析構(gòu)函數(shù)val 20 // 匿名對(duì)象在當(dāng)前行結(jié)束后立即析構(gòu) main函數(shù)即將結(jié)束 析構(gòu)函數(shù)val 10 // 有名對(duì)象在作用域結(jié)束main結(jié)束后析構(gòu)從結(jié)果能明顯看到匿名對(duì)象在調(diào)用完ShowVal()后立刻析構(gòu)而有名對(duì)象要等到main函數(shù)結(jié)束才析構(gòu)這正是二者生命周期的核心區(qū)別。7.對(duì)象拷貝時(shí)的編譯器優(yōu)化現(xiàn)代編譯器會(huì)為了盡可能提高程序效率在不影響代碼正確性的前提下盡量減少傳參和傳返回值過程中可省略的拷貝操作通常稱為 “拷貝省略” 優(yōu)化關(guān)于如何優(yōu)化C 標(biāo)準(zhǔn)并未做嚴(yán)格規(guī)定具體優(yōu)化邏輯由各個(gè)編譯器自行決定。當(dāng)前主流的較新版本編譯器會(huì)對(duì)單個(gè)表達(dá)式內(nèi)連續(xù)的拷貝操作進(jìn)行合并優(yōu)化部分更新、更 “激進(jìn)” 的編譯器甚至?xí)С挚绱a行、跨表達(dá)式的拷貝合并優(yōu)化。示例類#includeiostream using namespace std; class A { public: A(int a 0) :_a1(a) { cout A(int a) endl; } A(const A aa) :_a1(aa._a1) { cout A(const A aa) endl; } A operator(const A aa) { cout A operator(const A aa) endl; if (this ! aa) { _a1 aa._a1; } return *this; } ~A() { cout ~A() endl; } void Print() { cout A::Print _a1 endl; } private: int _a1 1; };單參數(shù)類型的構(gòu)造函數(shù)的隱式類型轉(zhuǎn)化的優(yōu)化示例int main() { //單參數(shù)類型的構(gòu)造函數(shù)的隱式類型轉(zhuǎn)化 A aa1 1;//構(gòu)造加拷貝構(gòu)造構(gòu)造臨時(shí)對(duì)象對(duì)臨時(shí)對(duì)象進(jìn)行拷貝構(gòu)造因?yàn)轭愋娃D(zhuǎn)化會(huì)產(chǎn)生臨時(shí)對(duì)象 //開始執(zhí)行發(fā)現(xiàn)編譯器優(yōu)化成直接拷貝 const A aa2 1;//生成臨時(shí)對(duì)象是引用沒有拷貝構(gòu)造 return 0; }傳參優(yōu)化示例void f1(A aa)//傳值傳參---會(huì)產(chǎn)生一個(gè)拷貝 {} void f2(A aa) {} int main() { A aa1(1);//構(gòu)造 //f1(aa1);//實(shí)參傳給形參進(jìn)行拷貝調(diào)用構(gòu)造拷貝構(gòu)造//不會(huì)優(yōu)化一般是在連續(xù)一個(gè)步驟才有優(yōu)化//但是不排除其他更激進(jìn)的 cout endl; //用引用減少拷貝構(gòu)造 f2(aa1); cout endl; //用匿名對(duì)象在連續(xù)過程 f1(A(1));//優(yōu)化成直接構(gòu)造 cout endl; f1(1);//本意是1走隱式類型轉(zhuǎn)化生成臨時(shí)對(duì)象再拷貝構(gòu)造優(yōu)化成直接構(gòu)造 cout endl; return 0; }傳返回值優(yōu)化示例A f3()//傳值傳參返回會(huì)產(chǎn)生一個(gè)臨時(shí)對(duì)象因?yàn)閍a出了作用域就銷毀了 { A aa(1); return aa;//產(chǎn)生臨時(shí)對(duì)象 } //應(yīng)該是aa作為局部對(duì)象出了作用域銷毀調(diào)用~A()由aa產(chǎn)生臨時(shí)對(duì)象調(diào)用Print //VS2022優(yōu)化嚴(yán)格來說沒有生成aa合二為一只生成臨時(shí)對(duì)象看執(zhí)行結(jié)果可推 int main() { f3().Print();//因?yàn)榕R死對(duì)象構(gòu)造拷貝構(gòu)造//用產(chǎn)生的臨時(shí)對(duì)象調(diào)用Print//臨時(shí)對(duì)象的周期就在這一行 cout endl; return 0; }A f4() { A aa(1);//構(gòu)造 return aa;//拷貝構(gòu)造 } //優(yōu)化省略了臨時(shí)對(duì)象VS2022更激進(jìn)連aa都省掉了建議用2019-debug觀察 int main() { A ret f4();//拷貝構(gòu)造 ret.Print(); cout endl; return 0; }A f4() { A aa(1);//構(gòu)造 cout ~~~~~~~~~ endl;//構(gòu)造了aaaa充當(dāng)臨時(shí)對(duì)象 return aa;//拷貝構(gòu)造 } int main() { A ret; ret f4();//賦值//賦值完后。臨時(shí)對(duì)象析構(gòu) ret.Print(); cout endl; return 0; }