1. 纯虚函数 (Pure Virtual Function)

定义与语法

纯虚函数是一个在基类中被声明,但没有定义(没有函数体)的虚函数。它告诉编译器:“这个函数必须存在,但具体怎么实现由我的派生类自己决定。”

它的语法非常独特,就是在虚函数声明的末尾加上 = 0

class Base {
public:
    // 这是一个纯虚函数
    virtual void show() = 0; 
};

语法要点:

  • 必须是类的成员函数。
  • 必须使用 virtual 关键字。
  • 在函数声明的末尾加上 = 0;

作用与目的

  1. 强制派生类实现: 纯虚函数的主要目的就是为派生类提供一个必须遵守的“协议”或“接口”。任何继承了含有纯虚函数的基类的派生类,如果想成为一个可以被实例化的具体类(Concrete Class),就必须重写(override)基类中所有的纯虚函数。
  2. 无需提供默认实现: 对于基类来说,某些操作可能没有一个有意义的默认实现。例如,一个抽象的 Shape(图形)类,如何实现一个 calculateArea()(计算面积)函数?“图形”本身没有面积,只有具体的“圆形”、“矩形”才有。因此,将其设为纯虚函数是最合适的。

2. 抽象类 (Abstract Class)

定义与特征

只要一个类包含至少一个纯虚函数,那么这个类就是抽象类。

// 因为 Shape 类包含了纯虚函数 area(),所以它是一个抽象类。
class Shape {
public:
    // 纯虚函数
    virtual double area() = 0; 
    
    // 抽象类也可以包含普通成员函数
    void setColor(const std::string& c) {
        color = c;
    }
 
    // 抽象类也可以包含普通成员变量
protected:
    std::string color;
};

核心特征:

  1. 不能被实例化: 你不能创建一个抽象类的对象。
Shape myShape; // 编译错误!因为 Shape 是抽象类。
  1. 主要用作基类: 抽象类的存在就是为了被其他类继承,从而定义一个通用的接口。
  2. 可以包含非纯虚函数和成员变量: 抽象类不仅仅是接口的集合,它还可以提供所有派生类共享的通用功能和数据(如上面例子中的 setColorcolor)。
  3. 可以定义指针和引用: 虽然不能创建抽象类的对象,但你可以创建指向抽象类的指针或引用。这是实现多态的关键。
Shape* shapePtr; // 合法
Shape& shapeRef = some_concrete_shape_object; // 合法

为什么抽象类不能被实例化?

很简单,因为它是“不完整”的。一个抽象类包含了至少一个没有实现的函数(纯虚函数)。如果你能创建它的对象,然后调用那个没有实现的函数,程序会做什么?这是未定义的行为,逻辑上也不通。因此,C++ 编译器直接禁止了这一行为。


3. 为什么需要抽象类和纯虚函数?

它们是面向对象设计中非常强大的工具,主要用于:

1. 强制实现接口(制定规范)

想象一下你在设计一个插件系统。你定义一个 Plugin 抽象类,其中包含 start()shutdown() 两个纯虚函数。任何想要开发插件的人都必须继承 Plugin 类,并实现 start()shutdown()。这样,你就能保证所有插件都有统一的、可预测的行为接口,你的主程序就可以通过这个接口安全地加载和卸载任何插件。

2. 实现多态

这是最常见的用途。通过基类指针或引用,你可以调用派生类中具体实现的方法,而无需在编译时知道对象的具体类型。这让代码更加灵活和可扩展。

例如,你可以有一个 Shape* 类型的数组,里面可以存放 Circle 对象、Rectangle 对象等。当你遍历这个数组并调用 shape->area() 时,程序会自动根据指针指向的实际对象类型,调用对应的 area() 函数。

3. 对现实世界进行抽象建模

软件设计常常是对现实世界的模拟。很多现实世界的概念是抽象的。比如“动物”是一个抽象概念,你看到的是具体的“猫”、“狗”。“交通工具”也是抽象的,你使用的是具体的“汽车”、“自行车”。

抽象类和纯虚函数让我们能够直接在代码中表达这种“抽象-具体”的关系。


4. 综合示例:图形绘制

这个例子完美地展示了以上所有概念。

#include <iostream>
#include <vector>
#include <string>
 
// 1. 定义抽象基类 Shape
// 它包含一个纯虚函数 area() 和一个普通的虚函数 draw()
// 这使得 Shape 成为一个抽象类
class Shape {
public:
    // 纯虚函数:任何派生类都必须实现它
    virtual double area() const = 0; 
    
