14. 继承与派生:单继承、多继承、虚继承、继承中的构造函数与析构函数、访问控制
继承,说白了就是代码复用的一种高级手段。你写了一个类,另一个类想直接拿它的功能,再往上加点自己的东西——这就是继承。我刚开始学C++那会儿,觉得继承不就是“儿子拿老子的东西”嘛,后来踩了不少坑才明白,这里面的门道远比想象的多。
核心一句话:继承是C++面向对象的三大特性之一,它允许我们基于已有类创建新类,复用、扩展、甚至改写原有行为。
14.1 单继承:最朴素的复用方式
单继承就是一个派生类只从一个基类继承。这是最常用的形式,也是我建议初学者最先掌握的。
class Animal {
public:
void eat() { cout << "吃东西" << endl; }
};
class Dog : public Animal {
public:
void bark() { cout << "汪汪" << endl; }
};
你看,Dog 类没写 eat 方法,但它能用。这就是继承的直接好处。我在项目中遇到过很多新手,恨不得把所有功能塞到一个类里,其实拆开用继承,代码清爽得多。
14.2 多继承:一把双刃剑
C++ 允许一个类继承多个基类。听起来很强大,对吧?但我要说句实话:多继承用不好,就是给自己挖坑。
class Teacher {
public:
void teach() { cout << "教书" << endl; }
};
class Researcher {
public:
void research() { cout << "科研" << endl; }
};
class Professor : public Teacher, public Researcher {
public:
void guide() { cout << "指导研究生" << endl; }
};
这个例子还算简单。但如果你两个基类都有同名方法,或者有相同的基类,问题就来了。嗯,这里要注意——多继承最大的坑就是“菱形继承”。
我曾经踩过的坑:两个基类都继承自同一个父类,派生类再同时继承这两个基类,结果数据成员被复制了两份。调用父类方法时,编译器直接报歧义错误。那会儿排查了一下午,最后发现是继承链设计出了问题。
14.3 虚继承:解决菱形继承的钥匙
为什么会这样?因为默认情况下,每个继承路径都会创建一份基类的副本。虚继承就是告诉编译器:不管通过几条路径继承这个基类,只保留一份。
class Animal {
public:
int age;
};
class Mammal : virtual public Animal {};
class Bird : virtual public Animal {};
class Bat : public Mammal, public Bird {}; // 只有一份 Animal
加了 virtual 关键字后,Bat 类里只有一份 age 成员。我个人习惯是:只要发现继承层次超过两层,就考虑用虚继承。宁可多写几个 virtual,也别等出 bug 再回头改。
小技巧:虚继承的构造函数调用规则比较特殊——最派生类要负责初始化虚基类。你想想看,如果让中间类去初始化,那到底听谁的?所以 C++ 规定:虚基类由最终派生类直接初始化。
14.4 继承中的构造函数与析构函数
这里有个顺序问题,我建议你死记硬背:构造从基类到派生类,析构从派生类到基类。
class Base {
public:
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived构造" << endl; }
~Derived() { cout << "Derived析构" << endl; }
};
输出顺序是:Base构造 → Derived构造 → Derived析构 → Base析构。就像盖房子,先打地基再盖楼,拆的时候先拆楼再拆地基。
派生类构造函数怎么给基类传参?用初始化列表:
class Base {
public:
Base(int x) { cout << "Base: " << x << endl; }
};
class Derived : public Base {
public:
Derived(int a, int b) : Base(a) {
cout << "Derived: " << b << endl;
}
};
注意:如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类构造函数。否则编译直接报错,没得商量。
14.5 访问控制:谁能看到什么
继承方式有三种:public、protected、private。说白了就是控制基类成员在派生类中的访问权限。
| 基类成员权限 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
我个人的经验是:绝大多数场景用 public 继承就够了。protected 继承偶尔用在框架设计中,private 继承我几乎不用——它更像是一种“实现复用”而不是“接口复用”。
避坑指南:我曾经用 private 继承想隐藏基类接口,结果派生类想调用基类方法还得用 using 声明重新暴露,代码越写越别扭。后来改用组合模式,清爽多了。所以我的建议是:能用组合就别用 private 继承。
知识体系总览
下面这张图是我整理的本章核心脉络,你看一眼就能把整个继承体系串起来:
这张图把本章的五个核心知识点串在了一起。你从上往下看:继承是总纲,下面分出单继承、多继承、虚继承、构造析构,最下面是访问控制贯穿始终。我个人觉得,把这五个点吃透了,C++ 的继承体系你就掌握了七八成。
嗯,最后说一句:继承不是万能的。我在实际项目中见过有人为了复用两行代码,硬生生搞出五层继承,最后改一个基类,所有派生类都跟着崩。该用组合的时候别犹豫,该用继承的时候也别怕。工具本身没有好坏,关键看你怎么用。