C++抽象类和纯虚函数

在继承层次结构中,有一项很方便的做法,可以在基类中定义一个成员函数,该函数只能在每个派生类中实现,而不能由基类本身来实现,因为合理实现所需的细节只能在派生类中找到。

如果是这种情况,那么 C++ 语言允许程序员将该函数声明为一个纯虚函数,也就是,一个在类中没有提供实现的成员函数。C++ 声明一个纯虚函数的方法是将表达式 =0 放在类声明中,而函数的主体则不存在。

例如,如果要将一个成员函数 void draw() 声明为纯虚函数,那么它的类中的声明语句示例如下:

void draw()= 0;

纯虚函数有时称为抽象函数,而如果某个类至少有一个纯虚函数,那么它将被称为抽象类C++ 编译器不允许实例化一个抽象类。抽象类只能被子类化,也就是说,只能使用它们作为派生其他类的基类。

派生自抽象类的类将继承基类中的所有函数,除非它覆盖继承的所有抽象函数,否则它本身也是抽象类。抽象类的用处在于它定义了一个接口,接口必须由从它派生的所有类的对象来支持。

可以把抽象类看作一个除子类外没有实例的类。现实生活中有许多抽象类的例子。例如,在动物王国中,"动物" 类就是所有动物的抽象类。所有动物的实例(例如狗、鸡、狐狸等)都属于 "动物" 抽象类中的某个子类,但 "动物" 本身不能实例化。

现在来看一看由一个形状的集合组成的图形系统,这些形状必须绘制在屏幕的某些位置上。每个形状对象都有一些成员变量来记录它的位置,并且还有一个成员函数,用于在正确位置绘制形状。该系统支持的不同形状可能包括矩形、六边形和其他形状。

因为矩形是一个形状,而六边形也是一个形状,所以单独创建一个 Shape 类是有意义的,然后就可以从 Shape 类中派生出 Rectangle 类和 Hexagon 类。Shape 类有一个成员函数 setPosition,将用于设置形状的位置;还有一个成员函数 Draw,用于绘制形状。但是,由于 Shape 是一个抽象类(没有任何形状可以是一个笼统概念上的“形状”,它必须是矩形、六边形、三角形或其他具体的形状),绘制一个特定形状的逻辑必须委托给一个合适的子类,因此,draw() 函数不能在 Shape 类中实现,必须将它声明为一个纯虚函数。

下面的程序演示了上述 Shape 类,它具有两个派生类:Rectangle 和 Hexagon。该类声明了一个纯虚函数 draw(),该函数将由其两个子类实现。main 函数使用了一个指针矢量来维护一个 Shape 对象的集合。
// This program demonstrates abstract base
// classes and pure virtual functions.
#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Shape
{
    protected:
        int posX, posY;
    public:
        virtual void draw() const =0;
        void setPosition(int pX, int pY)
        {
            posX = pX;
            posY = pY;
        }
};

class Rectangle : public Shape
{
    public:
        virtual void draw() const
        {
            cout << "Drawing rectangle at " << posX << " " << posY << endl;
        }
};

class Hexagon : public Shape
{
    public:
        virtual void draw() const
        {
            cout << "Drawing hexagon at " << posX << " " << posY << endl;
        }
};

int main()
{

    // Create vector of pointers to Shapes of various types
    vector<shared_ptr<Shape>> shapes
    {
        make_shared<Hexagon>(),
        make_shared<Rectangle>(),
        make_shared<Hexagon>()
    };
    // Set positions of all the shapes
    int posX = 5, posY = 15;
    for (int k = 0; k < shapes.size (); k++)
    {
        shapes[k]->setPosition(posX, posY);
        posX += 10;
        posY += 10;
    };
    // Draw all the shapes at their positions
    for (int j =0; j < shapes.size (); j++)
    {
        shapes[j]->draw();
    }
    return 0;
}
程序输出结果:

Drawing hexagon at 5 15
Drawing rectangle at 15 25
Drawing hexagon at 25 35

此程序提供了动态绑定和多态的另一个示例。来看以下声明:

shapeArray[j]->draw();

它将在循环中执行不同的次数。
for (int j = 0; j <shapes.size (); j ++)
{
    shapeArray[j]->draw();
}
第一次执行语句时,它将调用六边形对象的 draw 函数;而第二次执行时,它将调用矩形对象的 draw 函数。因为这两个 draw 函数是在不同的类中,所以它们会产生不同的行为。

关于抽象基类和纯虚函数,请记住以下知识要点:
  1. 当一个类包含一个纯虚函数时,它是一个抽象基类。
  2. 抽象基类不能被实例化。
  3. 纯虚函数用 =0 表达式声明,并且没有函数体或定义。
  4. 在需要实例化的派生类中必须覆盖纯虚函数。