天天看點

C++:44---關鍵字virtual、override、final

一、虛函數

  • 概念:在函數前面加virtual,就是虛函數

虛函數的一些概念:

  • 隻有成員函數才可定義為虛函數,友元/全局/static/構造函數都不可以
  • 虛函數需要在函數名前加上關鍵字virtual
  • 成員函數如果不是虛函數,其解析過程發生在編譯時而非運作時
  • 派生類可以不覆寫(重寫)它繼承的虛函數

重寫(覆寫)的概念與規則

  • 派生類重寫(覆寫)基類中的函數,其中函數名,參數清單,傳回值類型都必須一緻,并且重寫(覆寫)的函數是virtual函數
  • 虛函數在子類和父類中的通路權限可以不同
  • 相關規則:
  • ①如果虛函數的傳回值類型是基本資料類型:傳回值類型必須相同
  • ②如果虛函數的傳回值類型是類本身的指針或引用:傳回值類型可以不同,但派生類的傳回值類型小于基類傳回值類型
  • 基類與派生類的虛函數名與參數清單相同,至于參數清單為什麼一緻是為了避免虛函數被隐藏 
  • 函數傳回值有以下要求:
class A {
public:
int a;
public:
A(int num) :a(num) {};
virtual A& func() {}; //虛函數
};
class B:public A{
public:
int b;
public:
B(int num) :A(num) {};
virtual B& func() {}; //重寫了基類的虛函數
};      

二、為什麼要設計虛函數

  • 我們知道派生類會擁有基類定義的函數,但是對于某些函數,我們希望派生類各自定義适合于自己版本的函數,于是基類就将此函數定義為虛函數,讓派生類各自實作自己功能版本的函數(但是也可以不實作)
  • 我們通常在類中将這兩種成員函數分開來:
  • 一種是基類希望派生類進行覆寫的虛函數
  • 一種是基類希望派生類直接繼承而不要改變的函數

三、覆寫(重寫)

  • 概念:基類的虛函數,如果派生類有相同的函數,則子類的方法覆寫了父類的方法

覆寫(重寫)與隐藏的關系:

覆寫與隐藏都是子類出現與父類相同的函數名,但是有很多的不同

  • 隐藏可以适用于成員變量和函數,但是覆寫隻能用于函數
  • 覆寫(重寫)在多态中有很重要的作用

四、virtual、override關鍵字

virtual:

  • 放在函數的傳回值前面,用于表示該成員函數為虛函數
  • 父類虛函數前必須寫;子類虛函數前可以省略(不困省不省略,該函數在子類中也是虛函數類型)
  • virtual隻能出現在類内部的聲明語句之前而不能用于類外部的函數定義

override:

  • 父類的虛函數不可使用
  • 放在子類虛函數的參數清單後(如果函數有尾指傳回類型,那麼要放在尾指傳回類型後),用來說明此函數為覆寫(重寫)父類的虛函數。如果類方法在類外進行定義,那麼override不能加
  • 不一定強制要求子類聲明這個關鍵字,但是建議使用(見下面的五)
  • 這是C++11标準填入的

override設計的最初原因:

  • 有些情況下,我們的父類定義了一個虛函數,但是子類沒有覆寫(重寫)這個虛函數,而子類中卻出現了一個與基類虛函數名相同、但是參數不同的函數,這仍是合法的。編譯器會将派生類中新定義的這個函數與基類中原有的虛函數互相獨立,這時,派生類的函數沒有覆寫掉基類的虛函數版本,雖然程式沒有出錯,但是卻違反了最初的原則
  • 是以C++11标準添加了一個override關鍵字放在派生類的虛函數後,如果編譯器發現派生類重寫的虛函數與基類的虛函數不一樣(參數或其他不一樣的地方),那麼編譯器将報錯
class A{
virtual void f1(int) const;
virtual void f2();
void f3();
};
calss B:public A{
void f1(int)const override; //正确
void f2(int)override;       //錯誤,參數不一緻
void f3()override;          //錯誤,f3不是虛函數
void f4()override;          //錯誤,B沒有名為f4的函數
};      

五、禁止覆寫(final關鍵字)

  • 如果我們定義的一個虛函數不想被派生類覆寫(重寫),那麼可以在虛函數之後添加一個final關鍵字,聲明這個虛函數不可以被派生類所覆寫(重寫)
  • 如果函數有尾指傳回類型,那麼要放在尾指傳回類型後

