本篇文章列出一些 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 而子類無定義時,子類會沿用父類的定義
當我們刻意省略 Rectangle
的 area
定義時,可以發現基類指針會綁定到基類的函式。
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 = ▭
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;
}
Pingback: 還是搞不懂 virtual function 的概念嗎?來看這篇吧!簡明 C++ virtual function 機制與概念