跳轉到內容

C++ 程式設計/類/多型

來自 Wikibooks,開放世界中的開放書籍

動態多型(重寫)

[編輯 | 編輯原始碼]

到目前為止,我們已經瞭解到可以透過繼承在類中新增新的資料和函式。但是,如果我們希望派生類繼承基類的方法,但擁有不同的實現方式呢?這就是我們在談論多型性的時候,它是 OOP 程式設計中的一個基本概念。

如之前在 程式設計正規化部分 所見,多型性 分為兩個概念:靜態多型性動態多型性。本節重點介紹動態多型性,它在派生類重寫基類中宣告的函式時適用於 C++。

我們透過在派生類中重新定義方法來實現這個概念。但是,我們在這樣做時需要考慮一些因素,因此現在我們必須介紹動態繫結、靜態繫結和虛方法的概念。

假設我們有兩個類,ABB派生自A,並重新定義了存在於類A中的方法c()的實現。現在假設我們有一個類B的物件b。指令b.c()應該如何解釋?

如果b在堆疊中宣告(不是作為指標或引用宣告),編譯器會應用靜態繫結,這意味著它會解釋(在編譯時)我們指的是存在於B中的c()的實現。

但是,如果我們將b宣告為類A的指標或引用,編譯器在編譯時無法知道要呼叫哪個方法,因為b可以是AB型別。如果這在執行時解決,則將呼叫存在於B中的方法。這稱為動態繫結。如果這在編譯時解決,則將呼叫存在於A中的方法。這同樣是靜態繫結。

虛成員函式

[編輯 | 編輯原始碼]

成員函式相對簡單,但經常被誤解。這個概念是設計類層次結構中子類化類的一個重要組成部分,因為它決定了特定上下文中的重寫方法的行為。

虛成員函式是類成員函式,可以在從宣告它們的類派生的任何類中被重寫。然後,成員函式體被替換為派生類中的一組新的實現。

注意
重寫虛擬函式時,可以更改派生類成員函式的私有、受保護或公共狀態訪問狀態。

透過在方法宣告之前放置關鍵字,我們表明當編譯器必須決定應用靜態繫結還是動態繫結時,它將應用動態繫結。否則,將應用靜態繫結。

注意
雖然在子類定義中使用虛擬關鍵字不是必需的(因為如果基類函式是虛擬的,所有子類對其的重寫也將是虛擬的),但在為將來的重複利用(用於在同一專案之外使用)生成程式碼時,這樣做是一種很好的風格。

再次,這應該用一個例子來解釋

class Foo
{
public:
  void f()
  {
    std::cout << "Foo::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Foo::g()" << std::endl;
  }
};
 
class Bar : public Foo
{
public:
  void f()
  {
    std::cout << "Bar::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Bar::g()" << std::endl;
  }
};
 
int main()
{
  Foo foo;
  Bar bar;

  Foo *baz = &bar;
  Bar *quux = &bar;

  foo.f(); // "Foo::f()"
  foo.g(); // "Foo::g()"
 
  bar.f(); // "Bar::f()"
  bar.g(); // "Bar::g()"

  // So far everything we would expect...
 
  baz->f();  // "Foo::f()"
  baz->g();  // "Bar::g()"

  quux->f(); // "Bar::f()"
  quux->g(); // "Bar::g()"
 
  return 0;
}

我們對兩個物件的第一次呼叫f()g()很簡單。然而,當我們使用指向 Foo 型別的指標 baz 時,事情變得有趣起來。

f()不是,因此對f()的呼叫將始終呼叫與指標型別關聯的實現 - 在這種情況下是來自 Foo 的實現。

注意
記住,過載重寫是不同的概念。

虛擬函式呼叫在計算上比普通函式呼叫更昂貴。虛擬函式使用指標間接定址,呼叫,並且需要比普通成員函式多幾條指令。它們還要求任何包含虛擬函式的類/結構的建構函式初始化一個指向其虛成員函式的指標表。

所有這些特性都將在效能和設計之間產生權衡。應該避免在沒有現有結構需求的情況下預先宣告虛擬函式。請記住,僅在執行時解析的虛擬函式無法內聯。


Clipboard

要做
虛擬函式和內聯問題的示例。


注意
使用類模板可以解決使用虛擬函式的一些需求。當我們介紹模板時,將對此進行介紹。

純虛成員函式

[編輯 | 編輯原始碼]

還有一種有趣的可能性。有時我們不想提供函式的任何實現,而是希望要求對我們的類進行子類化的人自己提供實現。這就是虛擬函式的情況。

要指示一個純函式而不是實現,我們只需在函式宣告之後新增一個"= 0"。

