1. 程式人生 > >C++ 重點知識梳理(四) -------- 面向物件

C++ 重點知識梳理(四) -------- 面向物件

五、面向物件

5.1 面向物件的三大特性

三大特性:封裝,繼承,多型  

  1. 封裝:封裝是實現面向物件程式設計的第一步,封裝就是將資料或函式等集合在一個個的單元中(我們稱之為類)。封裝的意義在於保護或者防止程式碼(資料)被我們無意中破壞。
  2. 繼承:繼承主要實現重用程式碼,節省開發時間。子類可以繼承父類的一些東西。
  3. 多型:同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。分為編譯時多型和執行時多型。

5.2 函式過載和運算子過載

問:函式過載的依據?

答:

  1. 引數個數
  2. 引數型別
  3. const方法與非const方法構成過載

問:運算子過載的限制?

答:

  1. 被過載的運算子,至少有一個運算元是使用者自定義型別,也就是說不能過載C++語言的標準運算
  2. 過載的運算子的句法規則不可以改變,運算元、結合性和優先順序無法更改。以前是幾元現在就是幾元;該是左結合還是左結合;優先順序無法更改。
  3. 不能自定義運算子,不能建立新的運算子。
  4. 不能過載的運算子有:
    1. 成員訪問運算子 . 
    2. 成員指標運算子 .* 
    3. 作用域解析運算子 ::
    4. 條件運算子 ?:
    5. sizeof 
    6. typeid
    7. 四個型別轉換運算子
      1. const_cast
      2. static_cast
      3. dynamic_cast
      4. reinterpret_cast
  5. 只能通過成員函式過載,而不能通過友元過載的運算子:
    1. 賦值運算子 = 
    2. 函式呼叫運算子 () 
    3. 下標運算子 []
    4. 通過指標訪問成員運算子 ->
  6. 只能通過友元過載,不能通過成員函式過載的情況:
    1. 雙目運算子最好用友元過載,單目運算子最好用成員函式過載
    2. 若運算子所需的運算元(尤其是第一個運算元)希望有隱式型別轉換,則只能選用友元函式
    3. 左運算元是不同類的物件或者內部型別,比如ostream, istream, int, float等
    4. 當需要過載運算子具有可交換性時,選擇過載為友元函式
  7. 對返回型別沒有限制,可以是void或者其他型別
  8. 過載一元運算子需要注意,由於一元運算子沒有引數,字首和字尾無法區分,所以需要加一個啞元(dummy),啞元永遠用不上,如果有啞元,則是字尾形式,否則,就是字首。

5.3 哪些成員無法被繼承?

  1. 無法被繼承的有
    1. 建構函式
    2. 解構函式
    3. 賦值運算子
    4. 友元函式
  2. 可以被繼承的有
    1. 靜態成員
    2. 靜態方法
    3. 非靜態成員
    4. 非靜態方法(無論是private\public\protected,只是private的繼承了也無法訪問)
    5. 虛表指標

5.4 定義預設建構函式的兩種方法?

  1. 給已有的建構函式中的一個的所有引數加上預設值
  2. 通過方法過載定義一個無引數建構函式

注意:

  1. 隱式呼叫預設建構函式不要加括號(), 會被編譯器解釋為函式宣告。

5.5 呼叫非預設建構函式的三種方法?

  1. Foo f(...); // 隱式呼叫
  2. Foo f = Foo(...) ;// 顯式呼叫
  3. Foo* f = new Foo(); // 顯式呼叫 

5.6 由編譯器生成的6個成員函式?

注意:對於空類,不會生成任何成員函式,只會生成一個位元組的佔位符。

  1. 預設建構函式
  2. 解構函式
  3. 複製建構函式
  4. 賦值運算子
  5. 取地址運算子
  6. 取地址運算子 const版本

5.7 友元的三種實現方式

  1. 友元函式
  2. 友元類
  3. 友元成員函式

5.8 為什麼基類的解構函式為什麼要宣告為虛擬函式?

為了能在多型情況下準確呼叫派生類的解構函式。如果基類的解構函式非虛擬函式,則用基類指標或引用引用派生類進行析構時,只會呼叫基類的解構函式;如果是虛解構函式,則會依次呼叫派生類的析構和基類的析構。(基類的析構是一定會呼叫的,無論是否為虛)。

5.9 為什麼建構函式不可以是虛擬函式?

  1. 虛擬函式在執行期決定函式呼叫,而在構造一個物件時,由於物件還未構造成功,編譯器無法確定物件的實際型別,繼而無法決定呼叫哪一個建構函式。
  2. 虛擬函式的執行依賴於虛擬函式表,而虛擬函式表在建構函式中進行初始化工作,即初始化 vptr,讓它指向正確的虛擬函式表,而在構造期間,虛擬函式表還沒有初始化,所以無法決定呼叫哪個建構函式。

