Skip to content

C++ 接口

在面向对象编程中,接口为类定义了契约。它规定了一个类 能做什么(它的能力或方法),而不强制规定它 如何做。这促进了松耦合和多态性。

C++ 使用 抽象类 来实现接口。抽象类是不能直接实例化的类,它旨在用作派生类的基类。

如果一个类至少声明了一个 纯虚函数,它就成为抽象类。纯虚函数是一个虚函数,它在基类中声明,但不一定定义。通过在其签名末尾附加 = 0 来声明:

#include <string>
// 定义 'Drawable' 接口的抽象基类
class Drawable {
public:
// 纯虚函数 - 派生类必须实现此函数
virtual void draw() const = 0;
// 抽象类仍然可以有常规成员和方法
virtual std::string getName() const { return "Generic Drawable"; }
// 重要提示:多态使用的抽象基类需要一个虚析构函数
virtual ~Drawable() = default; // 使用默认实现
};

关于抽象类的要点:

  • 不能实例化: 你不能直接创建抽象类类型的对象(Drawable d; 会导致编译错误)。
  • 接口作用: 它们主要用作接口,定义了一组派生类必须实现的通用函数。
  • 多态性: 它们通常与指针或引用一起使用,以便通过通用的基类接口处理不同派生类的对象。
  • 强制实现: 任何继承自抽象类的具体(非抽象)类 必须 为 所有 继承的纯虚函数提供实现。
  • 虚析构函数: 如果你打算通过基类指针删除派生对象(Drawable* ptr = new Circle(); delete ptr;),基类 必须 有一个虚析构函数。否则,只会运行基类析构函数,导致资源泄漏(未定义行为)。添加 virtual ~ClassName() = default; 通常就足够了。

可以实例化(即非抽象)的类称为 具体类。从抽象类派生的具体类会为所有继承的纯虚函数提供实现。

让我们用具体的 Circle 和 Square 类来实现 Drawable 接口:

#include <iostream>
#include <string>
#include <vector>
#include <memory> // 用于 std::unique_ptr
// 抽象基类(接口)
class Shape {
public:
// 纯虚函数 - 定义接口契约
virtual double getArea() const = 0;
virtual std::string getName() const = 0;
// 虚析构函数对于带有虚函数的基类至关重要
virtual ~Shape() { std::cout << "Destroying Shape\n"; }; // 非默认析构函数示例
// 通用工具函数(可选)
void displayInfo() const {
std::cout << "Shape Type: " << getName() << ", Area: " << getArea() << std::endl;
}
protected:
// 如果需要,可以是保护构造函数,但 Shape 不能直接实例化
Shape() = default;
};
// 具体派生类:Rectangle
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 重写纯虚函数
double getArea() const override {
return width * height;
}
std::string getName() const override {
return "Rectangle";
}
~Rectangle() override { std::cout << "Destroying Rectangle\n"; }
private:
double width;
double height;
};
// 具体派生类:Triangle
class Triangle : public Shape {
public:
Triangle(double b, double h) : base(b), height(h) {}
// 重写纯虚函数
double getArea() const override {
return 0.5 * base * height;
}
std::string getName() const override {
return "Triangle";
}
~Triangle() override { std::cout << "Destroying Triangle\n"; }
private:
double base;
double height;
};
int main() {
// 使用 unique_ptr 进行自动内存管理 (RAII)
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Rectangle>(5.0, 7.0));
shapes.push_back(std::make_unique<Triangle>(5.0, 7.0));
std::cout << "Processing shapes polymorphically:\n";
for (const auto& shapePtr : shapes) {
// 根据实际对象类型调用正确的 getArea/getName
shapePtr->displayInfo();
}
std::cout << "\nShapes vector going out of scope...\n";
// unique_ptr 会自动在其包含的指针上调用 delete,
// 借助虚基类析构函数调用正确的派生类析构函数。
return 0;
}

编译并执行上述代码时,会产生类似于以下的输出:

Processing shapes polymorphically:
Shape Type: Rectangle, Area: 35
Shape Type: Triangle, Area: 17.5
Shapes vector going out of scope...
Destroying Rectangle
Destroying Shape
Destroying Triangle
Destroying Shape

注意我们如何通过 Shape 指针统一处理 Rectangle 和 Triangle 对象。override 关键字有助于确保我们正确实现了基类中的虚函数。虚析构函数确保了正确的清理。

使用抽象类(接口)是一种强大的设计策略:

  • 解耦: 使用接口的代码(例如使用 Shape* 的 main 函数)无需了解具体的具体类型(Rectangle、Triangle)。它只依赖于 Shape 接口。
  • 可扩展性: 通过从 Shape 派生并实现所需函数,你可以轻松添加新的形状(例如 Circle),而无需修改使用 Shape 接口的代码。
  • 标准化: 确保所有派生类都提供基类中纯虚函数定义的一组最小功能。

现代 C++ (C++20) 还引入了 Concepts,它提供了一种在编译时指定模板参数要求的方法,有时可作为泛型编程中传统接口的替代或补充。