深入 virtual function:搞懂 virtual Function 的使用規則

本篇文章列出一些 virtual function 相關的使用規則,以及不常用到的使用方式,當作個人筆記記錄在此,以防未來失憶,可以回來翻一翻。若對 virtual function 以及抽象基類 (abstract base class) 的概念還不甚了解,可以參考這篇文章:簡明 C++ virtual function 的機制與概念

virtual function 只會作用於指標或是引用

延續簡明 C++ virtual function 的機制與概念的例子,當我們用普通的基類物件,如 Shape,承接衍生類的物件,此時不會發生 dynamic binding 的機制,只會呼叫基類版本的函式。只有使用基類指標或是基類引用時,才會啟用 virtual function 的機制。

#include "Shape.h"
#include <iostream>

int main()
{
    Rectangle r(3, 4, "3x4 rectangle");
    Shape s1 = r;
    Shape *s2 = &r;
    Shape &s3 = r;

    print_shape(std::cout, s1);
    /*
    Name: 3x4 rectangle
    n: 4
    Area: -1
    Perimeter: -1
    */
    print_shape(std::cout, *s2);
    /*
    Name: 3x4 rectangle
    n: 4
    Area: 12
    Perimeter: 14
    */
    print_shape(std::cout, s3);
    /*
    Name: 3x4 rectangle
    n: 4
    Area: 12
    Perimeter: 14
    */

    return 0;
}

override 和 final 說明符

override 以及 final 說明符應該放置在成員函式之後,其中 override 雖然非必要,但可以幫助我們更容易找出程式碼的錯誤,並且讓程式員更好地理解程式碼。假設我們的衍生類定義了一個與基類某虛函式一模一樣名稱、但是參數不同的函式,這種定義是合法的,編譯器會將這個函式與虛函式視為獨立的個體,並沒有真正執行到覆寫的動作。若程式碼出問題時,我們可能無法快速地找出錯誤的地方。但是若我們在函式宣告的地方加入 override 說明符,那編譯器會預設此函式應該要覆寫某個父類函式,當編譯器發現函式與父類函式不匹配時會主動報錯,讓我們可以快速 debug。另外需要註明的是,override 只能用於 virtual function。

class Shape
{
public:
    Shape();
    ~Shape() = default;
    virtual float area();
    virtual float perimeter();
    friend void print_shape(std::ostream &os, Shape &shape);
    
protected:
    short n;
    std::string name;
};

class Rectangle: public Shape
{
public:
    Rectangle() = default;
    Rectangle(float w, float h, std::string name);
    ~Rectangle() = default;
    float area() override;
    float perimeter() override;
    friend void print_shape(std::ostream &os, Shape &shape);
    
protected:
    float w, h;
};

final 的說明符用於標明此函式將要覆寫某父類函式,並且防止任何未來子類函式再次被覆寫,因此 final 的功能有與 override 重疊的地方,額外再加上防止被子類覆寫。延續上面 Shape.h 的範例,若我們希望 area() 這個函式未來被繼承後不要再被改動,我們可以在宣告時加上 final 說明符:float area() override final;,或是 float area() final。這兩個方式都可以,對我來說 override 的功能被包含在 final 裡面,所以我自己覺得同時出現這兩個說明符有點冗長,個人偏好只寫 final。但也有人認為同時出現會讓程式碼更易讀,這個應該就是見仁見智了。

父類有定義 virtual function 而子類無定義時,子類會沿用父類的定義

當我們刻意省略 Rectanglearea 定義時,可以發現基類指針會綁定到基類的函式。

class Rectangle: public Shape
{
public:
    Rectangle() = default;
    Rectangle(float w, float h, std::string name);
    ~Rectangle() = default;
    //float area() override final;
    //float perimeter() override final;
    friend void print_shape(std::ostream &os, Shape &shape);
    
protected:
    float w, h;
};
#include "Shape.h"
#include <iostream>

int main()
{
    Rectangle rect(3, 4, "3x4 rectangle");
    print_shape(std::cout, rect);
     /*
    Name: 3x4 rectangle
    n: 4
    Area: -1
    Perimeter: -1
    */

    return 0;
}

迴避 virtual 機制

如果要迴避 virtual 機制的話,就如同迴避覆寫機制一樣,我們只要在前面標示父類的作用域,系統就會呼叫父類的 virtual function 了。

#include "Shape.h"
#include <iostream>

int main()
{
    Rectangle rect(3, 4, "3x4 rectangle");
    Rectangle *r = &rect;
    std::cout << "Base class area: " << r->Shape::area() << std::endl;
    //Base class area: -1

    return 0;
}

子類的 virtual function 參數以及回傳類形必須和父類一模一樣

需要注意的是,子類的虛函式回傳類型、輸入類型,必須與父類一模一樣,否則編譯會報錯:

class Rectangle: public Shape
{
public:
    Rectangle() = default;
    Rectangle(float w, float h, std::string name);
    ~Rectangle() = default;
    float area(int) override; 
    //error: 'float Rectangle::area(int)' marked 'override', but does not override
    //27 |  float area(int) override;
    //   |        ^~~~
    float perimeter() override;
    friend void print_shape(std::ostream &os, Shape &shape);
    
protected:
    float w, h;
};

上述有唯一的例外,如果成員含式回傳的是本身類型的指標或引用時,可以依據各衍生類的類型定義,如下程式碼。

class Shape
{
public:
    Shape(): n(-1), name("NaN") {}
    Shape(short n, std::string name);
    ~Shape() = default;
    virtual Shape* area() = 0;
    virtual Shape* perimeter() = 0;
    friend void print_shape(std::ostream &os, Shape &shape);
    
protected:
    short n;
    std::string name;
};

class Rectangle: public Shape
{
public:
    Rectangle() = default;
    Rectangle(float w, float h, std::string name);
    ~Rectangle() = default;
    Rectangle *area() override;
    Rectangle * perimeter() override;
    friend void print_shape(std::ostream &os, Shape &shape);
    
protected:
    float w, h;
};

不過需要注意的是,如果 virtual function 是回傳本身指針或引用,並且衍生類想要覆寫它,我們必須確保子類到父類的轉換是可訪問的。從外部來看要確保可訪問性,子類繼承父類的方式必須是 public。從內部來看 (子類的成員函式或友元),不論子類是用何種方式繼承父類,都可以訪問,但子類的子類 (孫類) 就不同了,孫類若是想要訪問子類對父類的轉換,子類繼承父類的方式必須是 public 或是 protected。舉例來說,B 類型以 private 的方式繼承 A 類型,但因為從內部來看,B 是可以向 A 轉換的,因此以下的 virtual function override 是可以成功的。但 C 類型從 B 繼續繼承,想要 override virtual function 時會發生錯誤,因為 B 類型以 private 的方式繼承 A 類型,但孫類 C 無法訪問子類對基類的轉換。詳細的繼承規則可見:c++ 繼承規則總整理

class A
{
public:
    virtual A* func()
    {
        return this;
    }
};

class B: private A
{
public:
    B* func() override
    {
        return this;
    }
};

class C: public B
{
public:
    C* func() override
    {
        return this;
    }
};
int main()
{
    A *a;
    B *b;
    C c;
    //a = &c;
    /*
    error: 'A' is an inaccessible base of 'C'
    34 |     a = &c;
       |          
    */
    b = &c;
    // pass!

    return 0;
}
Show 1 Comment

1 Comment

Comments are closed