C++多型(上)——虛擬函式、虛表
OOP的核心思想是多型性(polymorphism)。其含義是“多種形態”。我們把具有繼承關係的多個型別稱為多型型別。引用或指標的靜態型別和動態型別不同這一事實正是C++語言支援多型性的根本所在。
多型性:當用於物件導向程式設計的範疇時,多型性的含義是指程式能通過引用或指標的動態型別來獲取型別特定行為的能力。
多型性在C++中是通過虛擬函式來實現的。
首先我們先來強調幾組概念:
靜態多型和動態多型、靜態聯編和動態聯編、靜態型別和動態型別
1. 靜態型別和動態型別
靜態型別——在編譯時是已知的,它是在變數宣告時的型別或表示式生成的型別。
動態型別——物件在執行時的型別。引用所引物件或指標所指物件的動態型別可能與該引用或指標的靜態型別不同。基類的指標或引用可以指向一個派生類物件,在這種情況下,靜態型別是基類的引用(或指標),而動態型別是派生類的引用(或指標)。
Devese d;
Base *pb = &d; //靜態型別是基類的指標,動態型別是派生類的指標
2. 靜態聯編和動態聯編
靜態聯編——也稱靜態繫結或早期聯編,比如之前提到的函式過載,運算子過載。它是在編譯過程彙總進行的聯編。
動態聯編——也稱動態繫結,直到執行時才知道到底呼叫函式的哪個版本。在C++語言中,動態繫結的意思是在執行時根據引用或指標所繫結物件的實際型別來選擇執行虛擬函式的某一個版本。
3. 靜態多型和動態多型
(下圖中“多型多型”應該是“動態多型”,實在抱歉)
虛擬函式
虛擬函式——用於定義型別特定行為的成員函式。通過指標或引用對虛擬函式的呼叫直到執行時才被解析,依據是引用或指標所繫結物件的實際型別。
虛擬函式的用法格式:
virtual 函式返回型別 函式名 (引數列表) {函式體}
當我們使用基類的指標或引用基類中定義的一個函式時,我們並不知道該函式真正作用的物件是什麼型別,因為它可能是一個基類的物件也可能是一個派生類的物件。如果該函式是虛擬函式,則直到執行時才會決定到底執行哪個版本,判斷的依據是引用或指標所繫結物件的實際型別;反之,如果是非虛擬函式,對非虛擬函式的呼叫在編譯時進行繫結。
類似的,通過物件的函式呼叫也在編譯時繫結。物件的型別是確定不變的,我們無論如何都不可能令物件的靜態型別和動態型別不一致。因此,通過物件進行的函式呼叫將在編譯時被繫結到該物件所屬類中的函式版本上。
——摘自《C++ Primer》
看下面一段程式碼:
class Father
{
public:
void fun()
{
cout << "Father::fun()" << endl;
}
protected:
int _f;
};
class Child :public Father
{
public:
void fun()
{
cout << "Child::fun()" << endl;
}
protected:
int _c;
};
int main()
{
Child c;
Father f;
Father *pf = &c;
pf->fun();
system("pause");
return 0;
}
輸出的結果是:
上面程式中fun函式是非虛擬函式,所以呼叫函式在編譯時進行繫結,所以呼叫基類中的fun()函式。
下面我們將fun()函式設為虛擬函式,看結果有何不同?
class Father
{
public:
virtual void fun() //定義為虛擬函式
{
cout << "Father::fun()" << endl;
}
protected:
int _f;
};
class Child :public Father
{
public:
virtual void fun() //定義為虛擬函式
{
cout << "Child::fun()" << endl;
}
protected:
int _c;
};
int main()
{
Child c;
Father f;
Father *pf = &c;
pf->fun();
system("pause");
return 0;
}
執行結果:
兩次的結果明顯不同,這次呼叫的是派生類的fun()函式,因為是虛擬函式呼叫在執行時才知道呼叫函式哪個版本。
Father *pf = &c; //指標pf的靜態型別是基類指標,但是動態型別是派生類指標,而pf指標所繫結的物件的真是型別是派生類指標。所以呼叫派生類類中的fun()函式。
再看下面一段程式碼:
lass Father
{
public:
virtual void fun() //虛擬函式
{
cout << "Father::fun()" << endl;
}
protected:
int _f;
};
class Child :public Father
{
public:
virtual void fun() //虛擬函式
{
cout << "Child::fun()" << endl;
}
protected:
int _c;
};
int main()
{
Child c;
Father f;
f.fun();
c.fun();
system("pause");
return 0;
}
執行結果:
class Father
{
public:
void fun()
{
cout << "Father::fun()" << endl;
}
protected:
int _f;
};
class Child :public Father
{
public:
void fun()
{
cout << "Child::fun()" << endl;
}
protected:
int _c;
};
int main()
{
Child c;
Father f;
f.fun();
c.fun();
system("pause");
return 0;
}
執行結果:
上面兩段程式碼說明:
通過物件呼叫函式時,與是不是虛擬函式無關,因為函式呼叫是在編譯時繫結的。繫結的是所屬類中的函式版本。
虛擬函式工作原理
我們先來了解一個概念:虛擬函式表
虛擬函式表:虛擬函式表又稱虛表(vtbl),是一塊連續的記憶體,編譯器會為每一個含有虛擬函式的類建立一個虛表,該虛表將被所有該類的所有物件共享,裡面儲存的是該類的虛擬函式的地址。虛表的大小是N*4(N個虛擬函式,一個虛擬函式佔一行,最後以0結尾)。虛擬函式的實現就是通過虛表來實現的。之前講到只有虛擬函式才能被覆蓋,就是指的是虛表中虛擬函式地址被覆蓋。
在有虛擬函式的類例項化時,編譯器分配了指向該表的指標的記憶體(虛擬函式表指標vptr),簡單一點就是便一起給每個物件新增了一個隱藏成員,這個隱藏成員中儲存了一個指向虛擬函式表的指標。這意味著可以通過類例項化的地址得到虛表,然後遍歷其中的函式指標,並呼叫相應的函式。
注:
1,基類物件含有一個指標,該指標指向基類中所有虛擬函式的地址表。派生類物件將含有一個指向獨立地址表的指標。
2,如果派生類提供了基類虛擬函式的重定義,該虛擬函式表將儲存新函式的地址。即就是虛擬函式覆蓋實際是對虛擬函式表中的虛擬函式的地址的覆蓋。
3,如果派生類定義了新的虛擬函式,則該函式的地址將被加入到虛擬函式表中。注意,無論類中是一個還是多個虛擬函式,都只需在物件中新增一個地址成員,只是表的大小不同。
下面我們通過一個例子來看虛擬函式機制記憶體佈局:
class Father
{
public:
virtual void fun1()
{
cout << "Father::fun1()" << endl;
}
virtual void fun2()
{
cout << "Father::fun2()" << endl;
}
int _f;
};
class Child :public Father
{
public:
int _c;
};
int main()
{
Father f;
f._f = 1;
Child c;
c._c = 2;
system("pause");
return 0;
}
我們可以發現當例項化時編譯器就為物件生成了一個隱藏成員(虛表指標),也就是本例中的“1c 8c c8 00”.後面通過看虛表指標指向的虛表,可以發現有兩個地址,他們從上到下分別就是fun1(),fun2()的地址,第三行是0,虛表就是以NULL結尾的。
那我們來畫一下記憶體佈局:
下面我們來在派生類中對基類中的虛擬函式fun2()進行重定義,也就是覆蓋。看看會有什麼不同。
class Father
{
public:
virtual void fun1()
{
cout << "Father::fun1()" << endl;
}
virtual void fun2()
{
cout << "Father::fun2()" << endl;
}
int _f;
};
class Child :public Father
{
public:
virtual void fun2() //覆蓋基類中函式fun2()
{
cout << "Child::fun2()" << endl;
}
int _c;
};
int main()
{
Father f;
f._f = 1;
Child c;
c._c = 2;
system("pause");
return 0;
}
下面這幅圖就能證明上面這幅圖示註的正確性
下面將派生類中覆蓋基類虛擬函式fun2()的函式改為虛擬函式fun3(),看看會出現什麼情況?
class Father
{
public:
virtual void fun1()
{
cout << "Father::fun1()" << endl;
}
virtual void fun2()
{
cout << "Father::fun2()" << endl;
}
int _f;
};
class Child :public Father
{
public:
virtual void fun3()
{
cout << "Child::fun3()" << endl;
}
int _c;
};
int main()
{
Father f;
f._f = 1;
Child c;
c._c = 2;
system("pause");
return 0;
}
看上面這個例子,說明如果派生類定義了新的虛擬函式,則該函式的地址將被加入到虛擬函式表中。
今天就講到這,大家應該對虛擬函式、虛表有個清晰的認識。這裡是面試常問的。
相關文章
- C++多型之虛擬函式C++多型函式
- 虛擬函式表-C++多型的實現原理函式C++多型
- c++虛擬函式表C++函式
- 虛擬函式,虛擬函式表函式
- 虛擬函式與多型函式多型
- 【C++筆記】虛擬函式(從虛擬函式表來解析)C++筆記函式
- 詳解C++中的多型和虛擬函式C++多型函式
- C++(虛擬函式實現多型基本原理)C++函式多型
- 深入C++成員函式及虛擬函式表C++函式
- 基類指標、虛純虛擬函式、多型性、虛析構指標函式多型
- 【C++筆記】虛擬函式(從虛擬函式概念來解析)C++筆記函式
- 虛擬函式 純虛擬函式函式
- C++ 介面(純虛擬函式)C++函式
- C++ 虛擬函式表解析C++函式
- C++物件導向總結——虛指標與虛擬函式表C++物件指標函式
- C++單繼承、多繼承情況下的虛擬函式表分析C++繼承函式
- 介面、虛擬函式、純虛擬函式、抽象類函式抽象
- <<從0到1學C++>> 第7篇 多型性和虛擬函式C++多型函式
- C++虛擬函式學習總結C++函式
- [Lang] 虛擬函式函式
- C++純虛擬函式簡介及區別C++函式
- c++虛擬函式實現計算表示式子C++函式
- C++之類解構函式為什麼是虛擬函式C++函式
- 虛擬函式的記憶體佈局(上)函式記憶體
- 虛擬函式的呼叫原理函式
- C++ 派生類函式過載與虛擬函式繼承詳解C++函式繼承
- C++建構函式和解構函式呼叫虛擬函式時使用靜態聯編C++函式
- 虛擬函式的實現原理函式
- 抽象基類和純虛擬函式抽象函式
- 內聯(inline)函式與虛擬函式(virtual)的討論inline函式
- 深度解讀《深度探索C++物件模型》之C++虛擬函式實現分析(二)C++物件模型函式
- 深度解讀《深度探索C++物件模型》之C++虛擬函式實現分析(一)C++物件模型函式
- 深度解讀《深度探索C++物件模型》之C++虛擬函式實現分析(三)C++物件模型函式
- C++ 虛解構函式簡單測試C++函式
- 關於虛擬函式的一些理解函式
- [C++ Daily] 虛表與虛指標的理解C++AI指標
- 在Apache上實現多HTTPS虛擬主機ApacheHTTP
- setV:一個管理 Python 虛擬環境的 Bash 函式Python函式
- Java常見知識點彙總(④)——虛擬函式、抽象函式、抽象類、介面Java函式抽象