5.10 解構函式什麼時候宣告為私有?什麼時候不能宣告為私有?

  1. 私有解構函式可以使得物件只在堆上構造。在棧上建立的物件要求建構函式和解構函式必須都是公有的,否則編譯器報錯“解構函式不可訪問”;而堆物件由程式設計師建立和刪除,可以把解構函式宣告為私有的。由於delete會呼叫解構函式,而私有的析構無法被訪問,編譯器報錯,此時通過增加一個destroy()方法,在方法內呼叫解構函式來釋放物件:
    • void destroy() 
    • {
    • delete this; 
    • }
  2. 解構函式不能宣告為私有的情況:基類的解構函式不能宣告為私有,因為要在派生類的解構函式中被隱式呼叫。

5.11 建構函式什麼時候宣告為私有?什麼時候不能宣告為私有?

  1. 單例模式。
  2. 基類的建構函式不能宣告為私有,因為要在派生類的建構函式中被隱式呼叫。如果在派生類的建構函式中沒有顯式呼叫基類的構造,則會呼叫基類的預設建構函式。

5.12 不能宣告為虛擬函式的成員函式

建構函式:

首先明確一點,在編譯期間編譯器完成了虛表的建立,而虛指標在建構函式期間被初始化。

如果建構函式是虛擬函式,那必然需要通過虛指標來找到虛建構函式的入口地址,但是這個時候我們還沒有把虛指標初始化。因此,建構函式不能是虛擬函式。

行內函數:

編譯期行內函數在呼叫處被展開,而虛擬函式在執行時才能被確定具體呼叫哪個類的虛擬函式。行內函數體現的是編譯期機制,而虛擬函式體現的是執行期機制。

靜態成員函式:

靜態成員函式和類有關,即使沒有生成一個例項物件,也可以呼叫類的靜態成員函式。而虛擬函式的呼叫和虛指標有關,虛指標存在於一個類的例項物件中,如果靜態成員函式被宣告成虛擬函式,那麼呼叫成員靜態函式時又如何訪問虛指標呢。總之可以這麼理解,靜態成員函式與類有關,而虛擬函式與類的例項物件有關。

非成員函式:

虛擬函式的目的是為了實現多型,多型和繼承有關。所以宣告一個非成員函式為虛擬函式沒有任何意義。

5.13 虛擬函式機制以及記憶體分佈

虛擬函式機制涉及的指標和表有:

  • 虛擬函式表指標 vfptr和虛擬函式表 vftable
  • 虛繼承下還涉及 虛基類表指標 vbptr和虛基類表 vbtable

虛擬函式的實現過程:

1.編譯器為每個含有虛擬函式的類或者從此類派生的類建立一個虛擬函式表vftable, 儲存此類所有虛擬函式的地址,並增加一個隱藏成員虛擬函式表指標vfptr放在所有資料成員之前。在建立類的物件時,在建構函式內部對虛擬函式表指標進行初始化,指向之前建立的虛擬函式表。

2. 單繼承情況下,派生類會繼承基類所有的資料成員和虛擬函式表指標,並由編譯器生成虛擬函式表,在建立派生類例項時,將虛擬函式表指標指向新的,屬於派生類的虛擬函式表。

3. 多重繼承情況下,會有多個虛擬函式表,幾重繼承,就會有幾個虛擬函式表。這些表按照派生的順序依次排列,如果派生類改寫了基類的虛擬函式,那麼就會用派生類自己的虛擬函式覆蓋虛擬函式表的相應的位置,如果派生類有新的虛擬函式,那麼就新增到第一個虛擬函式表的末尾

4. 虛繼承情況下,會再建立一個虛基類表和一個虛基類表指標,也就是說,編譯器會增加兩個指標,一個是虛基類表指標,指向虛基類表,儲存了所有繼承過來的虛基類在記憶體中的地址(偏移量);另一個是繼承過來的虛擬函式表指標,儲存了虛擬函式的地址。如果派生類有新的虛擬函式,那麼就再增加一個虛擬函式表指標,指向一個新的虛擬函式表,儲存了派生類新的虛擬函式的地址。

5. 虛基類部分會在C++繼承層次中只有一份。所有由虛基類派生的類都持有一個虛基類表指標,指向一個虛基類表,表裡面儲存了所有它繼承的虛基類部分的地址。虛基類部分有一個虛擬函式表指標,指向虛擬函式表。

5.14 class 與 struct的區別

  1. class預設的繼承方式為private, struct 預設繼承方式為public 
  2. class的成員訪問預設為private, struct預設為public 

5.15 過載、重寫(覆蓋)與隱藏(重定義)的關係

過載  override 

重寫(覆蓋)override 

