多态

在深入本章之前,您应该对指针和类继承有正确的理解。如果您不太确定以下任何表达式的含义,您应该复习指定的章节。

语句解释章节
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
目录