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