隱藏  hide 

  1. 過載。函式名相同,引數個數、型別不同,或者用const過載。是同一個類中方法之間的關係,是水平關係。
  2. 重寫。派生類重新定義基類中有相同名稱和引數的虛擬函式,要求引數列表必須相同。方法在基類和派生中的訪問限制可以不同。
  3. 隱藏。派生類重新定義基類中有相同名稱的函式(引數列表可以不同)會把其他基類的同名方法隱藏起來,無法被派生類呼叫。

5.16 哪些情況下方法可以不寫定義?

  1. 純虛方法
  2. 非虛方法

所以,非純虛的虛方法也就是普通的虛方法必須寫定義,哪怕是空的,因為要生成虛擬函式表,沒有方法定義就沒有方法地址。

5.17 派生類可以不實現虛基類的純虛方法,派生類也成了抽象類。

5.18 三種繼承方式(public, private, protected)的區別?

  1. 公有繼承(public): 基類成員對其物件的可見性與一般類及其物件的可見性相同,public成員可見,protected和private成員不可見,基類成員對派生類的可見性對派生類來說,基類的public和protected成員可見:基類的public成員和protected成員作為派生類的成員時,它們都保持原有狀態;基類的private成員依舊是private,派生類不可訪問基類中的private成員。 基類成員對派生類物件的可見性對派生類物件來說,基類的public成員是可見的,其他成員是不可見的。 所以,在公有繼承時,派生類的物件可以訪問基類中的public成員,派生類的成員方法可以訪問基類中的public成員和protected成員。
  2. 私有繼承(private) 基類成員對其物件的可見性與一般類及其物件的可見性相同,public成員可見,其他成員不可見,基類成員對派生類的可見性對派生類來說,基類的public成員和protected成員是可見的:基類的public成員和protected成員都作為派生類的private成員,並且不能被這個派生類的子類所訪問;基類的私有成員是不可見的:派生類不可訪問基類中的private成員,基類成員對派生類物件的可見性對派生類物件來說,基類的所有成員都是不可見的,所以在私有繼承時,基類的成員只能由直接派生類訪問,無法再往下繼承。
  3. 保護繼承(protected) 保護繼承與私有繼承相似,基類成員對其物件的可見性與一般類及其物件的可見性相同,public成員可見,其他成員不可見,基類成員對派生類的可見性,對派生類來說,基類的public和protected成員是可見的:基類的public成員和protected成員都作為派生類的protected成員,並且不能被這個派生類的子類所訪問;基類的private成員是不可見的:派生類不可訪問基類中的private成員。基類成員對派生類物件的可見性對派生類物件來說,基類的所有成員都是不可見的。所以,在保護繼承時,基類的成員也只能由直接派生類訪問,而無法再向下繼承。C++支援多重繼承。多重繼承是一個類從多個基類派生而來的能力。派生類實際上獲取了所有基類的特性。當一個類 是兩個或多個基類的派生類時,派生類的建構函式必須啟用所有基類的建構函式,並把相應的引數傳遞給它們 。

5.19 如果賦值建構函式引數不是傳引用而是傳值會有什麼問題?

如果不是傳引用,會造成棧溢位。因為如果是Foo(Foo f)的形式,實參初始化形參的時候也會呼叫複製建構函式,造成死迴圈。所以,複製建構函式一定要傳引用:

Foo(Foo& f); 

5.20 如何實現只能動態分配類物件,不能定義類物件?

即只能將物件創建於堆上,不能創建於棧上。需要把建構函式和解構函式設為protected,派生類可以訪問,外部無法訪問。同時建立create和destroy函式,在內部呼叫構造和析構,用於建立和刪除物件。其中create設為static,使用類名訪問。

 class A{
 protected:
 	A(){};
 	~A(){};
 public:
 	static A* creat(){
 		return new A();
 	}
 	void destroy(){
 		delete this;
 	}
 };
 int main()
 {
 	A* a = A::creat();

 	a->destroy();
 }

5.21 如何實現只能在棧上建立物件?不能在堆上建立物件?

在堆上建立物件的唯一方法是使用new關鍵字,所以,只需要禁用new關鍵字就可以了。將operator new 設為私有的, 外部不可訪問。

 class A
 {
 private:
 	void* operator new(size_t t){}     // 注意函式的第一個引數和返回值都是固定的
 	void operator delete(void* ptr){} // 過載了new就需要過載delete
 public:
 	A(){}
 	~A(){}
 };

5.22 必須在建構函式初始化式裡進行初始化的資料成員有哪些?

  1. 常量成員,因為常量只能初始化不能賦值,所以必須放在初始化列表裡面
  2. 引用型別,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裡面
  3. 沒有預設建構函式的類型別,因為使用初始化列表可以不必呼叫預設建構函式來初始化,而是直接呼叫拷貝建構函式初始化

5.23 抽象類和介面的區別?

