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函數卻是動態綁定。