C++中的靜態綁定和動態綁定


C++在面向對象編程中,存在着靜態綁定和動態綁定的定義,本節即是主要講述這兩點區分。
我是在一個類的繼承體系中分析的,因此下面所說的對象一般就是指一個類的實例。
首先我們需要明確幾個名詞定義:

  • 靜態類型:對象在聲明時采用的類型,在編譯期既已確定;
  • 動態類型:通常是指一個指針或引用目前所指對象的類型,是在運行期決定的;
  • 靜態綁定:綁定的是靜態類型,所對應的函數或屬性依賴於對象的靜態類型,發生在編譯期;
  • 動態綁定:綁定的是動態類型,所對應的函數或屬性依賴於對象的動態類型,發生在運行期;

從上面的定義也可以看出,非虛函數一般都是靜態綁定,而虛函數都是動態綁定(如此才可實現多態性)。
先看代碼和運行結果:

 1 class A
 2 {
 3 public:
 4     /*virtual*/ void func(){ std::cout << "A::func()\n"; }
 5 };
 6 class B : public A
 7 {
 8 public:
 9     void func(){ std::cout << "B::func()\n"; }
10 };
11 class C : public A
12 {
13 public:
14     void func(){ std::cout << "C::func()\n"; }
15 };

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

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函數定義,其他不變,即

1 class C : public A
2 {
3 };
4 
5 pa->func();      //A::func() 理由同上;
6 pc->func();      //A::func() pc在類C中找不到func的定義,因此到其基類中尋找;
7 pnull->func();   //A::func() 原因也解釋過了;

如果為A中的void func()函數添加virtual特性,其他不變,即

1 class A
2 {
3 public:
4     virtual void func(){ std::cout << "A::func()\n"; }
5 };
6 
7 pa->func();      //B::func() 因為有了virtual虛函數特性,pa的動態類型指向B*,因此先在B中查找,找到后直接調用;
8 pc->func();      //C::func() pc的動、靜態類型都是C*,因此也是先在C中查找;
9 pnull->func();   //空指針異常,因為是func是virtual函數,因此對func的調用只能等到運行期才能確定,然后才發現pnull是空指針;

分析:
在上面的例子中,

1. 如果基類A中的func不是virtual函數,那么不論pa、pb、pc指向哪個子類對象,對func的調用都是在定義pa、pb、pc時的靜態類型決定,早已在編譯期確定了。

    同樣的空指針也能夠直接調用no-virtual函數而不報錯(這也說明一定要做空指針檢查啊!),因此靜態綁定不能實現多態;

2. 如果func是虛函數,那所有的調用都要等到運行時根據其指向對象的類型才能確定,比起靜態綁定自然是要有性能損失的,但是卻能實現多態特性;

 本文代碼里都是針對指針的情況來分析的,但是對於引用的情況同樣適用。

至此總結一下靜態綁定和動態綁定的區別
1. 靜態綁定發生在編譯期,動態綁定發生在運行期;

2. 對象的動態類型可以更改,但是靜態類型無法更改;

3. 要想實現動態,必須使用動態綁定;

4. 在繼承體系中只有虛函數使用的是動態綁定,其他的全部是靜態綁定;

 建議:

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

 

另外,在動態綁定也即在virtual函數中,要注意默認參數的使用。當缺省參數和virtual函數一起使用的時候一定要謹慎,不然出了問題怕是很難排查。
看下面的代碼:

 1 class E
 2 {
 3 public:
 4     virtual void func(int i = 0)
 5     { 
 6         std::cout << "E::func()\t"<< i <<"\n";
 7     }
 8 };
 9 class F : public E
10 {
11 public:
12     virtual void func(int i = 1)
13     {
14         std::cout << "F::func()\t" << i <<"\n";
15     }
16 };
17 
18 void test2()
19 {
20     F* pf = new F();
21     E* pe = pf;
22     pf->func(); //F::func() 1  正常,就該如此;
23     pe->func(); //F::func() 0  哇哦,這是什么情況,調用了子類的函數,卻使用了基類中參數的默認值!
24 }

為什么會有這種情況,請看《Effective C++ 第三版》 條款37。
這里只給出建議:
絕對不要重新定義一個繼承而來的virtual函數的缺省參數值,因為缺省參數值都是靜態綁定(為了執行效率),而virtual函數卻是動態綁定。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM