1. 程式人生 > >為什麼基類指標和引用可以指向派生類物件,但是反過來不行?

為什麼基類指標和引用可以指向派生類物件,但是反過來不行?

基類指標和引用

BaseClass *pbase = NULL;
DerivedClass dclass;
pbase = & dclass;
基類指標和引用可以指向派生類物件,但是無法使用不存在於基類只存在於派生類的元素。(所以我們需要虛擬函式和純虛擬函式)

原因是這樣的:

在記憶體中,一個基類型別的指標是覆蓋N個單位長度的記憶體空間。
當其指向派生類的時候,由於派生類元素在記憶體中堆放是:前N個是基類的元素,N之後的是派生類的元素。
於是基類的指標就可以訪問到基類也有的元素了,但是此時無法訪問到派生類(就是N之後)的元素.
型別一致並不是死板地說型別一定要完全一樣,型別是一種約束,幫助你驗證程式的正確性。比如說你女朋友說:“我要吃水果!”,這時候你送上去一個“蘋果”,也應該是滿足條件的,而送上去一個饅頭可能就孤獨一生了,這就是型別系統的作用.

程式碼中pb的靜態型別是Base*,這個是不可改變的,在其定義時就已經決定了。
但pb的動態型別是DerivedClass*,這個可以在執行時改變(這樣才實現了多型

C++在面向物件程式設計中,存在著靜態繫結和動態繫結的定義,本節即是主要講述這兩點區分。
是在一個類的繼承體系中分析的,因此下面所說的物件一般就是指一個類的例項。
首先我們需要明確幾個名詞定義:

靜態型別:物件在宣告時採用的型別,在編譯期既已確定;
動態型別:通常是指一個指標或引用目前所指物件的型別,是在執行期決定的;
靜態繫結:繫結的是靜態型別,所對應的函式或屬性依賴於物件的靜態型別,發生在編譯期;
動態繫結:繫結的是動態型別,所對應的函式或屬性依賴於物件的動態型別,發生在執行期;必須搞清楚的一點是:動態繫結只有當我們指標或引用呼叫虛擬函式的時候才會發生。
從上面的定義也可以看出,非虛擬函式一般都是靜態繫結,而虛擬函式都是動態繫結(如此才可實現多型性)。
先看程式碼和執行結果:

 class A
  {
 public:
      /*virtual*/ void func(){ std::cout << "A::func()\n"; }
  };
  class B : public A
  {
  public:
      void func(){ std::cout << "B::func()\n"; }
 };
 class C : public A
 {
 public:
     void func(){ std::cout << "C::func()\n"; }
 };

下面逐步分析測試程式碼及結果,

1 C* pc = new C(); //pc的靜態型別是它宣告的型別C*,動態型別也是C*;
2 B* pb = new B(); //pb的靜態型別和動態型別也都是B*;
3 A* pa = pc; //pa的靜態型別是它宣告的型別A*,動態型別是pa所指向的物件pc的型別C*;
4 pa = pb; //pa的動態型別可以更改,現在它的動態型別是B*,但其靜態型別仍是宣告時候的A*;
5 C pnull = NULL; //pnull的靜態型別是它宣告的型別C

,沒有動態型別,因為它指向了NULL;
如果明白上面程式碼的意思,請繼續,

1 pa->func(); //A::func() pa的靜態型別永遠都是A*,不管其指向的是哪個子類,都是直接呼叫A::func();
2 pc->func(); //C::func() pc的動、靜態型別都是C*,因此呼叫C::func();
3 pnull->func(); //C::func() 不用奇怪為什麼空指標也可以呼叫函式,因為這在編譯期就確定了,和指標空不空沒關係;
如果註釋掉類C中的func函式定義,其他不變,即

class C : public A
{

};

pa->func(); //A::func() 理由同上;
pc->func(); //A::func() pc在類C中找不到func的定義,因此到其基類中尋找;
7 pnull->func(); //A::func() 原因也解釋過了;
如果為A中的void func()函式新增virtual特性,其他不變,即

class A
{
public:
virtual void func(){ std::cout << “A::func()\n”; }
};

pa->func(); //B::func() 因為有了virtual虛擬函式特性,pa的動態型別指向B*,因此先在B中查詢,找到後直接呼叫;
8 pc->func(); //C::func() pc的動、靜態型別都是C*,因此也是先在C中查詢;
9 pnull->func(); //空指標異常,因為是func是virtual函式,因此對func的呼叫只能等到執行期才能確定,然後才發現pnull是空指標;
引用或指標的靜態型別與動態型別不同這一事實正是C++語言支援多型性的根本所在。當我們使用基類的指標或者引用呼叫基類中定義的一個函式時,我們並不知道該函式真正作用的物件是什麼型別,因為它可能是一個基類的物件也可能死派生類的一個物件。如果該函式時虛擬函式,則知道執行時才能知道到底執行哪一個版本,判斷的依據是引用或者指標所繫結的物件的真實型別。

另一方面,對非虛擬函式的呼叫和通過物件進行的函式(虛擬函式或非虛擬函式)呼叫
在編譯期繫結。物件的型別是不變的,我們無論如何不能令物件的靜態型別和動態型別不同(指標和引用可以不同)。因此,通過物件進行的函式呼叫將在編譯時繫結到該物件所屬類中的函式版本上。

分析:

如果基類A中的func不是virtual函式,那麼不論pa、pb、pc指向哪個子類物件,對func的呼叫都是在定義pa、pb、pc時的靜態型別決定,早已在編譯期確定了。
同樣的空指標也能夠直接呼叫no-virtual函式而不報錯(這也說明一定要做空指標檢查啊!),因此靜態繫結不能實現多型;

如果func是虛擬函式,那所有的呼叫都要等到執行時根據其指向物件的型別才能確定,比起靜態繫結自然是要有效能損失的,但是卻能實現多型特性;
本文程式碼裡都是針對指標的情況來分析的,但是對於引用的情況同樣適用。

至此總結一下靜態繫結和動態繫結的區別:

靜態繫結發生在編譯期,動態繫結發生在執行期;

物件的動態型別可以更改,但是靜態型別無法更改;

要想實現動態,必須使用動態繫結;

在繼承體系中只有虛擬函式使用的是動態繫結,其他的全部是靜態繫結;

建議:

絕對不要重新定義繼承而來的非虛(non-virtual)函式(《Effective C++ 第三版》條款36),因為這樣導致函式呼叫由物件宣告時的靜態型別確定了,而和物件本身脫離了關係,沒有多型,也這將給程式留下不可預知的隱患和莫名其妙的BUG;

另外,在動態繫結也即在virtual函式中,要注意預設引數的使用。當預設引數和virtual函式一起使用的時候一定要謹慎,不然出了問題怕是很難排查。
看下面的程式碼:

class E
{
public:
virtual void func(int i = 0)
{
std::cout << “E::func()\t”<< i <<"\n";
}
};
class F : public E
{
public:
virtual void func(int i = 1)
{
std::cout << “F::func()\t” << i <<"\n";
}
};

void test2()
{
F* pf = new F();
E* pe = pf;
pf->func(); //F::func() 1 正常,就該如此;
pe->func(); //F::func() 0 哇哦,這是什麼情況,呼叫了子類的函式,卻使用了基類中引數的預設值!
24 }
為什麼會有這種情況,請看《Effective C++ 第三版》 條款37。
這裡只給出建議:
絕對不要重新定義一個繼承而來的virtual函式的預設引數值,因為預設引數值都是靜態繫結(為了執行效率),而virtual函式卻是動態繫結。

override關鍵字(C++ 11)

基類中的虛擬函式在派生類中隱含的也是一個虛擬函式。當派生類覆蓋了虛擬函式時,該函式在基類中的形參必須與派生類中的形參嚴格匹配。如果,函式名字相同但是形參列表不同,這是合法的,但是新定義的函式與基類中的函式時相互獨立的。並沒有發生覆蓋,通常情況下,我們把這當做一種錯誤,因為我們希望它發生覆蓋,但是不小心形參列表弄錯了。除錯並發現這樣的錯誤很困難,因此新標準中引入了override關鍵字。在沒有發生覆蓋虛擬函式的情況下(比如:引數列表不同、基類中的這個函式不是虛擬函式或者是基類中沒有該函式)不能通過編譯。

相應的,我們還可以使用final將函式宣告為不允許覆蓋(只有虛擬函式才存在覆蓋)的。

final和override出現在形參列表和尾置返回型別之後。

虛擬函式與預設實參

虛擬函式也可以擁有預設實參,如果某次函式呼叫使用了預設實參,則該實參值由本次呼叫的靜態型別決定。也就是說,如果我們通過基類的指標或者引用呼叫引數,則使用基類中定義的預設實參,即使實際執行的是派生類總的函式版本也是如此。此時,傳入派生類函式的將是基類函式定義的預設實參。如果派生類函式依賴不同的實參,則程式結果將與我們的預期不同。

迴避虛擬函式機制

如果希望對虛擬函式的呼叫不要進行動態繫結,而是希望執行它的特定版本,使用作用域運算子可以實現這一目標。

baseP->Base::func();
通常情況下,只有成員函式和友元函式使用作用域運算子迴避虛擬函式。如果一個派生類的虛擬函式需要呼叫它的基類版本,如果沒有使用作用域運算子,則在執行時該呼叫將被解析成派生類版本自身的呼叫,從而導致無限迴圈。

http://m.blog.csdn.net/blog/yapian8/42460915# ////原文


關於我自己

  • 一個正派但不正經的程式設計師

  • 18年計算機專業碩士畢業生,騰訊SNG部門實習生,現加盟快手科技 ,後端研發工程師一枚

  • 喜歡技術,喜歡網際網路

  • 民遙控 ,趙雷、陳粒、宋冬野

  • 公眾號:程式設計美學,時不時寫篇文章,偶爾數羊,其實說到底,只是想和你聊聊
    在這裡插入圖片描述