C++基礎:多態 虛函數

面向對象程序設計中的多態性是指向不同的對象發送同一個消息,不同對象對應同一消息產生不同行爲。在程序中消息就是調用函數,不同的行爲就是指不同的實現方法,即執行不同的函數體。也可以這樣說就是實現了“一個接口,多種方法”。

  從實現的角度來講,多態可以分爲兩類:編譯時的多態性和運行時的多態性。前者是通過靜態聯編來實現的,比如C++中通過函數的重載和運算符的重載。後者則是通過動態聯編來實現的,在C++中運行時的多態性主要是通過虛函數來實現的,也正是今天我們要講的主要內容。

  1.不過在說虛函數之前,我想先介紹一個有關於基類與派生類對象之間的複製兼容關係的內容。它也是之後學習虛函數的基礎。我們有時候會把整型數據賦值給雙精度類型的變量。在賦值之前,先把整形數據轉換爲雙精度的,在把它賦值給雙精度類型的變量。這種不同類型數據之間的自動轉換和賦值,稱爲賦值兼容。同樣的,在基類和派生類之間也存在着賦值兼容關係,它是指需要基類對象的任何地方都可以使用公有派生類對象來代替。爲什麼只有公有繼承的纔可以呢,因爲在公有繼承中派生類保留了基類中除了構造和析構之外的所有成員,基類的公有或保護成員的訪問權限都按原樣保留下來,在派生類外可以調用基類的公有函數來訪問基類的私有成員。因此基類能實現的功能,派生類也可以。

  那麼它們具體是如何體現的呢?(1)派生類對象直接向基類賦值,賦值效果,基類數據成員和派生類中數據成員的值相同;(2)派生類對象可以初始化基類對象引用;(3)派生類對象的地址可以賦給基類對象的指針;(4)函數形參是基類對象或基類對象的引用,在調用函數時,可以用派生類的對象作爲實參;

#include "stdafx.h"
 #include<iostream>
 #include<string>
 
 class ABCBase
 {
 private:
         std::string ABC;    
 public:
         ABCBase(std::string abc)
         {
             ABC=abc;
         }
 void showABC();
 };
 
 void ABCBase::showABC()
 {
     std::cout<<"字母ABC=>"<<ABC<<std::endl;
 }
 
 class X:public ABCBase
 {
 public:
         X(std::string x):ABCBase(x){}
 };
 
 void function(ABCBase &base)
 {
 base.showABC();
 }
 
 
 int main()
 {
     ABCBase base("A");
 base.showABC();
 
     X x("B");
 base=x;
 base.showABC();
 
     ABCBase &base1=x;
     base1.showABC();
 
     ABCBase *base2=&x;
     base2->showABC();
 
     function(x);
 
 return0;
 }

結果:

要注意的是:第一,在基類和派生類對象的賦值時,該派生類必須是公有繼承的。第二,只允許派生類對象向基類對象賦值,反過來不允許;

  2.緊接着來講一下虛函數,它允許函數調用與函數體之間的聯繫在運行時才建立,即在運行時才決定如何動作。虛函數聲明的格式:

  virtual 返回類型 函數名(形參表)

  {

    函數體

  }

那麼定義虛函數有什麼用呢?讓我們先來看看下面這個示例:

#include "stdafx.h"
 #include <iostream>
 #include <string>
 
 class Graph
 {
 protected:
 double x;
 double y;
 public:
         Graph(double x,double y);
 void showArea();
 };
 
 Graph::Graph(double x,double y)
 {
 this->x=x;
 this->y=y;
 }
 
 void Graph::showArea()
 {
     std::cout<<"計算圖形面積"<<std::endl;
 }
 
 class Rectangle:public Graph
 {
 public:
         Rectangle(double x,double y):Graph(x,y){};
 void showArea();
 };
 
 void Rectangle::showArea()
 {
     std::cout<<"矩形面積爲:"<<x*y<<std::endl;
 }
 
 class Triangle:public Graph
 {
 public:
         Triangle(double d,double h):Graph(d,h){};
 void showArea();
 };
 
 void Triangle::showArea()
 {
     std::cout<<"三角形面積爲:"<<x*y*0.5<<std::endl;
 }
 
 class Circle:public Graph
 {
 public:
         Circle(double r):Graph(r,r){};
 void showArea();
 };
 
 void Circle::showArea()
 {
     std::cout<<"圓形面積爲:"<<3.14*x*y<<std::endl;
 }
 
 int main()
 {
     Graph *graph;
 
     Rectangle rectangle(10,5);
     graph=&rectangle;
     graph->showArea();
 
     Triangle triangle(5,2.4);
     graph=&triangle;
     graph->showArea();
 
     Circle circle(2);
     graph=&circle;
     graph->showArea();
 
 return0;
 }

