有时候父类只想定义一份"规范",强制所有子类必须实现某个功能,自己却不提供具体实现——这就是抽象类的用途。
想象国家电气标准委员会制定了一份"插座规范":所有插座必须能供电,必须有接地保护,必须能承受额定电压……但这份规范本身不是一个具体的插座,你不能直接把它买回家用。
各大厂商根据这份规范,各自制造出五孔插座、USB 插座、防水插座……这些才是真正可以用的具体产品。
C++ 的抽象类正是这个"规范"的角色:它定义了子类必须实现哪些功能(接口),但自己不提供实现,也不能被直接实例化。
Shape 是抽象类(虚线边框),声明了"所有图形都要能算面积",但它自己不知道面积怎么算。Circle、Rectangle、Triangle 各自继承并实现 Area(),才是可以使用的具体类。
在虚函数的末尾加 = 0,它就变成了纯虚函数。含有至少一个纯虚函数的类,自动成为抽象类。
| 1 | class Shape { |
| 2 | public: |
| 3 | virtual double Area() const = 0; // 纯虚函数:= 0 表示"只声明,不实现" |
| 4 | virtual void Draw() = 0; // 可以有多个纯虚函数 |
| 5 | virtual ~Shape() {} // 析构函数仍然要实现(可以为空) |
| 6 | }; |
| 7 | |
| 8 | // 子类必须实现所有纯虚函数,否则子类也变成抽象类 |
| 9 | class Circle : public Shape { |
| 10 | public: |
| 11 | double r; |
| 12 | Circle(double r) : r(r) {} |
| 13 | double Area() const override { return 3.14159 * r * r; } |
| 14 | void Draw() override { cout << "画一个圆" << endl; } |
| 15 | }; |
含有纯虚函数的类不能直接创建对象——这是编译器强制的。但可以用抽象类的指针或引用来统一管理子类对象,这正是它最大的用途。
| 1 | // Shape s; ❌ 编译错误!抽象类不能实例化 |
| 2 | // Shape s = Circle; ❌ 同样不行 |
| 3 | |
| 4 | Shape* p = new Circle(5.0); // ✅ 指针可以,指向子类对象 |
| 5 | cout << p->Area(); // 多态调用,输出圆的面积 |
| 6 | delete p; |
| 7 | |
| 8 | // 用 vector 统一管理多种图形 |
| 9 | vector<Shape*> shapes; |
| 10 | shapes.push_back(new Circle(3.0)); |
| 11 | shapes.push_back(new Rectangle(4.0, 5.0)); |
| 12 | shapes.push_back(new Triangle(6.0, 3.0)); |
| 13 | |
| 14 | for (Shape* s : shapes) |
| 15 | cout << s->Area() << endl; // 多态:各自调用自己的实现 |
| 16 | // 输出:28.27... 20.0 9.0 |
vector<Shape*> 就能装下所有种类的图形,循环里一行代码对所有图形调用 Area()——每种图形各自执行自己的计算逻辑,完全不需要写 if-else 判断类型。这种设计让添加新图形类型变得极其容易,不需要修改任何已有代码。
| 1 | #include <iostream> |
| 2 | #include <vector> |
| 3 | using namespace std; |
| 4 | |
| 5 | // ── 抽象基类 ── |
| 6 | class Shape { |
| 7 | public: |
| 8 | string name; |
| 9 | Shape(string n) : name(n) {} |
| 10 | virtual double Area() const = 0; |
| 11 | virtual double Perimeter() const = 0; |
| 12 | void Print() const { // 普通函数,可以有实现 |
| 13 | cout << name << ": 面积=" << Area() |
| 14 | << " 周长=" << Perimeter() << endl; |
| 15 | } |
| 16 | virtual ~Shape() {} |
| 17 | }; |
| 18 | |
| 19 | // ── 具体子类 ── |
| 20 | class Circle : public Shape { |
| 21 | double r; |
| 22 | public: |
| 23 | Circle(double r) : Shape("圆形"), r(r) {} |
| 24 | double Area() const override { return 3.14159 * r * r; } |
| 25 | double Perimeter() const override { return 2 * 3.14159 * r; } |
| 26 | }; |
| 27 | |
| 28 | class Rectangle : public Shape { |
| 29 | double w, h; |
| 30 | public: |
| 31 | Rectangle(double w, double h) : Shape("矩形"), w(w), h(h) {} |
| 32 | double Area() const override { return w * h; } |
| 33 | double Perimeter() const override { return 2 * (w + h); } |
| 34 | }; |
| 35 | |
| 36 | int main() { |
| 37 | vector<Shape*> shapes = { |
| 38 | new Circle(5), |
| 39 | new Rectangle(4, 6), |
| 40 | }; |
| 41 | for (Shape* s : shapes) { |
| 42 | s->Print(); // 多态:各自调用正确的 Area 和 Perimeter |
| 43 | delete s; |
| 44 | } |
| 45 | // 输出: |
| 46 | // 圆形: 面积=78.5397 周长=31.4159 |
| 47 | // 矩形: 面积=24 周长=20 |
| 48 | } |
Print() 是抽象类里的普通函数(不是纯虚函数),它可以有实现,并且在实现里调用了纯虚函数 Area() 和 Perimeter()。这是一个经典的模板方法模式——父类定义流程,子类填充细节。
恭喜你学完了第十二章的全部内容!我们从最基础的 class 定义出发,一步步走完了面向对象编程的完整体系。
struct vs class:唯一区别是默认访问权限,class 默认 private。this 指针:对象的基本结构。public / private / protected:封装的三把锁,getter/setter 提供受控的访问。operator< 配合 sort 是竞赛中最常用的运算符重载。static 成员属于整个类,不依赖任何对象;必须在类外定义。protected 专为继承设计。virtual 实现动态绑定;父类析构函数必须是虚的;用 override 防手误。= 0 定义接口规范,抽象类不能实例化,只能用指针/引用统一管理子类。