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. 接口设计原则
- 单一职责接口(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 { … };
- 接口最小化
抽象类中只声明行为,不包含任何状态或非虚接口;状态和共用实现可放在派生类或做为混入(mixin)基类提供。 - 虚析构
抽象类(接口类)必须声明虚析构,否则通过基类指针delete
时不会调用派生类析构,可能导致资源泄漏。 - 避免接口泄漏
不要在接口中暴露任何具体类型或实现细节,如指针类型、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)或混合策略,但需权衡灵活性与效率。