Java 和 C++ 都是面向對象的語言,允許對象之間的繼承。兩個語言的繼承都設置有允許子類覆蓋父類的“虛函數”,加引號是因爲 Java 中沒有虛函數這一術語,但是我們的確可以把 Java 的所有函數等同於虛函數,因爲 Java 類的所有非 static 函數都可以被子類覆蓋,這裏僅借用“虛函數”這一名詞的含義,不深究語言的術語問題。
Java 和 C++ 都允許在子類覆蓋父類時,改變函數的可訪問性。所謂“可訪問性”,就是使用 public 、protected、private 等訪問控制符進行修飾,用來控制函數能否被訪問到。通常可訪問性的順序爲(由於 C++ 中沒有包的概念,因此暫不考慮包訪問控制符,這並不影響這裏的討論):
public > protected > private
以 Java 爲例:
class Base {
protected void sayHello() {
System.out.println("Hello in Base");
}
}
class Child extends Base {
public void sayHello() {
System.out.println("Hello in Child");
}
}
注意這裏的 sayHello() 函數,父類 Base 中,該函數使用 protected 訪問控制符進行修飾,而子類將其改用 public,這不會有任何問題。子類對父類函數覆蓋時,擴大可訪問性,通常都不是問題。
本文要講的是,當子類對父類函數覆蓋的可訪問性縮小時,Java 和 C++ 採取了不同的策略。
首先以 Java 爲例,看下面的代碼:
class Base {
public void sayHello() {
System.out.println("Hello in Base");
}
}
class Child extends Base {
private void sayHello() {
System.out.println("Hello in Child");
}
}
上面的代碼中,第 8 行 **private void sayHello() {**會有編譯錯誤,導致這段代碼根本不能通過編譯。因爲 Java 不允許子類在覆蓋父類函數時,縮小函數的可訪問性,至於原因,我們可以用一個例子來說明。
例如我們在外部調用時使用下面的代碼:
Base base = new Base();
base.sayHello();
base = new Child();
base.sayHello();
假如之前的代碼可以通過編譯,那麼就存在這麼一種可能:由於 Java 是運行時綁定,當 base 指向 new Base() 時, sayHello() 是可以訪問到的,但是當 base 指向 new Child() 時,sayHello() 卻無法訪問到!在 Java 看來這是一個矛盾,應該避免出現這種問題,因此,Java 從編譯器的角度規定我們不能寫出上面的代碼。
而在 C++ 中,情況就不同了,來看 C++ 的例子:
class Base {
public:
virtual void sayHello() {
std::cout << "Hello in Base";
}
}
class Child : public Base {
private:
void sayHello() {
std::cout << "Hello in Child";
}
}
這段代碼在 C++ 中是完全正確的,可以通過編譯。注意,這裏的子類在覆蓋父類函數時,縮小了可訪問性。如果你沒有看出有什麼問題,那麼我們完全可以在外部調用時使用下面的代碼:
Child child;
child.sayHello(); // 不能通過編譯,因爲 sayHello() 是 private 的
static_cast<Base&>(child).sayHello(); // 可以通過編譯,因爲 sayHello() 是 public 的
第 2 行調用是失敗的,因爲在 Child 中,sayHello() 是 private 的,不能在外部調用。然而,當我們使用 static_cast 運算符將 Child 強制轉換成 Base 類型時,事情發生了改變——對於 Base 而言,sayHello() 是 public 的,因此可以正常調用。
針對這一點,C++ 標準的《Member access control》一章中《Access to virtual functions》一節可以找到如下的例子:
class B {
public:
virtual int f();
};
class D : public B {
private:
int f();
};
void f() {
D d;
B* pb = &d;
D* pd = &d;
pb->f(); // OK: B::f() is public, D::f() is invoked
pd->f(); // error: D::f() is private
}
對此,C++ 標準給出的解釋是:
Access is checked at the call point using the type of the expression used to denote the object for which the member function is called ( B* in the example above). The access of the member function in the class in which it was defined (D in the example above) is in general not known.
簡單翻譯過來有兩條要點:
- 訪問控制是在調用時檢查的,也就是說,誰調用了這個函數,就檢查誰能不能訪問這個函數。
- 成員函數的可訪問性一般是不知道的,也就是說,運行時檢查可訪問性時,並不能知道這個函數在定義時到底是 public 的還是 private 的。
正因如此,C++ 的調用方可以通過一些技巧性轉換,“巧妙地”調用到原本無法訪問的函數。一個現實的例子是:在 Qt 裏面,QObject::event() 函數是 public 的,而其子類 QWidget 的 event() 函數則改變成 protected。具體細節可以閱讀 Qt 的相關代碼。
總結來說,在子類覆蓋父類函數時,Java 嚴格限制了子類不能縮小函數可訪問性,但 C++ 無此限制。
個人認爲,從軟件工程的角度來說,Java 的規定無疑更具有工程上面的意義,函數的調用也更加一致。C++ 的標準則會明顯簡化編譯器實現,但是對工程而言並不算很好的參考。畢竟,一個明顯標註了 private 的函數,無論任何情況都不應該允許在外部被調用。
PS:C++ 標準的正式版是需要購買的,但是草案可以免費下載。C++ 標準草案的下載地址可以在下面的頁面找到:https://isocpp.org/std/the-standard
作者介紹
程樑,軟件工程師。目前專注於 Angular 項目研發,同時對 Java 服務器端開發、Qt 桌面開發等都有濃厚的興趣,個人博客 https://www.devbean.net。
本文系作者投稿文章。歡迎投稿。
投稿內容要求
- 互聯網技術相關,包括但不限於開發語言、網絡、數據庫、架構、運維、前端、DevOps(DevXXX)、AI、區塊鏈、存儲、移動、安全、技術團隊管理等內容。
- 文章不需要首發,可以是已經在開源中國博客或網上其它平臺發佈過的。但是鼓勵首發,首發內容被收錄可能性較大。
- 如果你是記錄某一次解決了某一個問題(這在博客中佔絕大比例),那麼需要將問題的前因後果描述清楚,最直接的就是結合圖文等方式將問題復現,同時完整地說明解決思路與最終成功的方案。
- 如果你是分析某一技術理論知識,請從定義、應用場景、實際案例、關鍵技術細節、觀點等方面,對其進行較爲全面地介紹。
- 如果你是以實際案例分享自己或者公司對諸如某一架構模型、通用技術、編程語言、運維工具的實踐,那麼請將事件相關背景、具體技術細節、演進過程、思考、應用效果等方面描述清楚。
- 其它未盡 case 具體情況具體分析,不虛的,文章投過來試試先,比如我們並不拒絕就某個熱點事件對其進行的報導、深入解析。
投稿方式
- 以 Word 或者 Markdown 文檔的形式將稿件投遞到 [email protected] 郵箱
重要說明
- 作者需要擁有所投文章的所有權,不能將別人的文章拿過來投遞。
- 投遞的文章需要經過審覈,如果開源中國編輯覺得需要的話,將與作者一起進一步完善文章,意在使文章更佳、傳播更廣。
- 文章版權歸作者所有,開源中國獲得文章的傳播權,可在開源中國各個平臺進行文章傳播,同時保留文章原始出處和作者信息,可在官方博客中標原創標籤。