C++篇:第八章_類_知識點大全
C++篇為本人學C++時所做筆記(特別是疑難雜點),全是硬貨,雖然看著枯燥但會讓你收益頗豐,可用作學習C++的一大利器
八、類
(一)類的概念與規則
- 「子類」和「子類型」的區別:
① 替換原則只適合於”子類型”關係,而一般程式語言只是考慮了”子類”關係,
② 子類 : 說明了新類是繼承自父類,故不能說繼承實現了子類型化
③ 子類型 : 強調的是新類具有父類一樣的行為(未必是繼承),故只有在公有繼承下,派生類是基類的子類型
④ 子類型關係是不可逆且不對稱的;子類型關係是可傳遞的
⑤ 子類型化與類型適應性是一致的
-
不可變類:說的是一個類一旦被實例化,就不可改變自身的狀態。常見的比如String和基本數據類型的包裝類,對於這種不可變類,一旦在進行引用傳遞的時候,發現當形參改變的時候二者地址不一樣;但當形參不做改變,只是單純把實參賦給形參的話二者地址是一樣的,所以在方法中對形參的改變,並不會影響實際參數。
-
類成員函數的定義不必須放在類定義體內部,但所有成員必須在類內部聲明
-
可以把子類的對象賦給父類的引用
-
向上轉換一定成功,向下轉換不一定成功。向下轉換必須存在虛函數,不然編譯錯誤
-
通用多態又分為參數多態和包含多態;特定多態分為過載多態和強制多態
-
以下三種情況下需要使用初始化成員列表:
① 情況一、需要初始化的數據成員是對象的情況(這裡包含了繼承情況下,通過顯示調用父類的構造函數對父類數據成員進行初始化)
② 情況二、需要初始化const修飾的類成員
③ 情況三、需要初始化引用成員數據
-
在C++中數據封裝是通過各種類型來實現的 // 錯誤;C++通過類來實現封裝性 ,把數據和與這些數據有關的操作封裝在一個類中,或者說,類的作用是把數據和演算法封裝在用戶聲明的抽象數據類型中
-
所謂的繼承使子類擁有父類所有的屬性和方法其實可以這樣理解,子類對象確實擁有父類對象中所有的屬性和方法,但是父類對象中的私有屬性和方法,子類是無法訪問到的,只是擁有,但不能使用。就像有些東西你可能擁有,但是你並不能使用。所以子類對象是絕對大於父類對象的,所謂的子類對象只能繼承父類非私有的屬性及方法的說法是錯誤的。可以繼承,只是無法訪問到而已
-
可不可以繼承不是由靜態還是實例決定,它是由修飾符來決定的
-
派生類與基類之間的特殊關係之一:基類指針可以在不進行顯示類型轉換的情況下指向派生類對象;基類引用可以在不進行顯示類型轉換的情況下引用派生類對象
-
如果函數內部有同名的類定義,則全局聲明在該函數內部是無效的,有效的是局部定義的(變數等均遵循這一規則)
-
在類中定義或聲明的數據類型的作用域是類內部,因此它們不能在類外使用
-
若::前沒有類名則該運算符不是類作用域限定符的含有,而是命名空間域限定符含義
-
根據重載或默認參數函數的要求,必須在第1次出現函數聲明或定義時就明確函數是否重載或有默認參數
-
對象指針訪問對象中的成員要用指針成員引用運算符「->」
-
基類是對派生類的抽象,派生類是對基類的具體化
-
在C++中多態性通過重載多態(函數和運算符重載)、強制多態(類型強制轉換),類型參數化多態(模板),包含多態(繼承及虛函數)四種形式實現
-
類中數據成員的生存期由對象的生存期決定 ,因為類中的成員變數是在類的對象聲明時創建,在對象生存期結束後截止
-
關於類和對象的描述不能說 是類就是 C 語言中的結構體類型,對象就是 C 語言中的結構體變數,因為c語言的結構只是一個簡單的構造數據類型,只能簡單的封裝數據;
c++的結構是支援面向對象程式設計的關鍵概念,是一種抽象數據類型,不僅如此還具有封裝特性,可以把數據和函數封裝在一起,並且可以限制成員訪問許可權,同時還具有繼承和多態等特性等,故能說C++語言中結構是一種類 -
類是一種數據結構;類是具有共同行為的若干對象的統一描述體
-
基類與派生類之間的關係可以有如下幾種描述:
① 派生類是基類的具體化:基類抽取了它的派生類的公共牲,而派生類通過增加行為將抽象類變為某種有用的類型
② 派生類是基類定義的延續
③ 派生類是基類的組合:在多繼承時,一個派生類有多於一個的基類,這時派生類將是所有基類行為的組合
- 在繼承圖中,有向線是從派生類指向基類
(二)類的使用
- 創建對象的示例:
① CSomething a();//只是定義一個方法,方法返回一個CSomething對象
② CSomething b(2);//增加1個對象
③ CSomething c[3];//對象數組,增加3個對象
④ CSomething &ra=b;//引用不增加對象
⑤ CSomething *pA=c;//地址賦值,不增加對象
⑥ CSomething *p=new CSomething;//在堆上構造一個對象,增加1個對象
-
面向對象程式設計方法的優點包含可重用性、可擴展性、易於管理和維護,但不簡單易懂
-
對象之間通訊實際上就是通過函數傳遞資訊,封裝是把數據和操作結合在一起,繼承是對於類的方法的改變和補充,重載是多態性之一
-
自動變數(auto)和暫存器變數(register)屬於動態存儲,調用時臨時分配單元
-
class 類名; // 類聲明,非類定義因為沒有類體;此時在聲明之後,定義之前,類是一個不完全類型,因此不能定義該類型的對象,只能用於定義指向該類型的指針及引用,或者用於聲明(不能是定義)使用該類型作為形參類型或返回類型的函數
-
類中不能具有自身類型的數據成員
-
如果對象的數據成員中包括動態分配資源的指針,則當對象賦值給同類對象時,只複製了指針值而沒有複製指針所指向的內容
-
對象的賦值只對其中的非靜態數據成員賦值,而不對成員函數賦值
-
普通函數與類的應用:
① 當函數形參是對象指針時,C++不能對對象指針進行任何隱式類型轉換
② 函數返回對象時,將其記憶體單元的所有內容複製到一個臨時對象中
③ 函數返回對象指針或引用,本質上返回的是對象的地址而不是它的存儲內容,因此不要返回局部對象的指針或引用,因為它在函數返回後是無效的
- 賦值兼容規則:指在需要基類對象的任何地方,都可以使用公有派生類的對象替代
① 派生類的對象可以賦值給基類對象
② 派生類對象可以初始化基類的引用
③ 派生類對象的地址可以賦給指向基類的指針
- 在公有繼承方式下,間接派生類對象可以直接調用基類中的公有成員函數,去訪問基類的私有數據成員
(三)聯編(綁定):
-
定義:將模組或函數合併在一起生成可執行程式碼的處理過程,同時對每個模組或函數分配記憶體空間,並對外部訪問也分配正確的記憶體地址
-
靜態聯編與動態聯編的區別:
① 靜態聯編 :指在編譯階段就將函數實現和函數調用關聯起來;它對函數的選擇是基於指向對象的指針或引用類型,通過對象名調用虛函數,在編譯階段就能確定調用的是哪一個類的虛函數
② 動態聯編:在程式執行的時候才將函數實現和函數調用關聯;一般情況下都是靜態聯編,涉及到多態和虛擬函數就必須使用動態聯編了;通過基類指針調用,在編譯階段無法通過語句本身來確定調用哪一個類的虛函數,只有在運行時指向一個對象後,才能確定調用時哪個類的虛函數
③ 動態聯編在編譯時只根據兼容性規則檢查它的合理性,即檢查它是否符合派生類對象的地址可以賦給基類的指針的條件
④ C++默認靜態綁定,當需要解釋為動態綁定時的辦法就是將基類中的函數聲明成虛函數。然而即使是調用虛函數,也只有在使用基類指針或引用的時候才動態綁定,其它還是靜態綁定的,並且基類的構造函數中對虛函數的調用不進行動態綁定
(四)記憶體
- C/C++記憶體分配函數:
① malloc 函數: void *malloc(unsigned int size)在記憶體的動態分配區域中分配一個長度為size的連續空間,如果分配成功,則返回所分配記憶體空間的首地址,否則返回NULL,申請的記憶體不會進行初始化。
② calloc 函數: void *calloc(unsigned int num, unsigned int size)按照所給的數據個數和數據類型所佔位元組數,分配一個 num * size 連續的空間。calloc申請記憶體空間後,會自動初始化記憶體空間為 0,但是malloc不會進行初始化,其記憶體空間存儲的是一些隨機數據。
③ realloc 函數: void *realloc(void *ptr, unsigned int size)動態分配一個長度為size的記憶體空間,並把記憶體空間的首地址賦值給ptr,把ptr記憶體空間調整為size。申請的記憶體空間不會進行初始化。
④ new是動態分配記憶體的運算符,自動計算需要分配的空間,在分配類類型的記憶體空間時,同時調用類的構造函數(malloc不會調用構造函數,free也不會調用析構函數),對記憶體空間進行初始化,即完成類的初始化工作。動態分配內置類型是否自動初始化取決於變數定義的位置,在函數體外定義的變數都初始化為0,在函數體內定義的內置類型變數都不進行初始化。
⑤ 不能說new 是分配記憶體空間的函數,該對於new的描述是錯誤的
-
C和C++語言是手動記憶體管理的,申請的記憶體需要手動釋放
-
使用New()和Delete()來創建和刪除對象,且該對象始終保持到delete運算時,即使程式運行結束它也不會自動釋放
-
基本類型的指針釋放由記憶體直接執行,所以delete和delete[]都能表達回收記憶體的意思。但自定義的對象由析構函數發起回收,所以每一個對象都要調用一次delete,所以才有有new[]對應delete[],但new[]用delete也行,只是為了規範最好不要
-
C++中只有構造函數和析構函數或其成員函數時所佔記憶體為1(不含虛函數),帶有虛函數的時候記憶體為4,普通繼承都是公用一張虛函數表,指針大小不增加。(當然有成員變數要加上成員變數的記憶體)
-
構造函數可以私有,但是此時直接定義類對象和new來定義對象都不再允許,因為new只管分配,不管構造,編譯器不會允許未構造的動態記憶體分配
-
對象大小 = 虛函數指針 + 所有非靜態數據成員大小 + 因對齊而多佔的位元組(所以成員函數(包括靜態和非靜態)和靜態數據成員都是不佔存儲空間的),因為是佔用全局的記憶體(因為是共享的),和全局變數分配的記憶體在同一個區域裡面,且類一般是棧區,而靜態變數是全局區域;成員函數(包括靜態和非靜態)和靜態數據成員都是不佔存儲空間的
-
C++對空類或者空結構體 ,對其sizeof操作時候,默認都是 1個位元組;多重繼承的空類的大小也是1
-
一個空類默認會生成構造函數,拷貝構造函數,賦值操作符(賦值函數),析構函數
-
在C中使用malloc(C中的動態分配記憶體函數)時不需要強制類型轉換,因為在C中從void*到其他類型的指針是自動隱式轉換的;
在C++中使用malloc時必須要強制類型轉換,否則會報錯
① malloc和free申請和釋放的虛擬記憶體,不是物理記憶體
② malloc的返回值是一個指針
③ malloc需要頭文件stdlib.h
- New:
① new創建對象不一定需要定義初始值,例如用new分配數組空間時不能指定初值,但一般都會賦初值
② new會調用構造函數
③ 對於一個new運算符聲明的指針,只能匹配一個delete使用,因此對一個指針可以使用多次delete是錯誤的
④ 用new運算動態分配得到的對象是無名的,它返回一個指向新對象的指針的值,故顯然new建立的動態對象是通過指針來引用的
⑤ 若記憶體不足會返回0值指針
-
free用來釋放記憶體空間,若要釋放一個對象還需調用其析構函數
-
new和delete運算符可以重載
-
記憶體知識點:
① 記憶體泄漏(Memory Leak)是指程式中已動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式運行速度減慢甚至系統崩潰等嚴重後果;也是記憶體中的數據出現丟失
(五)訪問許可權
-
類成員的訪問許可權中,Private只能被本類的成員函數和其友元函數訪問,不能被子類和其他函數訪問
-
關於不同類型繼承遵循訪問屬性只能縮小或等於的原則,即保護類型的成員函數就算是公有繼承後還是保護類型的成員函數
-
公有繼承的保護成員可以被類的方法訪問,不能被對象訪問,即可以被派生類成員訪問,但不能被派生類用戶訪問
-
派生類用戶(派生類對象)只有在public繼承下訪問基類的public成員
-
在派生類的函數中 能夠直接訪問基類的公有成員和保護成員 // 錯,因為派生類的靜態函數也不能訪問直接基類的成員變數且如果是間接繼承的基類的話要分情況討論
-
在C++中,來自class的繼承默認按照private繼承處理,來自struct的繼承默認按照public繼承處理
-
賦值兼容規則是指在需要基類對象的任何地方都可以使用公有派生類的對象來替代
(六)構造函數
- 構造函數:
① 若類A將其它類對象作為成員,且有父類,在定義類A的對象時,先調用父類的構造函數,在調用成員的構造函數,最後調用派生類的構造函數。可理解為「先父母,再客人,最後自己」(一定要注意構造函數和析構函數的調用順序,儘管基類的構造函數是虛函數但定義派生類對象都一定會調用它(總是寫錯這種題))
② 構造函數可以定義在類的內部或外部,但構造函數初始化列表只在構造函數的定義中而不是函數原型的聲明中指定
③ 在子類構造方法中調用父類的構造方法,super()必須寫在子類構造方法的第一行,否則編譯不通過(可以不寫,但是系統會自動添加上,此處強調的是必須在第一行)
④ 默認構造函數分為三類,默認普通構造函數(無形參);默認拷貝構造函數,形參為左值引用;默認移動構造函數(當類沒有自定義任何的拷貝控制成員,且每個非靜態變數都可移動時,會默認生成移動構造函數),形參為右值引用
⑤ 預設構造函數,拷貝構造函數,拷貝賦值函數,以及析構函數這四種成員函數被稱作特殊的成員函數。這4種成員函數不能被繼承(但不能被繼承的基類成員在派生類中都存在)
⑥ 一般沒有默認構造函數的成員,以及非靜態的const或引用類型的成員都必須在構造函數初始化列表中進行初始化
⑦ 數據成員被初始化的次序就是數據成員的聲明次序
⑧ 初始化式(定義構造函數時一個以冒號開始,接著是一個以逗號分隔的數據成員列表)可以是任意表達式
⑨ 必須在類的內部指定構造函數的默認參數
⑩ 默認構造函數由不帶參數的構造函數或所有形參均是默認參數的構造函數定義
⑪ 在一個類中定義了全部都是默認參數的構造函數後,不能再定義重載構造函數
⑫ 構造函數不需用戶調用,也不能被用戶調用
⑬ 一個類哪怕只定義了一個構造函數,編譯器也不會再生成默認構造函數
⑭ 派生類構造函數:
(1) 可以在一個類的構造函數中使用構造函數初始化列表顯式的初始化其子對象
(2) 構造函數調用順序:調用基類構造函數(按基類定義次序先後調用)->調用子對象構造函數(按聲明次序先後調用)->執行派生類初始化列表->執行派生類初始化函數體
(3)如果基類和子對象所屬類的定義中都沒有定義帶參數的構造函數,也不需要對派生類自己的數據成員初始化,則可以不必顯式的定義派生類構造函數,反之必須顯式定義派生類構造函數
(4) 如果在基類中沒有定義構造函數,或定義了沒有參數的構造函數,則定義派生類構造函數時可以不顯式的調用基類構造函數
(5) 如果基類中既定義了無參構造函數,又定義了有參構造函數,則在定義構造函數時既可顯式或不顯式調用基類構造函數
(6) 在調用派生類構造函數時,系統會自動先調用基類的無參數構造函數或默認構造函數
(7) 派生類的構造函數的成員初始化列表中,不能包含基類的子對象初始化(由基類的構造函數完成),可包含基類構造函數、派生類中子對象的初始化、派生類中一般數據成員的初始化
- 複製構造函數(拷貝初始化構造函數):
① 拷貝函數和構造函數沒有返回值
② 拷貝構造函數的參數可以使一個或多個,但左起第一個必須是自身類型的引用對象
③ 即使定義了其他除複製構造函數外的構造函數,編譯器也會自動生成一個預設的拷貝構造函數,但是不會是該類的保護成員
④ 拷貝初始化構造函數的作用是將一個已知對象的數據成員值拷貝給正在創建的另一個同類的對象
⑤ 拷貝構造函數的形參不限制為const,但是必須是一個引用,以傳地址方式傳遞參數,否則導致拷貝構造函數無窮的遞歸下去,指針也不行,本質還是傳值
⑥ 異常對象的初始化是通過拷貝初始化進行的
⑦ 一般形式為:類名(const 類名& obj)
- 轉換構造函數:
① 實現從其他類型到類類型的隱式轉換,只有一個形參,形參類型是需要轉換的數據類型
② 使用explicit關鍵字可禁止由構造函數定義的隱式轉換(指明該構造函數是顯式的),該關鍵字只能用於類內部的構造函數聲明上
③ 一般形式為:類名(const 指定數據類型& obj)
④ 該函數必須為成員函數,不能是友元類型
- 在什麼情況下系統會調用複製構造函數
① 用類的一個對象顯式或隱式去初始化另一個對象時
類名 對象1=對象2 // 複製初始化,調用複製構造函數(對象2已存在)
類名 對象1(對象2) // 直接初始化,調用與實參匹配的構造函數(對象2已存在)
② 函數實參按值傳遞對象時或函數返回對象時,函數形參是指針或引用類型時不調用
③ 根據元素初始化列表初始化數組元素時
- 構造函數中必須通過初始化列表來進行初始化情況
① 類成員為const類型
② 類成員為引用類型
③ 類成員為沒有默認構造函數的類類型
④ 如果類存在繼承關係,派生類必須在其初始化列表中調用基類的構造函數
-
編譯器生成的默認構造函數只負責初始化有默認構造函數的成員對象,其他的一律不負責(int等內置類型數據成員的初始化,這個該由程式設計師去做)
-
如果一個類中所有數據成員是公有的,則可以在定義對象時對數據成員進行初始化
(七)析構函數
-
一個類只能有1個析構函數
-
編譯器在為類對象分配棧空間時,會先檢查類的析構函數的訪問性,其實不光是析構函數,只要是非靜態的函數,編譯器都會進行檢查。如果類的析構函數是私有的,則編譯器不會在棧空間上為類對象分配記憶體。 因此, 將析構函數設為私有,類對象就無法建立在棧(靜態)上了,只能在堆上(動態new)分配類對象
-
析構函數沒有返回值,也沒有參數,沒有函數類型,因此不能被重載,但可以聲明成虛函數,因為析構函數一般不需要重載,把它設為虛函數就可以了,系統自動幫你析構的,且作用是為了多態發生時能夠完全析構
-
如果基類中的析構函數不是虛析構(沒有被virtual修飾),即使基類指針指向子類對象,析構的時候也不先調用子類的析構函數,而只調用基類的析構函數
-
析構函數可以在類內聲明,類外定義,但一個類中只能定義一個析構函數
-
定義析構函數時其名與類名完全相同-錯
-
如果基類的析構函數是虛函數,delete基類的指針時,不僅會調用基類的析構函數,還會調用派生類的析構函數,而調用的順序是先調用派生類的析構函數、然後調用基類的析構函數
如果基類的析構函數不是虛函數,那麼,像這種定義一個基類的指針,指向一個派生類的對象,當你delete這個基類的指針時,它僅調用基類的析構函數,並不調用派生類的析構函數
-
編譯器生成的析構函數稱為合成析構函數;它是按成員在類中的聲明次序的逆序撤銷成員的,且合成析構函數並不刪除指針成員所指向的對象(需要程式設計師顯式編寫析構函數去處理)
-
析構函數三法則:如果你自己寫了析構函數,就必須同時寫複製構造函數和賦值運算符重載,你不能只寫一個
-
先構造的後析構,後構造的先析構
-
在執行派生類的析構函數時,系統會自動調用基類的析構函數和子對象的析構函數,對基類和子對象進行清理
-
析構函數可以重寫(基類的析構函數要加virtual)
-
c++中析構函數是可以為純虛函數的,但是前提是:必須為其實現析構函數,否則派生類無法繼承,也無法編譯通過
-
析構函數沒有參數
(八)引用
- 引用的特點:
① 一定要初始化
② 引用對象要可以取地址 int &a=10//錯誤,int a=10;int &b=a;//正確
③ 引用不能改變
④ 引用變數使用過程中只能使用引用變數所引用的數值
⑤ 不能有空引用,引用必須與有效對象的記憶體單元關聯
⑥ 指定類型的引用不能初始化到其他類型的對象上
⑦ C++中不能建立引用數組和指向引用的指針,也不能建立引用的引用
⑧ 引用像指針,但這不是c++定義的,更多是編譯器的實現,所以不能說引用是指向變數的記憶體地址
- 使用”常引用”的原因:
① 保護傳遞給函數的數據不在函數中被改變
② 節省存儲空間
③ 提高程式的效率
- 指針和引用之間的聯繫:
① 引用可以表示指針
② 引用和指針都是實現多態效果的手段
③ 引用本身是目標變數的別名,對引用的操作就是對目標變數的操作
④ 但不能說引用和指針都是指向變數的記憶體地址;因為引用是一個指針常量,c++做了隱式的轉換,當你定義引用時,是定義了一個指針常量,使用引用時,c++會用*轉為原值,只是這是隱式的;至於都指向地址,是c++編譯器的定義
⑤ 引用聲明後,引用的對象不可改變,對象的值可以改變,指針可以隨時改變指向的對象以及對象的值
const int &q=x; //&q是對x變數的引用,但被定義為了常量,故q不再是變數,不能自增
(九)友元
-
友元機制-允許一個類將其非公有的成員的訪問權授予指定的函數或類
-
友元的正確使用能提高程式的運行效率,但破壞了類的封裝性和數據的隱蔽性,導致程式可維護性變差,因此一定要謹慎使用
-
友元包括友元函數和友元類
-
類A的友元類B和友元函數都可以訪問類A的私有成員
-
因為友元函數沒有當前對象,因此要定義單目運算符,就需要單參函數,要定義雙目運算符,就需要雙參函數
-
友元函數可以是另一個類的成員函數,稱為友元成員函數
-
定義後置「十+”或後置「–“運算是特例,它們是單目運算符,但需要兩個形參,頭一個形參是作用對象,後一個是int形參
-
用友元函數可以定義成員函數不能實現的運算,例如一些雙目運算符,右操作數是本類對象,而左操作數不是本類對象
-
一個類說明的友元函數不可以被派生類繼承
-
友元函數可以像普通函數一樣直接調用,不需要通過對象或指針
-
友元函數,不是類的成員函數,不能用類作用域符來標識該函數屬於哪個類
-
在C++中友元函數是獨立於當前類的外部函數,一個友元函數可以同時定義為兩個類的友元函數,友元函數即可以在類的內部,也可以在類的外部定義;在類的外面定義友元函數時不必加關鍵字friend
-
友元關係是單向的,不能傳遞或繼承
-
派生類的friend函數可以訪問派生類本身的一切變數,包括從父類繼承下來的protected域中的變數。但是對父類來說,他並不是friend的
-
因為友元函數不是類的成員,所以它不能直接訪問對象的數據成員(若選項中有其他明顯錯誤的選項則選其他選項),也不能通過this指針訪問對象的數據成員,它必須通過作為入口參數傳遞進來的對象名(或對象指針,對象引用)來訪問該對象的數據成員
-
由於一個友元函數可能屬於多個不同的類,所以在訪問時,必選加上對象名
-
友元函數不受訪問控制符的限制
(十)靜態
- 靜態數據成員:
① 靜態成員可以作為默認實參,非靜態成員則不能,因為它的值不能獨立於所屬的對象而使用
② static類變數是所有對象共有,其中一個對象將它值改變,其他對象得到的就是改變後的結果;是final修飾的變數不能修改
③ 因為靜態成員不能通過類構造函數進行初始化,因此靜態數據成員必須在類外初始化,靜態成員常量在類中初始化
④ 靜態變數可以用來計算類的實例變數(構造函數中對進行靜態變數+1操作,析構函數進行-1操作,便可計算現有對象數量)
⑤ static 修飾的變數只初始化一次, 當下一次執行到這一條語句的時候,直接跳過
⑥ 在外部變數的定義前面加上關鍵字static,就表示定義了一個外部靜態變數。外部靜態變數具有全局的作用域和全局的生存期,定義成static類型的外部變數將無法再使用extern將其作用範圍擴展到其他文件中,而是被限制在了本身所在的文件內,為程式的模組化、通用性提供方便
⑦ 不能通過類名調用類的非靜態成員函數
- 靜態成員函數:
① 靜態成員函數是類的成員函數,該函數不屬於該類申請的任何一個對象,而是所有該類成員共同共有的一個函數
② 靜態成員函數不能訪問非靜態數據成員,只能訪問靜態數據成員,故推出類方法中可以直接調用對象變數:錯誤
③ 靜態成員函數不能被聲明為const
④ 類的對象可以調用【非】靜態成員函數
⑤ 類的非靜態成員函數可以調用靜態成員函數,但反之不能
⑥ 沒有this指針
-
靜態成員不屬於對象,是類的共享成員,故在為對象分配的空間中不包括靜態數據成員所佔的空間
-
父類的static變數和函數在派生類中依然可用,但是受訪問性控制(比如,父類的private域中的就不可訪問),而且對static變數來說,派生類和父類中的static變數是共用空間的,這點在利用static變數進行引用計數的時候要特別注意。
static函數沒有「虛函數」一說。因為static函數實際上是「加上了訪問控制的全局函數」,全局函數哪來的什麼虛函數 -
static變數:
① 局部
② 全局
③ static函數(也叫內部函數)只能被本文件中的函數調用,而不能被同一程式其它文件中的函數調用
-
成員指針只應用於類的非靜態成員,由於靜態類成員不是任何對象的組成部分,所以靜態成員指針可用普通指針
-
可以使用作用域運算符「::」也可以使用對象成員引用運算符「.」或指針成員引用運算符「->」訪問靜態成員
-
類的靜態成員是所有類的實例共有的,存儲在全局(靜態)區,只此一份,不管繼承、實例化還是拷貝都是一份
-
初始化:
① 靜態常量數據成員可以在類內初始化(即類內聲明的同時初始化),也可以在類外,即類的實現文件中初始化,不能在構造函數中初始化,也不能在構造函數的初始化列表中初始化;
② 靜態非常量數據成員只能在類外,即類的實現文件中初始化,也不能在構造函數中初始化,不能在構造函數的初始化列表中初始化;
③ 非靜態的常量數據成員不能在類內初始化,也不能在構造函數中初始化,而只能且必須在構造函數的初始化列表中初始化;
④ 非靜態的非常量數據成員不能在類內初始化,可以在構造函數中初始化,也可以在構造函數的初始化列表中初始化;
⑤ 即:
(十一)重載
① 函數名相同
② 參數必須不同(個數或類型或順序)
③ 返回值類型可以相同也可以不同;因為函數並不會總是有返回值,函數的返回類型不能作為函數重載的判斷依據
-
函數重載是指在同一作用域內(在同一個類或名字空間中) ,可以有一組具有相同函數名,不同參數列表的函數,這組函數被稱為重載函數
-
只有當修飾的const為底層const而非頂層const時才可以區分,也就是說const必須修飾指針指向的對象而非指針本身(即const寫在後面)
-
子類重新定義父類虛函數的方法叫做覆寫不是重載
-
C++語言規定,運算符「=」、「[]」、「()」、「->」以及所有的類型轉換運算符只能作為成員函數重載
-
友元函數重載時,參數列表為1,說明是1元,為2說明是2元
成員函數重載時,參數列表為空,是一元,參數列表是1,為2元 -
聲明成員函數的多個重載版本或指定成員函數的默認參數只能在類內部進行
(十二)重寫
-
重寫就叫覆蓋。如果沒有virtual就是隱藏
-
被重寫的函數不能是static的。必須是virtual的
-
重寫函數必須有相同的類型,名稱和參數列表
-
重寫函數的訪問修飾符可以不同。儘管父類的virtual方法是private的,派生類中重寫改寫為public,protected也是可以的
-
重寫要求基類函數為虛函數,且基類函數和派生類函數函數名,參數等都相同
-
方法的覆蓋對返回值的要求是:小於等於父類的返回值
-
方法的覆蓋對訪問要求是:大於等於父類的訪問許可權
-
重定義(隱藏)是指派生類的函數屏蔽了與其同名的基類函數,規則如下:
① 如果子類的函數與父類的名稱相同,但是參數不同,父類函數被隱藏(重定義)
② 如果子類函數與父類函數的名稱相同&&參數也相同&&但是父類函數沒有virtual,父類函數被隱藏
③ 如果子類函數與父類函數的名稱相同&&參數也相同&&但是父類函數有virtual,父類函數被覆蓋(重寫)
-
fun(int)是類Test的公有成員函數,p是指向成員函數fun()的指針,則調用fun函數正確寫法應為p=&Test::fun
-
如果在派生類中聲明了與基類成員函數同名的新函數,即使函數的參數不同,從基類繼承的同名函數的所有重載形式也都會被覆蓋
(十三)虛函數、虛基類和虛繼承
-
virtual只在類體中使用
-
虛函數
① 虛函數必須是類的一個成員函數,不能使友元函數,也不能是靜態的成員函數
② C++規定構造函數不能是虛函數,而析構函數可以是虛函數。 這樣死記硬背也是一種辦法,但是不推薦。可以這麼理解:假設構造函數為虛函數,而虛函數的調用需要虛表,虛表又由構造函數建立。這樣就矛盾了。 就像兒子生了父親一樣,矛盾。所以,構造函數不能是虛函數
③ 純虛函數是可以有函數體的,當我們希望基類不能產生對象,然而又希望將一些公用程式碼放在基類時,可以使用純虛函數,並為純虛函數定義函數體,只是純虛函數函數體必須定義在類的外部
④ 虛函數能夠被派生類繼承
⑤ 只要父類成員函數定義成虛函數,無論子類是否覆蓋了父類的虛函數,調用父類虛函數時都會找到子類並子類相應的函數
⑥ 虛函數不可以內聯,因為虛函數是在運行期的時候確定具體調用的函數,內聯是在編譯期的時候進行程式碼展開,兩者衝突
⑦ 當編譯器編譯含有虛函數的類時,為該類增加一個指向虛函數表(相當於一個指針數組)的指針
⑧ 派生類能繼承基類的虛函數表,而且只要是和基類同名(參數也相同)的成員函數,無論是否使用virtual聲明,它們都自動成為虛函數
⑨ 當用基類指針或引用對虛函數進行訪問時,系統將根據運行時指針或引用所指向的或引用的實際對象來確定調用對象所在類的虛函數版本
⑩ 虛函數不可以重載
⑪ 在父類的構造函數和析構函數中都不能調用純虛函數(不能以任何方式調用)
⑫ 在構造函數執行完以後虛函數表才能正確初始化,同理在析構函數中也不能調用虛函數,因為此時虛函數表已經被銷毀
⑬ 下面情況調用不會出現多態性:
(1) 通過對象調用虛函數不會出現多態(通過指針或者引用才會有多態性,因為動態綁定(多態)只有在指針和引用時才有效,其他情況下無效)
(2) 在構造函數裡面調用虛函數不會出現多態
(3) 指定命名域調用不會出現多態
⑭ 設置虛函數需要注意以下幾個方面:
(1) 只有類的成員函數才能說明為虛函數。虛函數的目的是為了實現多態,多態和集成有關,所以聲明一個非成員函數為虛函數沒有任何意義。
(2) 靜態成員函數不能是虛函數。靜態成員函數對於每一個類只有一份程式碼,所有的對象共享這份程式碼,它不歸某個對象所有,所以沒有動態綁定的必要性。不能被繼承,只屬於該類。
(3) 內聯函數不能為虛函數。內聯函數在程式編譯的時候展開,在函數調用處進行替換。虛函數在運行時進行動態綁定的。
(4) 構造函數不能為虛函數。虛函數表在構造函數調用後才建立,因而構造函數不可能成為虛函數。虛函數的調用需要虛函數表指針,而該指針存放在對象的記憶體空間中;若構造函數聲明為虛函數,那麼由於對象還未創建,還沒有記憶體空間,更沒有虛函數表地址用來調用函數。
(5) 析構函數可以是虛函數,而且通常聲明為虛函數。
(6) 友元函數:友元函數不能被繼承,所以不存在虛函數
⑮ 基類的成員不能直接訪問派生類的成員,但可以通過虛函數間接訪問派生類的成員
⑯ virtual 函數是動態綁定,而預設參數值卻是靜態綁定。 意思是你可能會在「調用一個定義於派生類內的virtual函數」的同時,卻使用基類為它所指定的預設參數值。
結論:絕不重新定義繼承而來的預設參數值!
⑰ 虛析構函數:
(1) 若基類的析構函數聲明為虛函數,則由該基類所派生的所有派生類的析構函數也都自動成為虛函數
- 虛基類:
① 虛基類並不是在聲明基類時聲明的,而是在聲明派生類時,指定繼承方式時聲明的
② 設置虛基類的目的:消除二義性
③ 為保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中聲明為虛基類,否則仍然會出現對基類的多次繼承,C++編譯系統只執行最後的派生類對虛基類構造函數的調用,而忽略基類的其他派生類,這樣就保證了虛基類的數據成員不會被多次初始化
④ 如果虛基類中定義了帶參數的構造函數,而且沒有定義默認構造函數,則在其所有直接派生類和間接派生類中都要通過構造函數的初始化表對虛基類進行初始化
⑤ 虛基類的構造函數先於非虛基類的構造函數執行
-
封裝是面向對象編程中的把數據和操作數據的函數綁定在一起的一個概念(並不是單純將數據程式碼連接起來,是數據和操作數據的函數.),這樣能避免受到外界的干擾和誤用,從而確保了安全
-
在派生列表中,同一基類只能出現一次,但實際上派生類可以多次繼承同一個類。派生類可以通過兩個直接基類分別繼承自同一間接基類,也可以直接繼承某個基類,再通過另一個基類再次繼承該類。但是,如果某個類派生過程中出現多次,則派生類中將包含該類的多個子對象,這種默認情況在很多有特殊要求的類中是行不通的。虛繼承就是為了應對這一情況而產生,虛繼承的目的是令某個類做出聲明,承諾願意共享其基類。這樣不論虛基類在繼承體系中出現多少次,派生類中都只包含唯一一個共享的虛基類子對象
(十四)抽象類
- 抽象類有一下幾個特點:
① 抽象類只能用作其他類的基類,不能建立抽象類對象。
② 抽象類不能用作參數類型、函數返回類型或顯式轉換的類型。
③ 可以定義指向抽象類的指針和引用,此指針可以指向它的派生類,進而實現多態性。
- 抽象:
① 抽象類指針可以指向不同的派生類
② 抽象類只能用作其他類的基類,不能定義抽象類的對象。
③ 抽象類不能用於參數類型、函數返回值或顯示轉換的類型
④ 抽象類可以定義抽象類的指針和引用,此指針可以指向它的派生類,進而實現多態性。
⑤ 抽象類不可以為final的
⑥ 如果一個非抽象類從抽象類中派生,一定要通過覆蓋來實現繼承的抽象成員,因為如果一個非抽象類從抽象類中派生,不通過覆蓋來實現繼承的抽象成員,此時,派生類也會是抽象類
⑦ 抽象類就是必須要被覆蓋的 所以才不能和final一起用
(十五)final
- final的作用:
① 修飾變數,使用final修飾基本類型的變數,一旦對該變數賦值之後,就不能重新賦值了。但是對於引用類型變數,他保存的只是引用,final只能保證引用類型變數所引用的地址不改變,但不保證這個對象不改變,這個對象完全可以發生改變
② 修飾方法,方法不可被重寫,但是還是可以重載
③ 修飾類,類不可繼承
(十六)類模板
1.類模板的使用實際上是將類模板實例化成一個具體的類(不能寫模板類),而模板類是類模板實例化後的一個產物
- C++中為什麼用模板類的原因:
① 可用來創建動態增長和減小的數據結構
② 它是類型無關的,因此具有很高的可復用性。
③ 它在編譯時而不是運行時檢查數據類型,保證了類型安全
④ 它是平台無關的,可移植性
⑤ 可用於基本數據類型
- 函數模板與類模板的區別:
① 函數模板的實例化是由編譯程式處理函數調用時自動完成的;
② 類模板的實例化是由程式設計師在程式中顯式的指定;
③ 函數模板針對參數類型不同和返回值類型不同的函數;
④ 類模板針對數據成員、成員函數和繼承的基類類型不同的類
⑤ 一般函數模板習慣用typename作為類型形參之前的關鍵字,類模板習慣用class
- 在用類模板定義對象時,必須為模板形參顯式指定類型實參
- 類模板形參還可以是非類型(普通類型)形參,在定義對象時,必須為每個非類型形參提供常量表達式以供使用
- 類模板形參也可設置默認值
- 類模板的成員函數都是模板函數
- 關於模板和繼承的敘述:
① 模板和繼承都可以派生出一個類系
② 從類系的成員看,繼承類系的成員比模板類系的成員(類模板可作為基類)較為穩定
③ 從動態性能看,模板類系比繼承類系(虛函數實現動態多態性)具有更多的動態特性
(十七)成員指針
-
數據成員指針一般形式:數據成員類型 類名::*指針變數名 = 成員地址初值
-
成員函數指針:
① 必須保證三個方面與它所指函數的類型相匹配
(1) 函數形參的類型和數目,包括成員是否為const
(2) 返回類型
(3) 所屬類的類型
② 一般形式:返回類型(類名::*指針變數名)(形式參數列表)【const】=成員地址初值
-
若指向成員函數的指針不是類成員,則正確指向的格式為:指針=類名::成員函數名
-
使用類成員指針:
① 通過對象成員指針引用」.「可以從類對象或引用及成員指針間接訪問類成員,或者通過指針成員指針引用」->「可以從指向類對象的指針及成員指針訪問類成員
② 對象成員指針引用運算符左邊的運算對象必須是類類型的對象,指針成員指針引用運算符左邊的運算對象必須是類類型的指針,兩個運算符的右邊運算對象必須是成員指針
- 賦值指向成員函數的指針:
① 如果是類的靜態成員函數,那麼使用函數指針和普通函數指針沒區別,使用方法一樣
② 如果是類的非靜態成員函數,那麼使用函數指針需要加一個類限制一下
③ 成員指針只應用於類的非靜態成員,由於靜態類成員不是任何對象的組成部分,所以靜態成員指針可用普通指針
(十八)常對象、常數據成員、常成員函數、常指針、常引用
- 常成員函數:
① 常函數定義:數據類型 類名::函數名()const; // 在聲明和定義函數時都要有const關鍵字
② const成員函數表示該成員函數只能讀類數據成員,而不能修改類成員數據。定義const成員函數時,把const關鍵字放在函數的參數表和函數體之間。
為什麼不將const放在函數聲明前呢?因為這樣做意味著函數的返回值是常量(只讀),意義完全不同。無返回值就返回值類型為void
③ 常成員函數可以訪問const數據成員,也可以訪問非const數據成員
④ 常成員函數不能調用另一個非常成員函數
⑤ const放在函數前分為兩種情況:
(1) 返回指針,此時該對象只能立即複製給新建的const char,而不能是char,意在強調值被保存在常指針中
(2) 返回一個值,此時const無意義,應當避免(即本文檔函數使用的第六條)
- 常對象:
① 常對象定義時const寫在類名的前或後都行
② 常對象中的數據成員都是const的,因此必須要有初值,且無論什麼情況其數據成員都不能修改
③ 除合成默認構造函數或默認析構函數外,常對象也不能調用非const型的成員函數
④ 可以修改常對象中由mutable聲明的數據成員
⑤ 常對象數據成員只能被常成員函數訪問,不能被非常成員函數訪問
⑥ 只能用指向常對象的指針變數指向
- 常數據成員:
① const寫在數據成員類型前
② 非靜態的常數據成員只能通過構造函數初始化列表初始化
③ 可以被【非】const成員函數訪問
④ 函數形參前加const關鍵字是為了提高函數的可維護性
- 指向對象的常指針:
① const在指針變數名前*後
② 對象的常指針必須在定義時初始化,因為指針的指向不能改變
- 指向常對象的指針:
① const寫在類名前
② 即使指向一個非const的對象,其指向的對象依舊不能通過指針來改變
- 對象的常引用:
① const寫在類名前
(十九) 組合
- 區分組合和繼承:
① 繼承:若在邏輯上B 是一種A (is a kind of),則允許B 繼承A 的功能,它們之間就是Is-A 關係。如男人(Man)是人(Human)的一種,女人(Woman)是人的一種。那麼類Man 可以從類Human 派生,類Woman也可以從類Human 派生(男人是人,女人是人);在繼承體系中,派生類對象是可以取代基類對象的
② 組合:若在邏輯上A 是B 的「一部分」(a part of),則不允許B 繼承A 的功能,而是要用A和其它東西組合出B,它們之間就是「Has-A(有)關係」。例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是頭(Head)的一部分,所以類Head 應該由類Eye、Nose、Mouth、Ear 組合而成,不是派生而成
③ 繼承是縱向的,組合是橫向的
(二十)嵌套類
- 嵌套類:
① 嵌套類是獨立的類,基本上與它們的外圍類不相關
② 嵌套類(內嵌類)的名字只在其外圍類(包容類)的作用域中可見
③ 外圍類對嵌套類的成員沒有特殊訪問權,並且嵌套類對其外圍類也沒有特殊訪問權
④ 嵌套類就像外圍類的成員一樣,具有與其他成員一樣的訪問限制屬性等
⑤ 嵌套類可以直接引用外圍類的靜態成員、類型名和枚舉成員,當然,引用外圍類作用域外的類型名或靜態成員需要作用域運算符」::「
⑥ 在外圍類域外定義的嵌套類對象其作用域不屬於外圍類
⑦ 外圍類與嵌套類之間是一個主從關係
⑧ 使用嵌套類的目的是隱藏類名,減少全局標識符
⑨ 嵌套類中可以說明靜態成員
⑩ 嵌套類也是局部類,必須遵循局部類的規定,嵌套類的成員也必須定義在嵌套類內部
(二十一)局部類
- 局部類:
① 局部類所有成員(包括函數)必須完全定義在類體內
② 局部類只能訪問外圍作用域中定義的類型名(即局部類的作用域局限於定義它的成員函數內部)、靜態變數和枚舉成員,不能使用定義該類的函數中的變數
③ 外圍函數對局部類的私有成員沒有特殊訪問權,當然 局部類可以將外圍函數設為友元
④ 在局部類中不能聲明靜態數據成員
⑤ 局部類的成員函數只能使用隱式內聯方式實現
⑥ 局部類也是可以嵌套的,嵌套類的定義可以在局部類的之外,但是其定義要和局部類在一個作用域內
(二十二)this指針
- this指針:
① this指針存在的目的是保證每個對象擁有自己的數據成員,但共享處理這些數據成員的程式碼
② this指針並不是對象的一部分,this指針所佔的記憶體大小是不會反應在sizeof操作符上的
③ this指針的類型取決於使用this指針的成員函數類型以及對象類型
(1) 假如this指針所在類的類型是Stu_Info_Mange類型,並且如果成員函數是非常量的,則this的類型是:Stu_Info_Mange * const 類型,即一個指向非const Stu_Info_Mange對象的常量(const)指針
(2) 假如成員函數是常量類型,則this指針的類型是一個指向constStu_Info_Mange對象的常量(const)指針
(3) this指針是const限定的,故既不允許改變this指針的指向,也不允許改變this指向的內容
④ this只能在成員函數中使用。全局函數,靜態函數都不能使用this(靜態與非靜態成員函數之間有一個主要的區別就是靜態成員函數沒有this指針)。實際上,成員函數默認第一個參數為T* const register this
⑤ this在成員函數的開始執行前構造的,在成員的執行結束後清除
⑥ this指針只有在成員函數中才有定義。因此,你獲得一個對象後,也不能通過對象使用this指針。所以,我們也無法知道一個對象的this指針的位置(只有在成員函數里才有this指針的位置)。當然,在成員函數里,你是可以知道this指針的位置的(可以&this獲得),也可以直接使用的
⑦ 在C++中,根據this指針類型識別類層次中不同類定義的虛函數版本,因為this是表示當前對象函數的指針
⑧ 什麼時候會用到this指針:
(1) 在類的非靜態成員函數中返回類對象本身時,直接使用return *this;
(2) 當參數與數據成員名相同時,例this->n=n // 不能寫成n=n