結果:

結果似乎和我們想象的不一樣,既然Graph類(圖形類)的對象graph指針分別指向了Rectangle類(矩形類)對象,Triangle類(三角類)對象,以及Circle類(圓類)對象,那麼就應該執行它們自己所對應成員函數showArea(),怎麼結果會是Graph類(圖形類)的對象graph裏的成員函數呢?這好像和我們在C++之繼承與派生(2)一節裏所講到的派生類成員覆蓋了基類中使用相同名稱的成員(派生類對象調用同名成員函數是來自於自己類中成員函數,而非基類中上的)有所不同啊,其實當基類對象指針指向公有派生類的對象時,它只能訪問從基類繼承下來的成員,而不能訪問派生類中定義的成員。但是使用動態指針就是爲了表達一種動態調用的性質即當前指針指向哪個對象,就調用那個對象對應類的成員函數。那要怎麼來解決的,這時虛函數就體現出了它的作用。其實我們只需要對上一個示例代碼中所有的類裏出現的showArea()函數聲明之前加一個關鍵字virtual:

#include "stdafx.h"
 #include <iostream>
 #include <string>

 class Graph
 {
 protected:
 double x;
 double y;
 public:
         Graph(double x,double y);
 voidvirtual showArea();//定義爲虛函數或virtual void showArea()
 };
 
 Graph::Graph(double x,double y)
 {
 this->x=x;
 this->y=y;
 }
 
 void Graph::showArea()
 {
     std::cout<<"計算圖形面積"<<std::endl;
 }
 
 class Rectangle:public Graph
 {
 public:
         Rectangle(double x,double y):Graph(x,y){};
 virtualvoid showArea();//定義爲虛函數
 };
 
 void Rectangle::showArea()
 {
     std::cout<<"矩形面積爲:"<<x*y<<std::endl;
 }
 
 class Triangle:public Graph
 {
 public:
         Triangle(double d,double h):Graph(d,h){};
 virtualvoid showArea();//定義爲虛函數
 };
 
 void Triangle::showArea()
 {
     std::cout<<"三角形面積爲:"<<x*y*0.5<<std::endl;
 }
 
 class Circle:public Graph
 {
 public:
         Circle(double r):Graph(r,r){};
 virtualvoid showArea();//定義爲虛函數
 };
 
 void Circle::showArea()
 {
     std::cout<<"圓形面積爲:"<<3.14*x*y<<std::endl;
 }
 
 int main()
 {
     Graph *graph;
 
     Rectangle rectangle(10,5);
     graph=&rectangle;
     graph->showArea();
 
     Triangle triangle(5,2.4);
     graph=&triangle;
     graph->showArea();
 
     Circle circle(2);
     graph=&circle;
     graph->showArea();
 
 return0;
 }

 

其它代碼原封不動,這樣運行出來的結果就是我們所需要的:

