多型

在深入本章之前,您應該對指標和類繼承有正確的理解。如果您不太確定以下任何表示式的含義,您應該複習指定的章節。

語句解釋章節
int A::b(int c) { }
a->b資料結構
class A: public B {};友元和繼承

指向基類的指標

類繼承的一個關鍵特性是,指向派生類的指標與指向其基類的指標是型別相容的。多型就是利用這個簡單但強大且用途廣泛的特性的藝術。

關於矩形和三角形類的例子可以利用這一特性,使用指標重寫:

// pointers to base class
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
};

class Rectangle: public Polygon {
  public:
    int area()
      { return width*height; }
};

class Triangle: public Polygon {
  public:
    int area()
      { return width*height/2; }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << rect.area() << '\n';
  cout << trgl.area() << '\n';
  return 0;
}
20
10

函式 main 聲明瞭兩個指向 Polygon 的指標(名為 ppoly1ppoly2)。它們被分別賦予了 recttrgl 的地址,這兩個物件分別是 RectangleTriangle 型別的。這樣的賦值是有效的,因為 RectangleTriangle 都是從 Polygon 派生出來的類。

解引用 ppoly1ppoly2(使用 ppoly1->ppoly2->)是有效的,並允許我們訪問它們所指向物件的成員。例如,在前面的例子中,以下兩個語句是等價的:

1
2
ppoly1->set_values (4,5);
rect.set_values (4,5);

但是,因為 ppoly1ppoly2 的型別都是指向 Polygon 的指標(而不是指向 Rectangle 或指向 Triangle 的指標),所以只能訪問從 Polygon 繼承的成員,而不能訪問派生類 RectangleTriangle 的成員。這就是為什麼上面的程式直接使用 recttrgl 來訪問兩個物件的 area 成員,而不是透過指標;指向基類的指標無法訪問 area 成員。

如果 areaPolygon 的成員而不是其派生類的成員,那麼就可以透過指向 Polygon 的指標來訪問 area 成員。但問題是,RectangleTriangle 實現了不同版本的 area,因此沒有一個通用的版本可以在基類中實現。

虛成員

虛成員是一個可以在派生類中被重新定義的成員函式,同時透過引用保持其呼叫屬性。將函式變為虛擬函式的語法是在其宣告前加上 virtual 關鍵字。

// virtual members
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area ()
      { return 0; }
};

class Rectangle: public Polygon {
  public:
    int area ()
      { return width * height; }
};

class Triangle: public Polygon {
  public:
    int area ()
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon poly;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  Polygon * ppoly3 = &poly;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly3->set_values (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  cout << ppoly3->area() << '\n';
  return 0;
}
20
10
0

在這個例子中,所有三個類(PolygonRectangleTriangle)都有相同的成員:widthheight,以及函式 set_valuesarea

成員函式 area 在基類中被宣告為 virtual,因為它之後在每個派生類中被重新定義。非虛成員也可以在派生類中被重新定義,但派生類的非虛成員不能透過基類的引用來訪問。也就是說,如果在上面的例子中從 area 的宣告中移除 virtual,那麼所有三次對 area 的呼叫都將返回零,因為在所有情況下,被呼叫的都將是基類的版本。

因此,virtual 關鍵字本質上所做的,是允許派生類中與基類中同名的成員能夠透過指標被正確呼叫,更確切地說,是當指標型別為指向基類的指標,而它正指向派生類的物件時,就像上面的例子一樣。

一個宣告或繼承了虛擬函式的類被稱為多型類

請注意,儘管 Polygon 的一個成員是虛擬函式,但它仍然是一個常規類,甚至可以例項化一個物件(poly),它有自己對成員 area 的定義,該定義總是返回 0。

抽象基類

抽象基類與前面例子中的 Polygon 類非常相似。它們是隻能用作基類的類,因此允許擁有沒有定義的虛成員函式(稱為純虛擬函式)。其語法是用 =0(一個等號和一個零)來替換其定義。

一個抽象基類 Polygon 可能看起來像這樣:

1
2
3
4
5
6
7
8
9
// abstract class CPolygon
class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area () =0;
};

請注意,area 沒有定義;它被 =0 所取代,這使其成為一個純虛擬函式。包含至少一個純虛擬函式的類被稱為抽象基類

抽象基類不能用於例項化物件。因此,這個最後的抽象基類版本的 Polygon 不能用於宣告像下面這樣的物件:

1
Polygon mypolygon;   // not working if Polygon is abstract base class 

但是一個抽象基類並非完全無用。它可以用來建立指向它的指標,並利用其所有的多型能力。例如,下面的指標宣告是有效的:

1
2
Polygon * ppoly1;
Polygon * ppoly2;

並且當它們指向派生(非抽象)類的物件時,實際上可以被解引用。以下是完整的例子:

// abstract base class
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area (void) =0;
};

class Rectangle: public Polygon {
  public:
    int area (void)
      { return (width * height); }
};

class Triangle: public Polygon {
  public:
    int area (void)
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  return 0;
}
20
10

在這個例子中,不同但相關的型別的物件都透過一種唯一的指標型別(Polygon*)來引用,並且每次都會呼叫正確的成員函式,僅僅因為它們是虛擬函式。這在某些情況下非常有用。例如,抽象基類 Polygon 的一個成員甚至可以使用特殊指標 this 來訪問正確的虛成員,即使 Polygon 本身沒有實現這個函式。

// pure virtual members can be called
// from the abstract base class
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area() =0;
    void printarea()
      { cout << this->area() << '\n'; }
};

class Rectangle: public Polygon {
  public:
    int area (void)
      { return (width * height); }
};

class Triangle: public Polygon {
  public:
    int area (void)
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly1->printarea();
  ppoly2->printarea();
  return 0;
}
20
10

虛成員和抽象類賦予了 C++ 多型特性,這對於面向物件的專案最為有用。當然,上面的例子是非常簡單的用例,但這些特性可以應用於物件陣列或動態分配的物件。

下面是一個結合了最近幾章中一些特性的例子,如動態記憶體、建構函式初始化器和多型:

// dynamic allocation and polymorphism
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    Polygon (int a, int b) : width(a), height(b) {}
    virtual int area (void) =0;
    void printarea()
      { cout << this->area() << '\n'; }
};

class Rectangle: public Polygon {
  public:
    Rectangle(int a,int b) : Polygon(a,b) {}
    int area()
      { return width*height; }
};

class Triangle: public Polygon {
  public:
    Triangle(int a,int b) : Polygon(a,b) {}
    int area()
      { return width*height/2; }
};

int main () {
  Polygon * ppoly1 = new Rectangle (4,5);
  Polygon * ppoly2 = new Triangle (4,5);
  ppoly1->printarea();
  ppoly2->printarea();
  delete ppoly1;
  delete ppoly2;
  return 0;
}
20
10

請注意,ppoly 指標:

1
2
Polygon * ppoly1 = new Rectangle (4,5);
Polygon * ppoly2 = new Triangle (4,5);

被宣告為“指向 Polygon 的指標”型別,但分配的物件則直接宣告為派生類的型別(RectangleTriangle)。
Index
目錄