继承让新类复用已有类的代码,多态让同一段代码对不同对象表现出不同行为。这两件事合在一起,是面向对象"开闭原则"(对扩展开放、对修改关闭)的技术基础。本文用一个动物例子上手,再讲透背后的虚函数机制。
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 继承后 | 子类内可见 | 外部可见 |
|---|---|---|---|
| public | public | ✔ | ✔ |
| protected | protected | ✔ | ✘ |
| 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(),亲眼看多态生效。