在基類中的某成員函數被聲明爲虛函數後,在之後的派生類中科以重新來定義它。但定義時,其函數原型,包括返回類型、函數名、參數個數、參數類型的順序,都必須和基類中的原型完全相同。其實在上述修改後的示例代碼裏,只要在基類中顯式聲明瞭虛函數,那麼在之後的派生類中就需要用virtual來顯式聲明瞭,可以略去,因爲系統會根據其是否和基類中虛函數原型完全相同來判斷是不是虛函數。因此,上述派生類中的虛函數如果不顯式聲明也還是虛函數。最後對虛函數做幾點補充說明:(1)因爲虛函數使用的基礎是賦值兼容,而賦值兼容成立的條件是派生類之從基類公有派生而來。所以使用虛函數,派生類必須是基類公有派生的;(2)定義虛函數,不一定要在最高層的類中,而是看在需要動態多態性的幾個層次中的最高層類中聲明虛函數;(3)雖然在上述示例代碼中main()主函數實現部分,我們也可以使用相應圖形對象和點運算符的方式來訪問虛函數,如:rectangcle.showArea(),但是這種調用在編譯時進行靜態聯編,它沒有充分利用虛函數的特性。只有通過基類對象來訪問虛函數才能獲得動態聯編的特性;(4)一個虛函數無論配公有繼承了多少次,它仍然是虛函數;(5)虛函數必須是所在類的成員函數,而不能是友元函數,也不能是靜態成員函數。因爲虛函數調用要靠特定的對象類決定該激活哪一個函數;(6)內聯函數不能是虛函數,因爲內聯函數是不能在運行中動態確定其位置的即使虛函數在類內部定義,編譯時將其看作非內聯;(7)構造函數不能是虛函數,但析構函數可以是虛函數;

  如果在main()主函數中用new建立一個派生類無名對象和定義一個基類對象指針,並將無名對象的地址賦給基類對象指針時,當我們用delete運算符來撤銷無名對象時,系統只執行基類析構函數,而不執行派生類析構函數。比如:

 

 

 #include "stdafx.h"
 #include <iostream>
 #include <string>
 
 
 class Graph
 {
 protected:
 double x;
 double y;
 public:
         Graph(double x,double y);
 voidvirtual showArea();//定義爲虛函數或virtual void showArea()
 ~Graph();
 };
 
 Graph::Graph(double x,double y)
 {
 this->x=x;
 this->y=y;
 }
 
 void Graph::showArea()
 {
     std::cout<<"計算圖形面積"<<std::endl;
 }
 
 Graph::~Graph()
 {
     std::cout<<"調用圖形類析構函數"<<std::endl;
 }
 
 class Rectangle:public Graph
 {
 public:
         Rectangle(double x,double y):Graph(x,y){};
 virtualvoid showArea();//定義爲虛函數
 ~Rectangle();
 };
 
 void Rectangle::showArea()
 {
     std::cout<<"矩形面積爲:"<<x*y<<std::endl;
 }
 
 Rectangle::~Rectangle()
 {
     std::cout<<"調用矩形類析構函數"<<std::endl;
 }
 
 int main()
 {
     Graph *graph;
     graph=new Rectangle(10,5);
     graph->showArea();
 
     delete graph;
 
 return0;
 }

結果:

因爲在撤銷指針graph所指的派生類對象,在調用析構函數時,採用靜態聯編,只調用了Graph類的析構函數。如果也想調用派生類Rectangle類的析構函數的話,可將Graph類的析構函數定義爲虛析構函數。其定義的一般格式:

  virtual ~類名()

  {

    函數體

  };

雖然派生類的析構函數與基類的析構函數名字不同,但是如果將基類的析構函數定義爲虛函數,由該基類派生而來的所有派生類的析構函數都自動成爲虛函數。我們把上一示例中的Graph類的析構函數前加上關鍵字virtual,那麼執行結果:

顯然這個結果纔是我們所需要的。

 

3.上述示例中用了虛函數後,會發現其實Graph類(圖形類)中的虛函數的函數體根本沒有被用到過,就算被用到,該基類體現了圖形的抽象的概念,並不與具體事物相聯繫。所以基類中的虛函數也沒有實質性的功能。因此我們只需要在基類中留下一個函數名,而具體的實現留給派生類去定義。在C++中就是用純虛函數來說明的。純虛函數的一般形式:

  virtual 返回類型 函數名(形參表)=0;

這裏的"=0"並不是函數的返回值等於零,它只是起到形式上的作用,告訴編譯系統"這是純虛函數"。純虛函數不具備函數功能,不能被調用。

 

class Graph
 {
 protected:
 double x;
 double y;
 public:
         Graph(double x,double y);
 voidvirtual showArea()=0;//定義純虛函數
 };
 
 Graph::Graph(double x,double y)
 {
 this->x=x;
 this->y=y;
 }


4.如果一個類中至少有一個純虛函數,那麼就稱該類爲抽象類。所以上述中Graph類就是抽象類。對於抽象類有以下幾個注意點:(1)抽象類只能作爲其他類的基類來使用,不能建立抽象類對象;(2)不允許從具體類中派生出抽象類(不包含純虛函數的普通類);(3)抽象類不能用作函數的參數類型、返回類型和顯示轉化類型;(4)如果派生類中沒有定義純虛函數的實現,而只是繼承成了基類的純虛函數。那麼該派生類仍然爲抽象類。一旦給出了對基類中虛函數的實現,那麼派生類就不是抽象類了,而是可以建立對象的具體類;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章