C++ 纯虚函数与抽象类

在设计基类时,有时候基类只代表一个笼统的分类概念,自身没法写出有意义的默认代码。比如圆形和矩形都有明确的面积公式,可一个不知道具体形状的 “图形(Shape)” 基类,到底应该怎么计算面积呢?

与其在基类的虚函数里硬写一段占位代码,不如让基类只规定函数格式。在 C++ 中,我们可以把这类函数声明为 “纯虚函数(Pure Virtual Function)”,并把包含它的类称为 “抽象类(Abstract Class)”。

纯虚函数与抽象类的定义

在 C++ 中,如果我们想把基类中的虚函数声明为纯虚函数,只需要去掉它的函数体花括号 {},并在声明末尾加上 = 0 就可以了。

语法:

class 基类名 {
public:
    virtual 返回值类型 函数名(参数列表) = 0;
};

说明:

这里的 = 0 是声明纯虚函数的固定语法,小伙伴们不要把它误解成 “把函数赋值为 0”。它只是一个特殊标记,表示该函数在基类中只规定接口格式,真正的函数体必须留给派生类去编写。如果成员函数本身带有 const 修饰,那么 = 0 要固定写在 const 的后面,比如 virtual double getArea() const = 0;。

只要一个类中包含纯虚函数,这个类就称为 “抽象类”。对于抽象类来说,它具有以下 2 个重要特点:

  • 不能直接创建对象:因为抽象类里的纯虚函数还没有具体实现代码,如果直接创建对象(如 Shape s;),调用该函数时程序根本不知道应该执行什么,所以编译器禁止直接创建抽象类对象。
  • 可以定义引用或指针:虽然不能创建抽象类自身的对象,但我们依然可以定义抽象类的引用(如 Shape&)或指针(如 Shape*)去指向派生类对象,这是实现多态最常用的做法。

派生类实现纯虚函数

抽象基类把纯虚函数的接口规定好之后,继承它的派生类就可以根据自身特点,通过函数重写把纯虚函数的具体代码补齐。只有当派生类实现了基类中所有的纯虚函数后,该派生类才能正常创建对象。下面我们来看一个例子。

示例 1:定义并实现纯虚函数

#include <iostream>

// 抽象图形基类
class Shape {
public:
    virtual double getArea() = 0;
};

// 圆形类:实现纯虚函数
class Circle : public Shape {
public:
    double radius;

    Circle(double r) : radius(r) {}

    double getArea() {
        return 3.14 * radius * radius;
    }
};

// 矩形类:实现纯虚函数
class Rectangle : public Shape {
public:
    double width;
    double height;

    Rectangle(double w, double h) : width(w), height(h) {}

    double getArea() {
        return width * height;
    }
};

void printArea(Shape& shape) {
    std::cout << "图形面积:" << shape.getArea() << '\n';
}

int main() {
    Circle circle(5.0);
    Rectangle rect(4.0, 6.0);

    printArea(circle);
    printArea(rect);
    return 0;
}

运行结果如下。

图形面积:78.5
图形面积:24

分析:

在这个例子中,我们将 Shape 基类中的 getArea() 声明成了纯虚函数,不需要在基类里硬写一句无意义的 return 0.0;。此时 Shape 是一个抽象类,专门用来规定所有具体图形都必须提供 getArea() 这项功能。

派生类 Circle 和 Rectangle 继承 Shape 后,各自补齐了 getArea() 的面积计算公式,因此它们不再是抽象类,可以正常创建出 circle 和 rect 对象。

而对于抽象类 Shape 本身,我们只能像 printArea(Shape& shape) 那样使用它的引用或指针。如果尝试直接创建 Shape 对象,编译器就会报错:

Shape s;    // 编译报错:抽象类不能直接创建对象

那如果某个派生类继承了抽象类,却漏掉了里面的纯虚函数没有实现,会发生什么呢?我们可以看一下下面这个例子。

示例 2:未实现纯虚函数报错

