Java vs C++:子類覆蓋父類函數時縮小可訪問性的不同設計 頂 原 薦

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 具體情況具體分析,不虛的,文章投過來試試先,比如我們並不拒絕就某個熱點事件對其進行的報導、深入解析。

投稿方式

重要說明

  • 作者需要擁有所投文章的所有權,不能將別人的文章拿過來投遞。
  • 投遞的文章需要經過審覈,如果開源中國編輯覺得需要的話,將與作者一起進一步完善文章,意在使文章更佳、傳播更廣。
  • 文章版權歸作者所有,開源中國獲得文章的傳播權,可在開源中國各個平臺進行文章傳播,同時保留文章原始出處和作者信息,可在官方博客中標原創標籤。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章