C++ 接口(抽象类)

在 C++ 中,“接口”通常通过抽象类(Abstract Class)来实现——即只包含纯虚函数(pure virtual functions),不含数据成员或非虚实现。抽象类既定义了子类必须实现的行为契约,又能隐藏具体实现细节,实现面向接口编程(Programming to Interfaces)的思想。下面分几个方面详细介绍。


1. 抽象类的定义

// IShape 即为一个抽象接口类
class IShape {
public:
    // 纯虚函数:没有实现,必须由派生类重写
    virtual double area() const = 0;
    virtual double perimeter() const = 0;

    // 虚析构:确保通过基类指针删除时能正确调用派生析构
    virtual ~IShape() = default;
};
  • 纯虚函数:在声明处以 = 0 结尾,表示没有函数体,派生类 必须 提供重写(override)。
  • 抽象类:凡含有至少一个纯虚函数的类,即为抽象类,不能直接实例化

2. 实现接口:派生类

派生类通过公有继承并实现所有纯虚函数来“实现”接口:

class Circle : public IShape {
    double r_;
public:
    Circle(double r) : r_(r) {}

    double area() const override {
        return 3.1415926535 * r_ * r_;
    }
    double perimeter() const override {
        return 2 * 3.1415926535 * r_;
    }
};

class Rectangle : public IShape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}

    double area() const override {
        return w_ * h_;
    }
    double perimeter() const override {
        return 2 * (w_ + h_);
    }
};
  • override 关键字:显式声明“这是对基类虚函数的覆盖”,若签名不匹配,编译器会报错。
  • 如果派生类未实现所有纯虚函数,仍然是一个抽象类,亦不可实例化。

3. 使用接口:多态调用

通过接口指针或引用,可在运行时动态绑定调用不同实现,实现灵活拓展和解耦:

#include <iostream>
#include <memory>
#include <vector>

void printShapeInfo(const IShape& s) {
    std::cout << "Area: " << s.area()
              << ", Perimeter: " << s.perimeter() << "\n";
}

int main() {
    std::vector<std::unique_ptr<IShape>> shapes;
    shapes.emplace_back(std::make_unique<Circle>(2.0));
    shapes.emplace_back(std::make_unique<Rectangle>(3.0, 4.0));

    for (auto& shp : shapes) {
        printShapeInfo(*shp);
    }
}
  • 基类指针/引用:持有 IShape* 或 IShape&,实际指向派生对象。
  • 虚函数表(vtable):运行时根据对象的真实类型,动态调用对应重写函数。

4. 接口设计原则

  1. 单一职责接口(Interface Segregation Principle)
    将不同职责拆分成多个小接口,让使用者只依赖真正需要的行为:struct IRenderable { virtual void render() const = 0; virtual ~IRenderable() = default; }; struct IUpdatable { virtual void update(double dt) = 0; virtual ~IUpdatable() = default; }; // 类可以多重继承多个接口 class Sprite : public IRenderable, public IUpdatable { … };
  2. 接口最小化
    抽象类中只声明行为,不包含任何状态或非虚接口;状态和共用实现可放在派生类或做为混入(mixin)基类提供。
  3. 虚析构
    抽象类(接口类)必须声明虚析构,否则通过基类指针 delete 时不会调用派生类析构,可能导致资源泄漏。
  4. 避免接口泄漏
    不要在接口中暴露任何具体类型或实现细节,如指针类型、STL 容器等;如果必要,可返回抽象类型或使用 std::span、迭代器等更通用的方式。

5. 接口与纯静态接口区别

有时需要定义只含静态成员的“接口”,例如工具集(Utility)或策略(Policy),这不依赖对象实例,可使用命名空间或结构体静态方法:

struct MathUtil {
    static double clamp(double x, double lo, double hi) {
        return (x < lo ? lo : (x > hi ? hi : x));
    }
    // …更多静态工具函数
};

此类“接口”并非抽象类,不支持多态,只为逻辑分组和命名。


6. 接口与模板结合:静态多态

如需在编译期实现多态、消除虚调用开销,可结合模板与 CRTP(Curiously Recurring Template Pattern)实现静态“接口”:

template<typename Derived>
struct Drawable {
    void draw() const {
        // 委托给派生类静态实现
        static_cast<const Derived*>(this)->doDraw();
    }
};

class MyShape : public Drawable<MyShape> {
public:
    void doDraw() const {
        // 具体绘制实现
    }
};

int main() {
    MyShape s;
    s.draw();  // 编译期绑定,不需 vtable
}
  • 优点:零运行时开销;
  • 缺点:无法通过统一基类指针处理异构对象。

小结

  • 抽象类(含纯虚函数)是 C++ 实现接口的标准方式,支持运行时多态。
  • 接口类只声明行为,不含状态,实现细节全部留给派生类。
  • 设计接口时应遵循单一职责、接口最小化和虚析构等原则,保持可扩展、可维护。
  • 若对性能有极致需求,可考虑静态接口(模板/CRTP)或混合策略,但需权衡灵活性与效率。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注