在C++里并没有像Java或C#那样原生的interface关键字,但语言通过纯虚函数和抽象类提供了完全等价的接口抽象能力。纯虚函数是一种在基类中只声明不定义的虚函数,它的存在强制派生类给出具体实现;而包含至少一个纯虚函数的类就被称为抽象类,编译器禁止我们直接创建抽象类的实例。借助这套机制,可以把“能做什么”和“怎么做”分离开,让上层代码面向抽象编程。

一、纯虚函数与抽象类的基本语法
纯虚函数的写法是在虚函数声明后加上 = 0,这表示该函数没有函数体,留给子类去覆盖。一旦类中出现了这样的函数,这个类就自动成为抽象类。抽象类本身不能实例化,只能作为指针或引用的静态类型,指向真正的派生类对象。
下面是一段最基础的示例,展示如何定义抽象类和纯虚函数:
#include <iostream>
// 抽象类,充当接口
class Shape {
public:
// 纯虚函数
virtual double area() const = 0;
virtual void draw() const = 0;
// 抽象类的虚析构函数,保证派生类析构被正确调用
virtual ~Shape() {}
};
// 具体类,必须实现所有纯虚函数
class Circle : public Shape {
private:
double r;
public:
Circle(double radius) : r(radius) {}
double area() const override {
return 3.14159 * r * r;
}
void draw() const override {
std::cout << "Draw a circle" << std::endl;
}
};
int main() {
// Shape s; // 编译错误:不能实例化抽象类
Shape* p = new Circle(2.0);
std::cout << p->area() << std::endl;
p->draw();
delete p;
return 0;
}
在上面的代码中,Shape 就是抽象类,它规定了任何图形都必须提供 area 和 draw 两个行为。如果 Circle 漏掉了其中任意一个函数的 override,编译器会判定 Circle 仍然是抽象类,同样无法实例化。
需要注意的是,抽象类的析构函数通常应该声明为虚函数。因为当通过基类指针 delete 派生类对象时,只有虚析构才能保证派生类的析构函数被链式调用,避免资源泄漏。即便抽象类自身没有需要释放的资源,这个设计习惯也不可或缺。
二、接口实现的多态本质
所谓“接口实现”,在C++中就是让不同的类继承自同一个抽象类,并各自重写纯虚函数。由于函数调用在运行时根据对象真实类型绑定,我们可以用统一的基类指针操作完全不同的对象,这就是多态。它让模块之间的依赖停留在抽象层面,而不是具体类型。
以一个渲染系统为例,不论是圆形、矩形还是外部插件提供的特殊图形,只要它们都继承自 Shape,渲染器就不需要关心其内部结构:
#include <vector>
#include <iostream>
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() {}
};
class Rect : public Shape {
public:
void draw() const override {
std::cout << "Draw rectangle" << std::endl;
}
};
class Triangle : public Shape {
public:
void draw() const override {
std::cout << "Draw triangle" << std::endl;
}
};
void renderAll(const std::vector<Shape*>& list) {
for (auto s : list) {
s->draw();
}
}
这段代码的 renderAll 函数只认 Shape* 接口,却可以驱动 Rect、Triangle 等多种实现。新增图形类型时,原有渲染逻辑一行都不用改,符合开闭原则。
从底层看,每个含虚函数的类对象都带有一个隐藏的虚函数表指针,指向属于自己类型的虚函数表。调用 s->draw() 时,程序通过这张表找到实际的函数地址。纯虚函数在表中对应项通常指向一个报错桩或留空,从而从机制上阻止抽象类被直接调用。
三、常见误区与注意事项
初学者常误以为抽象类不能有任何普通成员函数或成员变量,其实恰恰相反。抽象类完全可以拥有数据成员、普通方法,甚至带实现的虚函数,只是它至少包含一个纯虚函数而已。把共用逻辑写在抽象类里,能减少子类重复代码。
另一个易错点是在派生类中忘记写 override 关键字,导致函数签名稍有偏差时编译器不报错,却悄悄新建了一个虚函数而非重写。开启 override 能让编译器帮你校验,强烈建议始终加上。
class Base {
public:
virtual void foo(int x) = 0;
};
class Wrong : public Base {
public:
// 参数类型写成long,未覆盖纯虚函数,Wrong仍是抽象类
virtual void foo(long x) override {} // 编译报错:没有可覆盖的函数
};
class Right : public Base {
public:
void foo(int x) override {} // 正确重写
};
此外,如果抽象类的纯虚函数需要在派生类中选择性复用,也可以给出默认实现,但语法上仍要保留 = 0,实现写在类外。这种设计在框架开发中较常见,既强制子类显式声明意图,又提供可调用基础逻辑。
最后,当抽象类被用作跨库接口时,要确保双方使用兼容的编译器和运行时,因为虚函数表布局属于实现细节。保持接口类简洁、只放纯虚函数和虚析构,是提升二进制稳定性的有效做法。
四、与多重继承结合模拟多接口
C++允许一个类同时继承多个抽象类,从而模拟一个类实现多个接口的效果。例如既能被序列化又能被绘制,只需分别继承 Serializable 和 Shape 两个抽象类,并全部实现其纯虚函数。
class Serializable {
public:
virtual std::string serialize() const = 0;
virtual ~Serializable() {}
};
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape, public Serializable {
public:
void draw() const override {}
std::string serialize() const override {
return "circle";
}
};
多重继承会带来菱形继承等复杂性,但当抽象类均无数据成员、仅作接口使用时,风险很低,是C++里实现“多接口”的标准手段。只要控制好继承层级,就能在保持类型安全的同时获得灵活的结构。
总结来看,纯虚函数和抽象类构成了C++的接口基石。理解它们的语法约束、多态原理以及工程中的避坑点,才能写出低耦合、易扩展的系统代码。
pure_virtual_functionabstract_classinterface_implementation修改时间:2026-07-31 23:54:34