天天看點

C++虛函數簡析

  C++的虛函數是其實作多态的基礎,今天在這裡分享一下我對C++虛函數相關知識的系統總結,技術有限,如有不當,歡迎指正。

  在将内容前,将大緻涉及到的内容圖解如下:

 

C++虛函數簡析

 

1. 有無虛函數在繼承中的差別

//-- Zuo add on 2018-04-07
class A
{
public:
    virtual void fun(){ std::cout << "A::fun"; }
    void fun1(){ std::cout << "A::fun1"; }
};

class B : public A
{
public:
    virtual void fun(){ std::cout << "B::fun"; }
    void fun1(){ std::cout << "B::fun1"; }
};

void main(char argc, char** argv)
{
  A *a = new A;
  A *b = new B;
  a->fun();
  b->fun();
  a->fun1();
  b->fun1();
}
           

輸出結果:

A::fun
B::fun
A::fun1
A::fun1
           

  可以看到,如果是虛函數,在繼承的時候,如果子類有重寫,就會被覆寫(由子類的實作代替父類的實作),如果是普通成員函數,就不會被子類的實作覆寫,這也就是多态的原型。

  例子很簡單,想必大家很容易看懂,但是這裡我提出兩個問題大家思想下,後面會有答案:

  1.1. 如果将上文的

A *a = new B;

修改成

B *a = new B;

結果會怎樣變化?

  1.2. 如果虛函數有預設值,預設值會有什麼特點?

2. 虛函數的本質

  虛函數的本質是因為C++給有虛函數的類增加了一張虛函數表,用來存儲所有虛函數的入口位址。在子類繼承父類的過程中,首先繼承的是這張虛函數表Virtual-Table(以下簡稱VT),如果子類有對父類虛函數的重寫,那麼就會在VT中覆寫對應的函數位址。這樣就可以實作同樣的調用,在不同的子類裡,有不同的實作,這也就是多态。

  注意一個問題,VT是跟着類走的,也就是說,如果是上文中1.1提到的,那麼VT的覆寫就不會發生,因為

A *a = new B;

等價于

A *a = (A*)new B;

正因為VT是跟着類走的,如果是

B *a = new B;

,那就無任何特别,普通的建立對象,如果是

A *a = new B;

,那在将B類轉換成A類的時候,也就是VT合并的時候。是以上文的1.1,輸出值就和

class A

毫無關系了。

  除了上面講到的,還有兩點特性:

   2.1. 隻有虛函數的入口位址才會被存儲在VT中,如果是普通成員函數,當然不會存儲在裡面。

   2.2. 為了提高虛函數的調用效率,VT的位址被存放在類的最前面。

  我從網上找了一張圖比較明了:

  

C++虛函數簡析

  在繼承的過程在VT被子類重寫的虛函數位址覆寫父類的虛函數位址,也就是同一個指針在不同的對象中可以指向不同的函數實作,這也就是虛函數的動态綁定實作多态的過程了。這個可以和普通函數的靜态綁定相對比,普通函數是在編譯期就靜态綁定了,而虛函數是在運作期通過VT存儲的函數位址實作動态綁定。

3. 純虛函數

  純虛函數是一種比虛函數更加極端的函數。它的形式如下:

  

virtual void fun() = 0;

  它存在的目的是為了規範接口,使得子類必須要實作對應的接口,如果子類沒有實作接口,則會編譯報錯。

  使用純虛函數要注意一點,包含純虛函數的類被稱為抽象類,一般被設計為基類,且抽象類不能被執行個體化(因為有未實作的純虛函數)。

4. 安全性-通路non-public虛函數

  VT的存在固然為實作C++的多态立下汗馬功勞,但是凡事都有雙面性,它的到來,也引入了C++的一些安全上的不足。上文我們說到了,為了提高多态調用的性能,C++将VT位址存放在類空間的段首位置,是以我們通過擷取類的位址可以找到VT的位址,也就是可以得到一個類所有虛函數的位址,那如果,這裡面的虛函數有是non-public的,那就破壞了C++的封裝屬性了。Show Code:

//-- Zuo add on 2018-04-07
class A
{
private:
    virtual void fun(){ qDebug() << "A fun"; }
};

void main(char argc, char** argv)
{
    A *a = new A;
    typedef void(*fun)(void);
    std::cout << "虛函數表位址 = " << (int*)(a) << std::endl;
    std::cout << "第一個虛函數位址 = " << (int*)(*(int*)a << std::endl;
    //-- 将虛函數位址轉換為void fun(void)函數指針
    fun f = (fun)*((int*)(*(int*)a));
    f();
}
           

可以看到,這裡可以無報錯的通路到原本為private的虛函數。

5. 虛函數的預設值不能被覆寫

  虛函數雖然可以被子類所覆寫以形成多态,但是有一個細節還是要注意,虛函數的預設值是不能被覆寫的,還是上面的代碼:

//-- Zuo add on 2018-04-07
class A
{
public:
    virtual void fun(int a = ){ std::cout << "A::fun && a = " << a; }
};

class B : public A
{
public:
    virtual void fun(int a = ){ std::cout << "B::fun && a = " << a; }
};

void main(char argc, char** argv)
{
  A *a = new A;
  A *b = new B;
  a->fun();
  b->fun();
}
           

輸出結果:

A::fun && a = 
B::fun && a = 
           

可以看到這裡的

a

沒有被改變。

繼續閱讀