抽象類是包含純虛擬函式的類 C++中的介面是指只包含純虛擬函式的抽象類,不能被例項化。 一個類可以實現多個介面(多重繼承)

5.24 虛基類和虛繼承,虛基指標和虛基表

虛基類是使用virtual繼承的公共基類。虛繼承使得在記憶體中只有基類成員的一份拷貝。虛繼承消除了歧義,如果B,C,繼承於A,A中有一個公有成員 i,D繼承於B,C,此時D無法訪問 i,因為會有歧義,不知道是B還是C的,此時使用虛繼承可以解決,讓B,C以虛繼承方式繼承A,這樣就消除了歧義。底層實現原理:底層實現原理與編譯器相關,一般通過虛基類指標實現,即各物件中只儲存一份父類的物件,多繼承時通過虛基類指標引用該公共物件,從而避免菱形繼承中的二義性問題。 

虛基類的初始化與一般多繼承的初始化在語法上是一樣的,但建構函式的呼叫次序不同。派生類建構函式的呼叫次序有三個原則:

(1)虛基類的建構函式在非虛基類之前呼叫; (2)若同一層次中包含多個虛基類,這些虛基類的建構函式按它們說明的次序呼叫; (3)若虛基類由非虛基類派生而來,則仍先呼叫基類建構函式,再呼叫派生類的建構函式。

虛繼承的派生類會增加一個隱藏成員虛基指標vbPtr指向虛基表vbTable。

5.25 建構函式和解構函式中可以呼叫呼叫虛擬函式嗎?

可以,虛擬函式底層實現原理(但是最好不要在構造和解構函式中呼叫) 可以,但是沒有動態繫結的效果,父類建構函式中呼叫的仍然是父類版本的函式,子類中呼叫的仍然是子類版本的函式。 effictive c++第九條,絕不在構造和析構過程中呼叫virtual,因為建構函式中的base的虛擬函式不會下降到derived上。而是直接呼叫base類的虛擬函式。

5.26 建構函式和解構函式呼叫順序?

  1. 先呼叫基類建構函式
  2. 在呼叫成員類建構函式
  3. 最後呼叫本身的建構函式
  4. 析構順序相反

5.27 動態繫結如何實現?

C++ 中,通過基類的引用或指標呼叫虛擬函式時,發生動態繫結。引用(或指標)既可以指向基類物件也可以指向派生類物件,這一事實是動態繫結的關鍵。用引用(或指標)呼叫的虛擬函式在執行時確定,被呼叫的函式是引用(或指標)所指物件的實際型別所定義的。

5.28 多型性有哪些?

多型指當不同的物件收到相同的訊息時,產生不同的動作

  1. 編譯時多型(靜態繫結),函式過載,運算子過載,模板。
  2. 執行時多型(動態繫結),虛擬函式機制。

5.29 建構函式可不可以丟擲異常?解構函式呢?

1. 建構函式中儘量不要丟擲異常,能避免的就避免,如果必須,要考慮不要記憶體洩露! 2. 不要在解構函式中丟擲異常!

理論上都可以丟擲異常。 但解構函式最好不要丟擲異常,將會導致析構不完全,從而有記憶體洩露。

為什麼不應該在解構函式中丟擲異常?

1)如果解構函式丟擲異常,則異常點之後的程式不會執行,如果解構函式在異常點之後執行了某些必要的動作比如釋放某些資源,則這些動作不會執行,會造成諸如記憶體洩漏的問題。 2)通常異常發生時,c++的機制會呼叫已經構造物件的解構函式來釋放資源,此時若解構函式本身也丟擲異常,則前一個異常尚未處理,又有新的異常,會造成程式崩潰的問題。

3)當在某一個解構函式中會有一些可能(哪怕是一點點可能)發生異常時,那麼就必須要把這種可能發生的異常完全封裝在解構函式內部,決不能讓它丟擲函式之外(這招簡直是絕殺!呵呵!

5.30 成員函式呼叫底層機制?

 例如我們要呼叫Point的例項 p 的 vec3 normalize() 方法,即 p.normalize();編譯器會做下面的轉變:

1. 改寫函式的原型,增加一個額外的引數 this 指標到引數列表的最前面: 

// 如果成員函式是非const函式,則this指標是指標常量

vec3 Point :: normalize( Point* const this); 

// 如果成員函式是const函式,則this指標是指向常量的指標常量

vec3 Point :: normalize( const Point* const this); 

2. 將函式內部對“非靜態成員”的訪問,改寫為通過this指標訪問

{

return sqrt(

this->x * this->x + 

this->y * this->y +

this->z * this->z

); 

}

3. 將成員函式重寫寫為一個外部函式,並修改函式名,避免名稱和其他函式名衝突:

extern normalize__3PointFv(register const Point* const this);