弄清楚了這四個關鍵詞,也就弄懂了C++語言中的虛函數

基於一個簡單的實例討論了C++語言中的虛函數,我們提到了“動態綁定”這個詞,大意就是動態綁定在很大程度上滿足了虛函數的特性,從而支持了C++的多態性。不過我們知道,C++是一門強類型的語言,它是如何在保持

靜態類型的同時,實現動態綁定的呢?

弄清楚了這四個關鍵詞,也就弄懂了C++語言中的虛函數

如何在保持靜態類型的同時,實現動態綁定的呢?


“靜態”VS“動態”

在C++語言中,對象指針一般能夠提供兩種類型信息:

  1. 指針本身的類型
  2. 指針指向類的類型

鑑於指針指向的類可能是繼承某個基類派生而來的,所以這兩個信息可能是不同的,請看下面這段C++語言代碼示例:

class Animal
{
public:
virtual void eat() {
std::cout << "I'm eating generic food." ;
}
};

class Cat : public Animal
{
public:
void eat() {
std::cout << "I'm eating a rat.";
}
};

Animal *a = new Animal;
Cat *c = new Cat;

a = c;
a->eat();
弄清楚了這四個關鍵詞,也就弄懂了C++語言中的虛函數

C++語言代碼示例

上述代碼執行完畢後,(靜態)類型為Animal *的指針 a 指向的實際上是Cat對象 c,因為Animal::eat()是虛函數,所以a->eat()在運行時(動態)確定為Cat::eat()。到這裡其實可以看出,所謂“靜態”,其實就是在編譯時確定的類型,而“動態”則是在運行時確定的類型,二者在C++語言中提供不同的功能。

靜態類型通常在編譯時用於成員函數調用的合法性檢查,編譯器會根據指針的靜態類型來確定程序是否能夠合法的調用某個成員函數,如果可以,那麼它指向的對象也一定可以。例如,如果 Animal對象有某個成員函數可供調用,那麼Cat對象也一定可以調用該成員函數,因為Cat是Animal的其中一種派生類。

動態綁定則在程序運行過程中,執行被調用函數時用於確定實際的函數地址,它被稱作“動態”是因為其在程序運行時才最終被確定下來,C++語言中的虛函數的特性正是基於“動態綁定”機制實現的。

四個關鍵詞

要進一步的理解C++語言中的“動態”和“靜態”相關的概念,需要先理解下面這四個關鍵詞:

  1. 對象的靜態類型:聲明對象使用的類型,編譯時確定。
  2. 對象的動態類型:指針當前所指對象的類型,運行時確定。
  3. 靜態綁定:綁定對象的靜態類型,發生在編譯時。
  4. 動態綁定:綁定對象的動態類型,發生在運行時。

關於前兩個名詞,可以看下面這幾段C++語言代碼示例:

Cat *c = new Cat;

此時對象指針 c 的靜態類型是聲明它時採用的Cat *,動態類型也是Cat *。對於下面這行C++語言代碼:

Animal *a = c;

對象指針 a 的靜態類型是Animal *,動態類型就不同了,而是Cat *。對象的靜態類型一旦確定,就不能修改了,但是動態類型可以,例如:

a = new Animal;

現在來看看“靜態綁定”和“動態綁定”,我們先為 Animal 和 Cat 類新增加兩個非虛函數:

class Animal
{
public:
virtual void eat() {
std::cout << "I'm eating generic food." ;
}
void run() {
std::cout << "some can run." ;
}
};

class Cat : public Animal
{
public:
void eat() {
std::cout << "I'm eating a rat.";
}
void run() {
std::cout << "I can run." ;
}
};

Cat *c = new Cat;
Animal *a = c;
弄清楚了這四個關鍵詞,也就弄懂了C++語言中的虛函數

為 Animal 和 Cat 類新增加兩個非虛函數

此時 a->run() 和 c->run() 調用的是同一個函數嗎?顯然不是的,非虛函數 run() 是靜態綁定的,這一過程發生在編譯時,並且具體調用哪個函數由對象的靜態類型確定,所以雖然 a 和 c 指向同一個對象,但是 a 的靜態類型是Animal *,所以 a->run() 調用的是 Animal::run(),同理,c->run() 調用的是 Cat::run()。

那 a->eat() 和 c->eat() 調用的是同一個函數嗎?是的,eat() 函數是虛函數,它是動態綁定的,這一過程發生在運行時,並且具體調用哪個函數由指針指向的對象類型確定,因為 a 和 c 指向的都是Cat對象,所以 a->eat() 和 c->eat() 調用的都是 Cat::eat() 函數。

看來,弄清C++語言中哪些是靜態綁定,哪些是動態綁定是問題的關鍵,我查閱了很多資料,沒有得到直接的答案,但是相當多的C++程序員都認為只有涉及到虛函數時才使用動態綁定,其他的都是靜態綁定。當然了,這一結論是根據我有限的學識推測的,只不過我目前還沒有發現例外,如果有錯誤,希望可以指點一二。

注意事項

C++語言程序開發中有一條“潛規則”:Never redefine function’s inherited default parameters value,意思是絕對不要重定義函數繼承而來的默認參數,對於虛函數來說也一樣需要遵守。請看下面這個例子:

class A {
virtual void vfoo(int i=1);
};
class B :public A{
virtual void vfoo(int i=2);
};
B *b = new B;
A *a = b;
a->vfoo();
b->vfoo();
弄清楚了這四個關鍵詞,也就弄懂了C++語言中的虛函數

不要重定義函數繼承而來的默認參數

經過上面的討論,相信讀者一眼就能看出 a->vfoo() 和 b->vfoo() 調用的都是 B::vfoo(),但是它們的默認參數是多少呢?會不會都是 2 呢?按照前面的理論“除了虛函數,C++語言中的其他都是靜態綁定的”,那麼默認參數也應該是靜態綁定的,這就是說默認參數主要取決於對象指針的靜態類型,也即 a->vfoo() 的默認參數取決於 a 的靜態類型A *,所以它的默認參數是 1,同理,b->vfoo() 的默認參數應該是 2,將示例C++語言代碼寫完整測試之:

#include <iostream>

using namespace std;

class A {
public:
virtual void vfoo(int i=1){
cout < }
};
class B :public A{
public:
virtual void vfoo(int i=2){
cout < }
};

int main()
{
B *b = new B;
A *a = b;
a->vfoo();
b->vfoo();

return 0;
}/<iostream>
弄清楚了這四個關鍵詞,也就弄懂了C++語言中的虛函數

將示例C++語言代碼寫完整

編譯並執行這段代碼,得到如下輸出:

$ g++ t.cpp
$ a.out
B: 1
B: 2

可見,默認參數的確是靜態綁定的。但是這樣的輸出很怪異,動態綁定和靜態綁定糾纏在一起,很容易弄混頭腦,相信沒有人喜歡這樣,所以還是遵守行業“潛規則”吧。

小結

本文主要討論了理解C++語言中虛函數的關鍵——“靜態綁定”和“動態綁定”的概念,這兩個過程分別發生編譯時和運行時,前者根據對象的指針類型確定調用函數,後者則根據指針指向的對象類型確定調用函數。文章在最後還引入了一條C++程序員間的潛規則,並通過一段簡短的示例說明了遵守潛規則的必要性。

歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。

代碼看著不方便的話,可以點擊文章末尾的“瞭解更多”。


分享到:


相關文章: