多態性與虛函數
面向對象程序設計中的多態性
多態性(polymorphism)
調用同一個函數名,根據需要實現不同的功能。
- 編譯時的多態性:靜態多態(函數重載、運算符重載)
- 運行時的多態性:動態多態(虛函數)
函數重載和運算符重載實現的多態性屬於靜態多態性,在程序編譯時就能決定調用的是哪個函數。靜態多態性又稱編譯時的多態性。靜態多態是通過函數重載、運算符重載實現的。
運行時的多態性是指在程序執行之前,根據函數名和參數無法確定應該調用哪一個函數,必須在程序的執行過程中,根據具體的執行情況來動態地確定。動態多態是通過虛函數(virtual function
)實現的。
派生類與基類對象之間的賦值
- 1
可以將派生類對象的值賦給基類對象,但只對從基類繼承來的成員賦值。
class Base{
};
class Derived:public Base{
};
Base b;
Derived d;
b = d;
- 派生類對象可以初始化基類的引用
Derived d;
Base &b = d;
//但b只能引用從基類繼承來的成員
- 派生類對象的地址賦給基類的指針變量。
bp只能引用從基類繼承來的成員。
Base *bp;
Derived d;
bp = &d;
公有派生類對象可以被當作基類的對象使用。反之則不可。
通過基類對象名、指針只能使用從基類繼承的成員。
class A{
public:
void show(){cout<<"A"<<endl;}
};
class AA:public A
{
public:
void show(){cout<<"AA"<<endl;}
};
void f(A *pa){
pa->show();
}
int main()
{
A a;
AA aa;
f(&a);
f(&aa);
return 0;
}
函數f()
的形參是類A的指針,所以調用的方法只能是基類A中的方法。
A
A
這和java
的多態大不一樣。
public class Polymorphism {
public static void main(String[] args) {
A a = new A();
AA aa = new AA();
f(a);
f(aa);
}
static void f(A a){
a.show();
}
}
class A {
void show() {
System.out.println("A");
}
}
class AA extends A{
void show()
{
System.out.println("AA");
}
}
A
AA
- 這種賦值與訪問性的關係。發現只有共有派生的子類才能向上轉型。
虛函數
在程序運行時通過基類指針調用相同的函數名,而實現不同功能的函數稱爲虛函數。
一旦把基類的成員函數定義爲虛函數,由基類所派生出來的所有派生類中,該函數均保持虛函數的特性。
在派生類中重新定義基類中的虛函數時,可以不用關鍵字virtual來修飾這個成員函數 。
虛函數的聲明
virtual關鍵字
virtual 函數類型 函數名(形參表)
{
函數體
}
若要通過基類指針訪問派生類中相同名字的函數,必須將基類中的同名函數定義爲虛函數,這樣,將各種派生類對象的地址賦給基類的指針變量後,就可以動態地根據這種賦值調用不同類中的函數。
若要達成java的效果,只需
在基類A的同名函數show()
前加上virtual
關鍵字。
class A{
public:
virtual void show(){cout<<"A"<<endl;}
};
若要通過基類引用訪問派生類中相同名字的函數,也是可以的。
class A{
public:
virtual void show(){cout<<"A"<<endl;}
};
class AA:public A
{
public:
void show(){cout<<"AA"<<endl;}
};
void f(A &a){
a.show();
}
int main()
{
A a;
AA aa;
f(a);
f(aa);
return 0;
}
A
AA
如果去掉f(A &a)的&
呢?
輸出結果。
A
A
所以通過虛函數的實現的多態,必須要通過指針或引用。
只有在程序的執行過程中,依據指針具體指向哪個類對象,或依據引用哪個類對象,才能確定調用哪一個版本,實現動態綁定。
虛析構函數
如果要通過基類指針調用派生類對象的析構函數(裏面包含delete),就需要讓基類的析構函數成爲虛函數。
class A{
public:
A(){cout<<"A"<<endl;}
~A(){cout<<"~A"<<endl;}
};
class AA:public A{
public:
int *p;
AA(){cout<<"AA"<<endl;p = new int;}
~AA(){cout<<"~AA"<<endl;delete p;}
};
int main()
{
A *pa = new AA;
delete pa;
return 0;
}
A
AA
~A
解決辦法就是在基類的析構函數前加上關鍵字virtual
class A{
public:
A(){cout<<"A"<<endl;}
virtual ~A(){cout<<"~A"<<endl;}
};
class AA:public A{
public:
int *p;
AA(){cout<<"AA"<<endl;p = new int;}
~AA(){cout<<"~AA"<<endl;delete p;}
};
虛函數的幾點說明:
-
當在基類中把成員函數定義爲虛函數後,在其派生類中定義的虛函數必須與基類中的虛函數同名,參數的類型、順序、參數的個數必須一一對應,函數的返回類型也相同。
若函數名相同,但參數的個數不同或者參數的類型不同時,則屬於函數重載,而不是虛函數。
若函數名不同,顯然是不同的成員函數。 -
虛函數必須是類的一個成員函數,不能是友元函數,也不能是靜態的成員函數。
-
可把析構函數定義爲虛析構函數。但是,不能將構造函數定義爲虛函數。
-
在派生類中如果沒有重新定義虛函數時,與一般的成員函數一樣,當調用這種派生類對象的虛函數時,則調用基類中的虛函數。
-
實現動態多態性時,必須使用基類類型的指針變量或引用,指向不同的派生類對象,調用虛函數實現動態多態性。
通過對象名訪問虛函數時,只能靜態綁定。即由編譯器在編譯的時候決定調用哪個函數。 -
虛函數與一般的成員函數相比較,調用時的執行速度要慢一些。爲了實現多態性,在每一個派生類中均要保存相應虛函數的入口地址表,函數的調用機制也是間接實現的。因此,除了要編寫一些通用的程序,必須使用虛函數才能完成其功能要求外,通常不必使用虛函數。
-
一個函數如果被定義成虛函數,則不管經歷多少次派生,仍將保持其虛特性(根據實際的類型來調用相應的函數,而不是聲明的類型),以實現“一個接口,多個形態”。
但一定要注意virtual
在類派生結構中的位置:
class Base0{
public:
void v(){cout<<"Base0\n";}
};
class Base1:public Base0{
public:
virtual void v(){cout<<"Base1\n";}
};
class A1:public Base1{
public:
void v(){cout<<"A1\n";}
};
class A2:public A1{
public:
void v(){cout<<"A2\n";}
};
class B1:private Base1{
public:
void v(){cout<<"B1\n";}
};
class B2:public B1{
public:
void v(){cout<<"B2\n";}
};
int main()
{
Base0 *pb0;
A1 a1;
(pb0=&a1)->v();
A2 a2;
(pb0=&a2)->v();
return 0;
}
原因在於pb0接受了它的派生類的賦值,只能調用自己的函數,virtual
只是修飾了它的派生類。
Base0
Base0
純虛函數(抽象類)
在基類中不對虛函數給出有意義的實現,只在派生類中有具體的意義。這時基類中的虛函數只是一個入口,具體的操作由派生類中的對象實現。這種虛函數稱爲純虛函數(pure virtual)。
至少包含一個純虛函數的類,稱爲抽象類(abstract classes)。抽象類只能作爲派生類的基類,不能用來聲明這種類的對象。
class <抽象類名>
{
virtual <類型> <函數名>(<參數表>) = 0;
……
}
-
在定義純虛函數時,不能定義虛函數的實現部分。
-
把函數名賦於0,本質上是將指向函數的指針值賦爲0。與定義空函數不一樣,空函數的函數體爲空,即調用該函數時,不執行任何動作。在沒有重新定義這種純虛函數之前,是不能調用這種函數的。
-
因爲純虛函數沒有實現部分,所以不能產生對象。但可以定義指向抽象類的指針,即指向這種抽象基類的指針。當用這種基類指針指向其派生類的對象時,必須在派生類中給出純虛函數的實現,否則會導致程序運行錯誤。
-
抽象類的唯一用途是爲派生類提供基類。
純虛函數是作爲派生類中的成員函數的基礎,實現動態多態性。
demo
class A{
public:
virtual void f()=0;
};
class A1:public A{
public:
void f(){cout<<"A1\n";}
};
class A2:public A{
public:
void f(){cout<<"A2\n";}
};
void fun(A *pa){
pa->f();
}
int main()
{
A1 a1;
A2 a2;
fun(&a1);
fun(&a2);
return 0;
}
虛函數與虛基類
- 虛基類的用處:派生類只會有唯一一份基類的拷貝。
要注意virtual
修飾的是那唯一的一份基類,在派生的時候聲明。
demo
class Base{
};
class A1:public Base{
};
class A2:public Base{
};
class B:public A1,public A2{
};
int main()
{
B b;
Base *base = &b; //error , Ambiguous conversion from derived class 'B' to base class 'Base':
return 0;
}
要改爲:
class Base{
};
class A1:virtual public Base{
};
class A2:virtual public Base{
};
class B:public A1,public A2{
};
int main()
{
B b;
Base *base = &b;
return 0;
}
- 虛函數的作用
用基類的指針或引用去根據實際對象的類型進行函數的調用。
這就是virtual
關鍵字在c++中主要的兩個用處。
派生類中重定義基類中的重載函數
C++中繼承層次的函數調用遵循以下四個步驟:
- 首先確定進行函數調用的對象、引用或指針的靜態類型。
- 在該類中查找函數,如果找不到,就在直接基類中查找,如此循着類的繼承鏈往上找,直到找到該函數或者查找完最後一個類。如果不能在類或其相關基類中找到該名字,則調用是錯誤的。
- 一旦找到了該名字,就進行**常規類型檢查,**查看如果給定找到的定義,該函數調用是否合法。
- 假定函數調用合法,編譯器就生成代碼。如果函數是虛函數且通過引用或指針調用,則編譯器生成代碼以確定根據對象的動態類型運行哪個函數版本,否則,編譯器生成代碼直接調用函數。
下面的寫法會報錯。
class Base{
public:
void print(){
cout<<"print() in Base"<<endl;
}
void print(int x){
cout<<"print(int x) in Base"<<endl;
}
void print(string s)
{
cout<<"print(string s) in Base"<<endl;
}
};
class Derived:public Base{
public:
void print(){
cout<<"print() in Derived"<<endl;
}
};
int main()
{
Derived d;
d.print();
d.print(""); // error
d.print(1); // error
return 0;
}
當僅重寫了其中的一個重載函數時,在做函數名匹配時,在本類中就可以匹配到了,就不會向其父類查找了。在派生類中,僅記錄了這個被重寫的函數的信息,沒有其他重載函數,就導致了上述錯誤的出現。即:派生類中的函數會將其父類中的同名函數屏蔽掉。
- 如果派生類想通過自身類型使用基類中重載版本,派生類最好重定義所有重載版本。
解決方案1:
在派生類中通過using
爲父類函數成員提供聲明,這樣,派生類不用重定義所繼承的每一個基類版本。
class Derived:public Base{
public:
using Base::print; // 使用這條語句即可繼承父類所有print()函數的重載
void print(){
cout<<"print() in Derived"<<endl;
}
};
解決方案2:
通過基類指針調用
int main() {
Derived d;
Base* bp = &d;
d.print();
bp->print(10); // 這裏可沒有進行虛函數,執行的還是父類中的函數。
bp->print(""); //
return 0;
}
解決方案3:
在派生類中需要重載的那個版本在基類中聲明爲virtual,實現動態綁定,就能統一的使用基類指針來調用了:
class Base{
public:
virtual void print(){
cout<<"print() in Base"<<endl;
}
void print(int x){
cout<<"print(int x) in Base"<<endl;
}
void print(string s)
{
cout<<"print(string s) in Base"<<endl;
}
};
class Derived:public Base{
public:
void print(){
cout<<"print() in Derived"<<endl;
}
};
int main()
{
Derived d;
Base *pb = &d;
pb->print(); //動態綁定到Derived的函數
pb->print(1);
pb->print("");
return 0;
}
個人覺得,第三種方法最爲自然,並且最好還是自己顯式地將函數全部進行重載(不要只重載一個)。
虛函數動態綁定的實現原理
-
多態類型與非多態類型
- 有虛函數的類類型稱爲多態類型
- 其它類型皆爲非多態類型
-
二者的差異
多態類型支持運行時類型識別;多態類型對象佔用額外的空間。 -
虛表(virtual table)
每個多態類有一個虛表(virtual table),虛表中有當前類的各個虛函數的入口地址 -
虛指針(virtual point)
每個對象有一個指向當前類的虛表的指針(虛指針vptr) -
動態綁定的實現
構造函數中爲對象的虛指針賦值
通過多態類型的指針或引用調用成員函數時,通過虛指針找到虛表,進而找到所調用的虛函數的入口地址,通過該入口地址調用虛函數。