開始之前,我們首先假設這樣一組形狀類。Shape,Point,Circle很明顯,它們有着Shape<-Point<-Circle的繼承關係,每個類都有一個printName,print,area函數,用來打印自己的類型、相關數據以及面積。當我們想要得到不同形狀的對象的這些信息時,我們自然希望能統一的作爲Shape的對象來處理,這就要用到虛函數。
1、虛函數
簡單的說,虛函數就是在函數原型前加上virtual關鍵字。函數一旦被聲明爲虛函數,即使類在改寫它的時候沒有將其聲明爲虛函數,它從該點的繼承層次結構中仍然是虛函數。如果基類中一個函數被聲明爲虛函數,在若干子類中有着不同的實現,那麼我們在創建若干子類的不同對象時,可以用基類的指針或者引用來指明子類對象並且調用相應子類的函數。
2、純虛函數
純虛函數是在聲明虛函數時初始化爲0的函數。
3、抽象類
簡單的說,帶有一個或多個未實現的純虛函數的類是抽象類。如果一個類繼承自一個抽象類,但它沒有全部實現父類裏面所有的純虛函數,那麼那些純虛函數在子類中仍然是純虛的,這個子類仍然是抽象類,仍然不能實例化對象。
雖然不能實例化抽象類,但我們可以聲明一個抽象類的指針和引用,在實例化對象時,可以用不同的子類來實現,從而實現多態性操作。
4、多態性
簡單的說,多態性就是對象能夠對同一個函數調用作出不同的響應。多態性通過虛函數來實現,當我們用基類的指針來調用虛函數時,程序會根據對象的屬性選擇其自身的函數實現。即使程序員不知道對象類型,程序仍然可以作出適合該對象類型的行爲。
5、虛析構函數
用多態性動態分配對象會產生一個問題,就是當我們delete一個指向對象的基類指針時,基類的析構函數仍然會被調用。解決的方法是把基類的析構函數聲明爲虛的,這樣可以利用虛函數的性質來調用適合該對象的合適的類的析構函數。當對象被刪除時,派生類的基類部分也會被刪除——在派生類析構函數之後自動執行基類的析構函數。所以,如果一個類有虛函數,即使該類不需要虛析構函數,也最好提供一個虛析構函數,以保證該類派生出來的子類所包括的析構函數能被正確調用。下面來看一個綜合的例子
#ifndef SHAPE
#define SHAPE
class Shape{
public:
Virtual double area() const { return 0.0; }
Virtual void printName() const =0;//純虛函數
Virtual void print() const = 0;//純虛函數
};
#endif
/*****************************************************************/
//pint.h
#ifndef POINT
#define POINT
#include <iostream>
using std::cout;
#include "shape.h"
class Point:public Shape{
public:
Pint(int = 0, int = 0);
void setPoint(int, int);
int getX() const { return x; }
int getY const { return y; }
Virtual void printName() const { cout << "Point: ";}
Virtual void print() const;
private:
int x, y;
};
#endif
/********************************/
//point.cpp
#include "point.h"
void Point::setPoint( int a, int b)
{
x = a;
y = b;
}
void Point::print()
{
cout << '[ '<<x<<" , "<<y<<' ]';
}
/**************************************************************************/
//circle.h
#ifndef CIRCLE
#define CIRCLE
#include "point.h"
class Circle:public Point{
public:
Circle(double r = 0.0,int x =0,int y = 0);
void setRadius( double );
double getRadius() const;
Virtual double area() const;
Virtual void printName() const { cout << "Circle:";}
virtual void print() const;
private:
double radius;
};
#endif
/******************************/
//circle.cpp
#include <iostram>
using std::cout;
#include "circle.h"
Circle::Circle(double r,int a, int b) : Point( a, b)//調用基類構造函數
{
setRadius( r );
}
void Circle::setRadius(double r )
{
radius = r > 0 ? r : 0;
}
double Circle::getRadius() const
{
return radius;
}
double Circle::area() const//實現area
{
return 3.14159 * radius * radius;
}
void Circle::print() const
{
Point:print();
cout << "; Radius = " << radius;
}
/***************************************************************/
//test.cpp
#include <iostream>
using std::cout;
using std::endl;
#include <iomanip>
using std::ios;
using std::setiosflags;
using std::setprecision;
#include "shape.h"
#include "point.h"
#include "circle.h"
void usePointer( const Shape * );
void useReference ( const Shape &);
int main()
{
cout << setiosflags ( ios::showpoint) << setprecision( 2 );
Point point( 7, 11);
Circle circle(3.5, 22, 8);
//顯示調用各對象自己的函數
point.printName();
point.print();
cout << endl;
circle.printName();
circle.print();
cout << endl;
Shape * shapes[2];
shapes[0] = &point;
shapes[1] = &circle;
cout << endl <<"Using base-class point"<<endl;
int i;
for( i = 0; i < 2; i++)
usePointer(shapes[i]);//指針調用
cout << endl <<"Using base-class references"<<endl;
for(i = 0;i < 2; i++)
useReference(*shapes[i]);//引用調用
return 0;
}
void usePointer( const Shape * shepe_ptr)//指針調用實現多態
{
shape_ptr -> printName();
shape_ptr -> print();
cout << endl << shape_ptr -> area()<<endl;
}
void useReference( const Shape &shape_Ref)//引用調用實現多態
{
shape_Ref.printName();
shape_Ref.print();
cout << endl << shape_Ref.area()<<endl;
}
6、多態性、虛函數、動態綁定的本質
通過上面的例子,我們應該對動態綁定的概念以及虛函數的應用有了基本掌握,但是它們到底是怎麼實現的,仍然是一個問題,下面就來介紹一下:
當編譯一個或多個虛函數的類時,C++會爲這些類建立一個虛函數表(vtable)。每次執行該類的虛函數時,程序都會用vtable來選擇恰當的實現方法,如圖(圖中的實心黑色圓點表示指針):
假如我們調用shapes[1]的printName方法,那麼實現過程如上圖加粗線表示:
2,通過復引用指針得到circle對象(我們此時仍然不知道對象類型)
3,復引用circle對象的vtable指針以得到circle vtable
4,跳過4字節得到printName函數的指針
5,復引用printName函數的指針來執行相應操作
至此,我們對虛函數的實現有了相應的認識,但要注意的一點是,動態綁定是以多次指針復引用爲代價的,因此在性能要求很高的情況下要考慮這裏面的運行時間。
如果還沒理解的話,我們可以這樣來想,爲什麼可以動態綁定函數呢?就是因爲我們在對象裏保存了它所能調用的虛函數的信息,不同類型的對象所包含的虛函數信息是不同的,因此我們可以“動態”的綁定函數。
——總結自C++編程金典