    // 虚函数:提供一个默认实现,但派生类可以重写它
    virtual void draw() const {
        std::cout << "Drawing a generic shape." << std::endl;
    }
    
    // 虚析构函数:非常重要!确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数
    virtual ~Shape() {
        std::cout << "Shape destructor called." << std::endl;
    }
};
 
// 2. 创建具体派生类 Circle
class Circle : public Shape {
private:
    double radius;
 
public:
    Circle(double r) : radius(r) {}
 
    // 实现基类的纯虚函数 area()
    double area() const override {
        return 3.14159 * radius * radius;
    }
    
    // 重写基类的虚函数 draw()
    void draw() const override {
        std::cout << "Drawing a circle with radius " << radius << "." << std::endl;
    }
    
    ~Circle() {
        std::cout << "Circle destructor called." << std::endl;
    }
};
 
// 3. 创建具体派生类 Rectangle
class Rectangle : public Shape {
private:
    double width;
    double height;
 
public:
    Rectangle(double w, double h) : width(w), height(h) {}
 
    // 实现基类的纯虚函数 area()
    double area() const override {
        return width * height;
    }
    
    // 重写基类的虚函数 draw()
    void draw() const override {
        std::cout << "Drawing a rectangle with width " << width << " and height " << height << "." << std::endl;
    }
    
    ~Rectangle() {
        std::cout << "Rectangle destructor called." << std::endl;
    }
};
 
 
// 4. 在 main 函数中展示多态性
int main() {
    // Shape s; // 编译错误:不能实例化抽象类 Shape
 
    // 使用基类指针的容器来存储不同的派生类对象
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(5.0));
    shapes.push_back(new Rectangle(4.0, 6.0));
    shapes.push_back(new Circle(10.0));
 
    // 遍历容器,多态地调用 draw() 和 area()
    for (const auto& shape : shapes) {
        shape->draw(); // 调用的是 Circle::draw() 或 Rectangle::draw()
        std::cout << "Area: " << shape->area() << std::endl << std::endl; // 调用的是 Circle::area() 或 Rectangle::area()
    }
    
    // 释放内存
    for (auto& shape : shapes) {
        delete shape; // 因为析构函数是虚函数,所以会正确调用派生类的析构函数
    }
    
    return 0;
}

输出:

Drawing a circle with radius 5.
Area: 78.5397
 
Drawing a rectangle with width 4 and height 6.
Area: 24
 
Drawing a circle with radius 10.
Area: 314.159
 
Circle destructor called.
Shape destructor called.
Rectangle destructor called.
Shape destructor called.
Circle destructor called.
Shape destructor called.

5. 关键点与注意事项

派生类的责任

如果一个派生类继承了抽象类,但没有实现所有的纯虚函数,那么这个派生类自己也变成了抽象类,同样不能被实例化。

class SemiCircle : public Shape {
    // 注意:这里没有实现 area() 函数
public:
    void draw() const override { /* ... */ }
};
// SemiCircle mySemiCircle; // 编译错误!SemiCircle 也是抽象类。

抽象类的构造函数与析构函数

  • 构造函数: 抽象类可以有构造函数。虽然不能直接创建抽象类的对象,但派生类在构造时会调用基类的构造函数,用于初始化从基类继承来的成员。
  • 析构函数: 强烈建议将基类的析构函数声明为虚函数 (virtual)。如示例所示,当你通过基类指针 Shape* 删除一个派生类对象时,如果析构函数不是虚的,只会调用基类的析构函数,导致派生类的资源(如内存)泄露。将析构函数设为虚函数可以确保调用链是正确的:先调用派生类的析构函数,再调用基类的析构函数。

6. 总结

特性纯虚函数 (virtual ... = 0;)抽象类
是什么一个没有实现的虚函数,用于定义接口。包含至少一个纯虚函数的类。
目的强制派生类提供具体实现。作为通用接口的基类,定义一组规范。
实例化N/A (是函数)不能被实例化。
如何使用声明在基类中,由派生类实现。作为基类被继承,可以通过指针和引用操作派生类对象。
核心关系纯虚函数是创建抽象类的工具。抽象类是纯虚函数存在的载体和应用场景。

抽象类和纯虚函数是C++实现“面向接口编程”的基石,它强制了类的设计规范,并利用多态性极大地提高了代码的灵活性和可维护性,是每一个C++开发者都必须掌握的核心概念。