7.3 抽象类与纯虚函数
从设计需求说起
Section titled “从设计需求说起”上一节的 Character 基类提供了 attack() 的默认实现——「进行了普通攻击」。但在实际设计中,Character 本身是一个抽象概念,不应该有具体的攻击方式;每种角色必须提供自己的攻击实现。
换句话说,我们希望:
Character类不能被直接实例化(没有「普通角色」这种东西)- 所有派生类必须实现
attack()函数,否则编译报错
纯虚函数和抽象类正是为此设计的。
在虚函数声明末尾写上 = 0,该函数就成为纯虚函数(Pure Virtual Function):
class Character {protected: std::string name;
public: Character(const std::string& name) : name(name) {} virtual ~Character() = default;
virtual void attack() = 0; // 纯虚函数:没有默认实现
void showStatus() const { std::cout << name << std::endl; }};纯虚函数只有声明,没有函数体(通常不提供实现)。它告诉编译器:「这个函数的具体行为由派生类决定,基类不提供默认版本。」
包含至少一个纯虚函数的类称为抽象类(Abstract Class)。抽象类不能被实例化:
int main() { // Character c("测试"); // 编译错误!Character 是抽象类,不能创建对象 return 0;}但抽象类可以作为指针或引用的类型,用于多态:
Character* ptr = new Warrior("亚瑟"); // 合法Character& ref = someWarrior; // 合法派生类必须实现纯虚函数
Section titled “派生类必须实现纯虚函数”如果派生类没有实现基类的所有纯虚函数,它自身也会成为抽象类,同样不能实例化:
class Character {public: virtual void attack() = 0; virtual void defend() = 0;};
class Warrior : public Character {public: void attack() override { std::cout << "挥剑!" << std::endl; } // 没有实现 defend()};
int main() { // Warrior w; // 编译错误!Warrior 仍然是抽象类(defend 未实现) return 0;}只有实现了全部纯虚函数的派生类才能实例化:
class Warrior : public Character {public: void attack() override { std::cout << "挥剑!" << std::endl; }
void defend() override { std::cout << "举盾防御!" << std::endl; }};
Warrior w; // 正确,所有纯虚函数都已实现完整示例:图形类层次
Section titled “完整示例:图形类层次”#include <iostream>#include <cmath>#include <vector>
// 抽象基类:图形class Shape {public: virtual ~Shape() = default;
virtual double area() const = 0; // 纯虚函数 virtual double perimeter() const = 0; // 纯虚函数 virtual std::string typeName() const = 0;
void print() const { std::cout << typeName() << " 面积=" << area() << " 周长=" << perimeter() << std::endl; }};
// 圆形class Circle : public Shape {private: double radius;
public: Circle(double r) : radius(r) {}
double area() const override { return 3.14159265 * radius * radius; }
double perimeter() const override { return 2 * 3.14159265 * radius; }
std::string typeName() const override { return "圆形"; }};
// 矩形class Rectangle : public Shape {private: double width, height;
public: Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
double perimeter() const override { return 2 * (width + height); }
std::string typeName() const override { return "矩形"; }};
// 三角形class Triangle : public Shape {private: double a, b, c; // 三条边
public: Triangle(double a, double b, double c) : a(a), b(b), c(c) {}
double area() const override { double s = (a + b + c) / 2; return std::sqrt(s * (s - a) * (s - b) * (s - c)); }
double perimeter() const override { return a + b + c; }
std::string typeName() const override { return "三角形"; }};
int main() { std::vector<Shape*> shapes; shapes.push_back(new Circle(5.0)); shapes.push_back(new Rectangle(4.0, 6.0)); shapes.push_back(new Triangle(3.0, 4.0, 5.0));
for (const Shape* s : shapes) { s->print(); // 多态调用 }
// 清理内存 for (Shape* s : shapes) { delete s; }
return 0;}输出:
圆形 面积=78.5398 周长=31.4159矩形 面积=24 周长=20三角形 面积=6 周长=12这个设计的要点:
Shape定义了统一接口(area()、perimeter()、typeName())- 每种图形实现自己的计算逻辑
print()是非虚函数,基于虚函数构建通用行为- 新增图形类型只需继承
Shape并实现纯虚函数,已有代码无需修改
当一个抽象类只包含纯虚函数(没有数据成员和非虚函数),它就起到了接口(Interface)的作用:
class Serializable {public: virtual ~Serializable() = default; virtual std::string serialize() const = 0; virtual void deserialize(const std::string& data) = 0;};
class Printable {public: virtual ~Printable() = default; virtual void print(std::ostream& out) const = 0;};C++ 没有像 Java 那样的 interface 关键字,但通过纯虚函数类可以达到同样的效果。一个类可以同时继承多个接口类(多重继承),这是 C++ 中多重继承最常见的正当用法,将在下一节讨论。
纯虚函数可以有实现
Section titled “纯虚函数可以有实现”虽然不常见,但纯虚函数可以提供实现。派生类仍然必须重写它,但可以通过作用域解析符调用基类的版本:
class Character {public: virtual void attack() = 0; // 仍然是纯虚函数};
// 在类外提供实现void Character::attack() { std::cout << "基础攻击动画" << std::endl;}
class Warrior : public Character {public: void attack() override { Character::attack(); // 调用基类提供的默认实现 std::cout << "附加剑气效果!" << std::endl; }};这种技巧适用于「派生类必须重写,但可以复用一部分通用逻辑」的场景。
Q:抽象类可以有构造函数吗?
可以。虽然抽象类不能直接实例化,但派生类构造时需要调用基类的构造函数来初始化基类部分的成员。
Q:析构函数可以是纯虚函数吗?
可以,但必须同时提供实现(因为派生类析构时会调用基类的析构函数)。这是一种让类成为抽象类的技巧,适用于类中没有其他适合作为纯虚函数的成员函数的情况:
class AbstractBase {public: virtual ~AbstractBase() = 0; // 纯虚析构函数};
AbstractBase::~AbstractBase() {} // 必须提供实现Q:抽象类和普通基类如何选择?
如果基类的某些函数必须由派生类提供具体实现,使用纯虚函数。如果基类能提供合理的默认行为,使用普通虚函数。两者可以混合使用。