示範案例

class A
{
virtual void func1()final {};
};
class B:public A
{
virtual void func1()override {}; //報錯,func1被A聲明為final類型
};
class A
{
virtual void func1() {};
};
class B:public A
{
virtual void func1()override final {}; //正确
};
class C :public B
{
virtual void func1()override {}; //報錯,func1被B聲明為final類型
};      

六、虛函數的預設實參

  • 和其他函數一樣,虛函數也可以擁有預設實參,使用規則如下:
  • 如果派生類調用虛函數沒有覆寫預設實參,那麼使用的參數是基類虛函數的預設實參;如果覆寫了虛函數的預設實參,那麼就使用自己傳入的參數
  • 派生類可以改寫基類虛函數的預設實參,但是不建議,因為這樣就違反了預設實參的最初目的
  • 建議:如果虛函數使用了預設實參,那麼基類和派生類中定義的預設實參最好一緻
class A
{
virtual void func1(int a, int b = 10) {};
};
class B:public A
{
virtual void func1(int a,int b=10)override {}; //沒有改變
};
class C :public B
{
virtual void func1(int a, int b = 20)override {}; //改變了預設實參,不建議
};
class D :public C
{
virtual void func1(int a, int b)override {}; //删去了預設實參,那麼在調用fun1時,必須傳入a和b
};      

七、動态綁定

  • 概念:當某個虛函數通過指針或引用調用時,編譯器産生的代碼直到運作時才能确定到該調用哪個版本的函數(根據該指針所綁定的對象)
  • 必須清楚動态綁定隻有當我們通過指針或引用調用“虛函數”時才會發生,如果通過對象進行的函數調用,那麼在編譯階段就确定該調用哪個版本的函數了(見下面的示範案例)
  • 當然,如果派生類沒有重寫基類的虛函數,那麼通過基類指針指向于派生類時,調用虛函數還是調用的基類的虛函數(因為派生類沒有重寫)
  • 動态綁定與“派生類對象轉換為基類對象”是相似的,原理相同 

示範案例

class A
{
public:
void show()const{
cout << "A";
};
};
class B :public A //B繼承于A
{
public:
void show()const{
cout << "B";
};
};
void printfShow(A const& data)
{
data.show();
}
int main()
{
A a;
B b;
printfShow(a);
printfShow(b);
return 0;
}      
  • 上面的程式中,B繼承于A,并且B隐藏了A的show()函數。當我們運作程式時,可以看到程式列印的是“AA”。是以可以得出,非虛函數的調用與對象無關,而是取決于類的類型(這個在程式的編譯階段就已經确定了),此處函數的參數類型為A,所有列印的永遠是A裡面的show()函數
  • 現在我們修改程式,将基類A的show函數改為虛函數形式
class A
{
public:
virtual void show()const{
cout << "A";
};
};      
  • 現在再來運作程式,可以看到程式列印的是“AB”。這就是動态綁定産生的效果,對于虛函數的調用是在程式運作時才決定的

八、回避虛函數的機制

  • 上面我們介紹過,我們通過指針調用虛函數,會産生動态綁定,隻有當程式運作時才回去确定到底該調用哪個版本的函數
  • 某些情況下,我們希望對虛函數的調用不要進行動态綁定,而是強迫其執行虛函數的某個特定版本。這種方式的調用是在編譯時解析的。方法是通過域運算符來實作
  • 通常,隻有成員函數(或友元)中的代碼才需要使用作用域運算符來回避虛函數的機制
  • 什麼時候需要用到這種回避虛函數的機制:
  • 通常,基類定義的虛函數要完成繼承層次中所有的類都要完成的共同的任務,而各個派生類在虛函數中各自添加自己的功能。此時,派生類希望使用基類的虛函數來完成大家共同的任務,那麼就通過域運算符來調用基類的虛函數
#include <iostream>
using namespace::std;
class A
{
public:
virtual void func1() { cout << "A" << endl; };
};
class B:public A
{
public:
virtual void func1()override { cout << "B" << endl; };
};
int main()
{
A *p;
B b;
p = &b;
p->A::func1();   //正确,列印A
//p->B::func1(); //錯誤的用法
p->func1();      //正确,列印B
return 0;
}