1. 程式人生 > >PL真有意思(七):資料抽象和麵向物件

PL真有意思(七):資料抽象和麵向物件

前言

在之前的名字、作用域那篇提到模組型別,它使程式設計師可以從一個給定抽象出發,通過例項化產生多個例項;再後面是類,它使程式設計師可以定義一族相關的抽象。

在這一篇裡,我們會來看一下面向物件程式設計及其三個基本概念、動態方法約束、多重繼承等等

面向物件程式設計

隨著軟體變得越來越複雜,資料抽象已經變成了軟體工程中最重要的部分。由模組和模組型別提供的這種抽象至少帶來了如下三個好處:

  • 它可以減少程式設計師必須同時考慮的細節量,減少了人的概念負擔
  • 它起到一種故障遏制作用,可以防止程式設計師以不適當的方式使用程式的各種部件。也限制了查詢程式錯誤必須考慮的程式的部分
  • 它為程式部件之間的獨立性提供了一個重要的層次,使得將程式的構造分配到各個單獨的部分更加容易,在修改部件的內部實現就可以避免改動使用它們的外部程式碼

但是很顯然,第三點在實踐中卻很難做到。因為或許我們有一個原先做好的模組幾乎具有當前某個新應用所需的全部性質,但是並不完全適用。假如我們有一個佇列抽象,但是需要的卻是能夠在兩端插入刪除,那麼就不完全符合了

面向物件的程式設計可以看作是這個方向上的一種努力,它使我們能夠更容易的擴充套件或精化現有抽象的方式定義新抽象,也就是繼承。

封裝和繼承

封裝機使程式設計師可以將資料和操作它們的子程式組織在一起,對抽象的使用者隱藏起各種無關緊要的實現細節。

隨著繼承的引入,面嚮物件語言不但要支援基於模組的語言的作用域規則,還需要處理另外一些問題,比如基類中的私有成員對於派生類的方法應該是可見的嗎?基類中的公用成員在派生類中也總是公用的嗎?

在C++中可見性規則背後的基本原則可以總結如下

  • 任何一個類都可以限制其成員的可見性。只要該類宣告在作用域中,其公用成員就都是可見的。私用成員只在本類的方法中可見。保護成員在本類及其派生類中可見

  • 派生類可以顯式其基類成員的可見性,但是不能提升它們的可見性。基類私用成員在派生類中根本不可見。公用基類的保護成員和公用成員,在派生類中仍然分別是保護的和公用的成員。

  • 如果一個派生類通過將基類宣告的protected或private而限制了基類成員的可見性,那麼在這個派生類中還可以一個一個恢復基類成員的可見性。

巢狀類

許多語言都允許類宣告的巢狀。這帶來了一個直接的問題:如果Inner是Outer的成員,那麼Inner的方法能夠看到Outer的成員嗎?

在C++和C#中,只允許訪問外層類的靜態成員。而Java中則更復雜,它允許巢狀類訪問外層類的任意成員。因此內層類的每個例項都必須屬於外層類的一個例項。

初始化和終結處理

我們將物件的生存期定義為它佔據空間並因此能儲存資料的那段時間。大多數面向物件的語言都提供了某種特殊機制,用以在物件的生存期開始時自動做初始化。也有幾種語言提供了類似的解構函式機制,用於在物件生存期結束時自動來終結它。

建構函式的選擇

C++、Java和C#都允許程式為一個類指定多個建構函式。多個建構函式的行為方式就像是過載的子程式,必須能根據其引數的個數和型別區分它們。

執行順序

C++強調每個物件都要在使用前初始化,進一步說,如果該物件的類派生自另一個類,C++強調必須在呼叫派生類的建構函式之前呼叫基類的建構函式。以保證派生類不會看到它所繼承的域處於不一致的狀態。

在Java中

super(args);

super關鍵字用於引用當前類的基類。如果沒有這種對super的呼叫,Java編譯器就會自動插入一個對基類的無參建構函式的呼叫。

廢料收集

當一個C++物件被銷燬時,首先會呼叫它所在派生類的解構函式,然後按照與派生類相反的順序呼叫各基類的解構函式。在C++中,解構函式最常見的用途就是手工釋放儲存空間。

但是現在的許多語言都提供了自動廢料收集

動態方法約束

假設我們現在有三個類:

class Persopn {}
class Student : public Persion {}
class Professor : public Persion {}

Student s;
Professor p;

Persopn *x = &s;
Persopn *y = &p

現在假設三個類都有一個print_mailing_label方法,那麼對x,y呼叫這個方法將會呼叫的是基類的Persion的方法,還是根據現在變數引用的s、p的型別來做選擇呢?

第一種選擇是靜態方法約束,而第二種方法是動態方法約束。動態方法約束是面向物件程式設計的核心概念

虛方法和非虛方法

C++和C#預設使用靜態方法約束,但是程式設計師可以將特定的方法標記為virtual,要求對它使用動態約束。對虛方法的呼叫將在執行時根據物件的類而不是引用的型別指派適當的方法實現

抽象類

在大多數面嚮物件語言中,基類中都可以不給出virtual方法的體。在Java和C#中,做這件事的方法是將類或沒有體的方法都標記為abstract

無論用什麼語法形式,如果一個類中包含了至少一個抽象方法,這個類就稱為是抽象的。我們不能夠宣告抽象類的物件

成員查詢

對於靜態方法約束,編譯器總可以基於所引用的變數的型別確定應該呼叫相應的方法的哪個版本。然而,對於動態約束,被引用或指標變數所引用的物件中就必須包含足夠的資訊,使編譯器生成的程式碼能夠在執行時找到正確的方法版本。

最常見的實現方式是用記錄的形式表示每個物件,這種記錄中第一個域是一個指標,指向該物件的類的虛方法表。虛表也就是一個數組,其中的第i個項指明該物件的第i個虛方法的程式碼地址。同一個類的所有物件共享同一個虛表。

多型性

動態方法約束將多型性引入到期望某個基類foo的物件引用的所有程式碼中。只要派生類的物件支援這個基類的操作,這些程式碼對於基類的任何派生類的物件都可以很好的工作。

有人可能會認為,有了繼承和動態方法約束後就不再需要泛型了,但實際情況並非如此,為了訪問這些派生類的特殊內容,就必須進行強制轉換,並且得到的程式碼仍然是不安全的,但是泛型能夠解決這些問題

多重繼承

有些時候,讓一個派生類繼承多個基類的特徵也是非常有用的。例如我們需要一個學生類,又希望能夠方便進行增加刪除,那麼就可能希望從Person類和連結串列類派生出一個類來。

C++和Python都有多重繼承。Java、C#則只提供了一種受限的多重繼承方式。

總結

在這一篇的一開始我們指出了面向物件程式設計的三大基本概念:封裝、繼承和多型。在之後我們討論了物件的初始化和終結操作、動態方法約束和多重繼承