继承让新类复用已有类的代码,多态让同一段代码对不同对象表现出不同行为。这两件事合在一起,是面向对象"开闭原则"(对扩展开放、对修改关闭)的技术基础。本文用一个动物例子上手,再讲透背后的虚函数机制。

1. 为什么需要继承

没有继承时,猫和狗各写一份 speak(),重复代码越堆越多。有了继承,公共部分放进基类,差异部分由子类覆盖:

class Animal {
public:
    void eat() { /* 所有动物都会吃 */ }
    virtual void speak() { cout << "..." << endl; }
};

class Dog : public Animal {
public:
    void speak() override { cout << "汪汪" << endl; }
};

2. 继承的语法与访问权限

继承方式 public/protected/private 决定基类成员在新类中的可见性。实际工程 99% 用 public 继承,它表达"子类是基类的一种"(is-a)关系。

基类成员public 继承后子类内可见外部可见
publicpublic
protectedprotected
private不可见

子类构造函数必须显式调用基类构造函数(默认调用无参版):

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}   // 初始化基类部分
};

3. 虚函数与 override

普通成员函数按"指针类型"调用,虚函数按"实际对象类型"调用——这就是多态的关键。C++11 起用 override 标注覆写,编译器帮你检查签名是否写错:

void make_speak(Animal& a) {
    a.speak();   // 传入 Dog 就汪汪,传入 Cat 就喵喵
}

4. 纯虚函数与抽象类

把虚函数写成 = 0,它就成了纯虚函数,所在类变为抽象类——不能实例化,只能被继承。这用来定义"接口":

class Shape {
public:
    virtual double area() const = 0;   // 纯虚函数
    virtual ~Shape() = default;        // 别忘了虚析构!
};

class Circle : public Shape {
    double r;
public:
    Circle(double r) : r(r) {}
    double area() const override { return 3.14159 * r * r; }
};

5. 多态的实现原理:虚函数表

含虚函数的类,每个对象开头藏着一个指针 vptr,指向该类的"虚函数表"(vtable),表里存着各虚函数的真实地址。调用 a.speak() 时,编译器生成的是"查表跳转",所以运行时才知道该调哪个版本。代价:每个对象多 8 字节(64 位平台) + 一次间接跳转。

6. 为什么析构函数要 virtual

通过基类指针 delete 派生对象时,如果析构函数不是虚的,只会调用基类析构,派生类资源泄漏。这是 C++ 最经典的坑之一:

Animal* p = new Dog("旺财");
delete p;   // 非虚析构:只析构 Animal 部分 → 泄漏

规则很简单:只要类里有虚函数,析构函数就声明为 virtual

💡 记住口诀:"基类指针,派生对象;虚函数按对象走,非虚函数按指针走"。写一个包含 Animal/Dog/Cat 的小程序,用 vector<Animal*> 存起来统一调用 speak(),亲眼看多态生效。