再次 - 一個例子

class Widget
{
public:
   virtual void paint() = 0;
};

class Button : public Widget
{
public:
   void paint() // is virtual because it is an override
   {
       // do some stuff to draw a button
   }
};

因為paint()是一個純函式在 Widget類中,我們必須在所有具體子類中提供實現。如果我們沒有提供,編譯器會在構建時給出錯誤。

這有助於提供介面 - 我們期望基於特定層次結構的所有物件的行為,但當我們想忽略實現細節時。

那麼這為什麼有用呢?

讓我們以我們之前的例子為例,我們有一個純用於繪製。在許多情況下,我們希望能夠對小部件執行操作,而不必擔心它是哪種小部件。繪製是一個簡單的例子。

想象一下,我們的應用程式中有一些東西會在小部件變得活動時重新繪製小部件。它只適用於指向小部件的指標 - 例如Widget *activeWidget() const可能是一個可能的函式簽名。因此,我們可能執行以下操作

Widget *w = window->activeWidget();
w->paint();

我們希望實際呼叫適合“真實”小部件型別的 paint 成員函式 - 而不是Widget::paint()(這是一個“純”,如果使用虛擬排程呼叫,會導致程式崩潰)。透過使用一個函式,我們確保子類的成員函式實現 -Button::paint()在本例中 - 將被呼叫。


Clipboard

要做
提及介面類


協變返回型別

[編輯 | 編輯原始碼]

協變返回型別是派生類中的虛擬函式能夠返回指向自身例項的指標或引用,如果基類中的方法版本也這樣做。例如

class base
{
public:
  virtual base* create() const;
};

class derived : public base
{
public:
  virtual derived* create() const;
};

這允許避免強制轉換。

注意
一些較舊的編譯器不支援協變返回型別。對於此類編譯器,存在變通方法。

虛建構函式

[編輯 | 編輯原始碼]

存在一個類層次結構,基類為Foo。給定層次結構中的一個物件bar,希望能夠執行以下操作

  1. 建立一個物件bazbar(例如,類Bar)使用該類的預設建構函式初始化。通常使用的語法是
    Bar* baz = bar.create();
  2. 建立一個物件bazbar這是bar的副本。通常使用的語法是
    Bar* baz = bar.clone();

在類Foo中,方法Foo::create()Foo::clone()宣告如下

class Foo
{
    // ...

    public:
        // Virtual default constructor
        virtual Foo* create() const;

        // Virtual copy constructor
        virtual Foo* clone() const;
};

如果Foo要用作抽象類,則這些函式可以被設定為純虛擬函式

class Foo
{
   // ...

    public:
        virtual Foo* create() const = 0;
        virtual Foo* clone() const = 0;
};

為了支援建立預設初始化的物件,以及建立複製物件,每個類Bar在層次結構中必須具有公共預設建構函式和複製建構函式。Bar的虛建構函式定義如下

class Bar : ... // Bar is a descendant of Foo
{
    // ...

    public:
    // Non-virtual default constructor
    Bar ();
    // Non-virtual copy constructor
    Bar (const Bar&);

    // Virtual default constructor, inline implementation
    Bar* create() const { return new Foo (); }
    // Virtual copy constructor, inline implementation
    Bar* clone() const { return new Foo (*this); }
};

上面的程式碼使用了協變返回型別。如果你的編譯器不支援Bar* Bar::create(),請使用Foo* Bar::create()代替,同樣適用於clone().

在使用這些虛建構函式時,你必須手動透過呼叫delete baz;來釋放建立的物件。如果使用智慧指標(例如std::unique_ptr<Foo>) 用於返回型別而不是普通的Foo*.

請記住,無論是否Foo使用動態分配的記憶體,您必須定義解構函式virtual ~Foo ()並將其設為負責使用指向祖先型別的指標來釋放物件。

虛擬解構函式

[edit | edit source]

務必記住,即使在任何基類中定義空虛擬解構函式也很重要,因為如果不這樣做會導致編譯器生成的預設析構函數出現問題,它將不會是虛擬的。

在派生類中重新定義時,不會覆蓋虛擬解構函式,每個解構函式的定義是累積的,它們從最後一個派生類開始,一直到第一個基類。

純虛擬解構函式

[edit | edit source]

每個抽象類都應該包含純虛擬解構函式的宣告。

純虛擬解構函式是純虛擬函式的一種特殊情況(意在在派生類中被覆蓋)。它們必須始終被定義,並且該定義應該始終為空。

class Interface {
public:
  virtual ~Interface() = 0; //declaration of a pure virtual destructor 
};

Interface::~Interface(){} //pure virtual destructor definition (should always be empty)
華夏公益教科書