#include <iostream>

class Shape {
public:
    virtual double getArea() = 0;
};

class Triangle : public Shape {
public:
    // 未实现基类的纯虚函数 getArea()
};

int main() {
    Triangle tri;    // 编译报错
    return 0;
}

运行结果如下。

(报错)error: cannot declare variable 'tri' to be of abstract type 'Triangle'

分析:

从报错信息可以看到,编译器提示不能将变量 tri 声明为抽象类型 Triangle。这是因为 Triangle 继承了抽象类 Shape,却没有实现基类的纯虚函数 getArea()。

此时相当于 Triangle 内部依然保留着未完成的纯虚函数,所以 Triangle 自己也会变成抽象类,执行 Triangle tri; 创建对象时自然就会编译报错。

注意 如果一个抽象基类中包含多个纯虚函数,派生类必须把这些纯虚函数全部实现出来,才能成为可正常创建对象的具体类。只要漏掉其中任何一个,该派生类本身就依然是抽象类。

抽象类中可以定义普通成员吗?

初学抽象类时,有些小伙伴可能会误以为 “抽象类里面只能写 = 0 的纯虚函数”。实际上并非如此,抽象类内部同样可以定义普通的成员变量、普通成员函数,甚至还可以定义构造函数。

也就是说,如果某项属性或操作对所有派生类来说都是一样的,我们可以直接把它写在抽象基类中供子类复用。只有那些各个派生类做法不同的功能,才需要声明为纯虚函数。请看下面例子。

示例 3:抽象类包含普通成员

#include <iostream>
#include <string>

class Appliance {
public:
    std::string brand;

    Appliance(std::string b) : brand(b) {}

    // 普通成员函数:所有家电共用的操作
    void powerOn() {
        std::cout << brand << "已接通电源\n";
    }

    // 纯虚函数:交由派生类各自实现
    virtual void work() = 0;
};

class Washer : public Appliance {
public:
    Washer(std::string b) : Appliance(b) {}

    void work() {
        std::cout << brand << "洗衣机开始洗涤衣物\n";
    }
};

int main() {
    Washer washer("海尔");
    Appliance& app = washer;

    app.powerOn();
    app.work();
    return 0;
}

运行结果如下。

海尔已接通电源
海尔洗衣机开始洗涤衣物

分析:

在上面代码中,所有家电共有的品牌属性 brand 和通电操作 powerOn() 都直接写在了抽象基类 Appliance 中,只有具体工作方式不同的 work() 声明为了纯虚函数。派生类 Washer 只需要把 work() 实现出来,就能直接复用基类的公共成员。

可能有些小伙伴会问:前面不是刚说过抽象类不能创建对象吗,为什么 Appliance 里还能定义构造函数呢?

其实,这个构造函数并不是用来单独创建 Appliance 对象的,而是专门留给子类 Washer 在初始化列表中调用的(即 Appliance(b)),用来完成基类成员变量 brand 的初始化。

常见问题

1. 在实际开发中,什么时候使用普通基类,什么时候使用抽象类?

普通基类和抽象类都可以作为多态的基类,我们可以根据下面两点来判断:

普通基类:当基类本身代表一个可以独立存在的具体事物(如普通订单 Order),并且它的虚函数也有合理的默认做法时,写成普通虚函数即可。

抽象类:当基类只是一个笼统的分类概念(如图形 Shape、家电 Appliance),没法写出合理的默认实现,并且希望强制所有子类都按统一格式实现某个函数时,就将该函数声明为纯虚函数,把基类设计成抽象类。

2. C++ 中没有 interface 关键字,应该怎么定义 “接口”?

Java、C# 等语言专门提供了 interface 关键字来定义接口,而 C++ 中没有这个关键字。在实际开发中,如果一个类不定义普通成员变量,而是把业务函数全部声明为 = 0 的纯虚函数,这样的抽象类在 C++ 里就充当了纯粹的 “接口类”。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号