前言:
各位代码航海家,欢迎回到C++继承宇宙 !上回我们解锁了继承的「基础装备包」,成功驯服了public
、protected
和花式成员隐藏术。但------
⚠️前方高能预警: 继承世界的暗流涌动远不止于此!今天我们将勇闯三大神秘海域:
-
多继承の百慕大三角 👻------当你的类同时认了两个"爹"
-
虚继承の量子纠缠 ⚛️------专治"菱形继承"引发的时空悖论
-
继承构造/析构の连锁反应 💥------比《信条》更烧脑的逆向工程
准备好你的IDE光剑和调试护盾,我们即将潜入继承深渊!
🎮 本关Boss预告
class 爷爷 {};
class 爸爸1 : virtual public 爷爷 {}; // 虚继承!
class 爸爸2 : virtual public 爷爷 {};
class 你 : public 爸爸1, public 爸爸2 {}; // 多继承の最终形态!
灵魂拷问:
-
当
爷爷
的遗产被爸爸1
和爸爸2
重复继承时,你
会继承几份祖传代码? -
为什么祖师爷要发明"虚继承"这种黑科技?
-
构造函数们究竟在继承链上玩什么接力赛?
(摸鱼提示:文末附赠**「菱形继承生存指南」**,保你跳出编译错误的黑洞!)
☕ 建议装备
-
咖啡因补给包 ×1
-
防止指针错乱的思维导图 ×1
-
暂时忘记Java/Python的勇气 ×1
⚡ 3秒后进入继承下篇------ 程序员,你是否选择「接受挑战」?
(按下F5继续执行代码...🚀)
1.派生类的默认成员函数
1.1.四个常见的默认成员函数

通过之前的学习,我们知晓,C++有六种默认构造函数,忘记的或者是不知晓的读者可以看我之前写过的初始C嘎嘎的文章,那里详细记载了,当然,主要的默认成员函数其实有四种:构造函数、拷贝构造函数、赋值运算符重载,析构函数默认的意思指的是我们不写,编译器会帮我们自动生成一个,那么在派生类中,这四种函数又是如何生成的呢?
1.**派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。**其实我们可以把基类当做是一个自定义类型的成员变量,众所周知,自定义类型的成员变量会调用自己的默认构造函数,如果没有默认构造函数的时候,编译器就会报错,必须显示的去调用构造函数。而基类的默认构造也和这个类似,大多数情况我们都是不需要自己调用构造函数的,当然,不排除我们没写默认构造函数,那么它的用法如下所示:
cpp
class Person
{
public:
Person(const string& name) :t_name(name) //这里就用一个正常的构造函数,但不是默认构造函数
{}
protected:
string t_name;
};
class Student :public Person
{
public:
Student(const string& _t_name,const string& name) : Person(_t_name),_name(name) //直接调用其父类的构造函数即可,当做整体进行构造
{}
protected:
string _name;
};
2.**派生类对象初始化先调用基类构造再调派生类构造。**这个很好去理解,因为基类是比派生类的成员变量出现的早的,所以出现最早的优先调用构造函数,所以先调用基类的构造在调用子类的构造。
3.派生类的拷贝构造函数必须调用基类的拷贝构造来完成基类的拷贝初始化 。这个也是比较好理解的,因为拷贝构造需要我们传入对应的对象,不像默认构造函数那样有缺省值直接调用缺省值就好,所以它要求我们去显示的调用拷贝构造,这里的知识和我们第一篇继承的知识联系在了一起,不知道读者是否还记得**"切片"**,也就是public继承的派生类对象可以赋值给基类的指针 / 基类的引用。也就是如下图所示:

此时我们仅需在派生类的拷贝构造函数中传入派生类对象的引用,并且基类的拷贝构造函数的参数必须是基类的引用,此时我们就可以通过切片把派生类对象中基类的一部分切给要进行拷贝构造的派生类的基类部分了,可谓是非常的优美。下面我将通过一个例子让各位了解用法。
cpp
class Person
{
public:
Person(Person& s1):t_name(s1.t_name)
{}
protected:
string t_name;
};
class Student :public Person
{
public:
Student(Student& s1): Person(s1),_name(s1._name) //直接把对象传到基类即可
{}
protected:
string _name;
};
4.派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。这个为什么显示调用和拷贝构造是一样的,这里我就不详说了,不过这里也是牵扯到了上篇博客的一个小知识点:隐藏的知识,当函数名相同的时候,并且函数分别属于基类的作用域和派生类的作用域时,那么此时就是构成了隐藏关系。而基类和子类的赋值运算符重载名字是一模一样的,所以构成了隐藏关系,此时我们需要指定类域才可以调用基类的赋值运算符重载,它的用法如下所示:
cpp
class Person
{
public:
Person& operator=(const Person& s1)
{
t_name = s1.t_name;
return *this;
}
protected:
string t_name;
};
class Student :public Person
{
public:
Student& operator=(const Student& s1)
{
Person::operator=(s1); //一定要指明基类的作用域
_name = s1._name;
return *this;
}
protected:
string _name;
};
5.**派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。**这个也是牵扯到了我们第一次初学C++类和对象的时候学到的知识:后定义的先析构,因为派生类的成员是后来定义的,所以它是最开始析构的,所以我们先清理派生类的成员函数,在进行基类成员的清理。当然,析构函数我们最好还是不要显示的调用,至于为何,请看第七点。
6.**派生类对象析构清理先调用派生类析构再调基类的析构。**这个上面解释了,我就不细说了。
7**.因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。**所以我的建议就是析构函数我们还是老老实实的让派生类自己去调用基类的析构函数吧,这样比较方便。

各位读者,看到这里是不是手痒想当"代码界的灭霸"了?别急着敲继承的响指!今天咱们要聊的C++贵族礼仪------如何让类在族谱上写下"此脉单传,永不加粗"!
想打造这种孤高の王族血统?可不是给构造函数上锁那么简单(那招就像在城堡门口挂"内有恶犬",防得住新手防不住杠精)。C++11早给我们准备了基因改造药剂------final关键字!这玩意儿比皇后的毒苹果还好使:
1.2.实现一个不能被继承的类
方法一
不知道各位读者是否还记得我上篇文章所讲述的继承方式,基类使用private成员限定符限定的成员是不可以被派生类使用的,那么这就相当于不可以被派生类继承(当然,还是继承下来的,只不过不允许使用)了。所以当我们让基类的构造函数被private限定以后,那么此时派生类就无法调用基类的默认构造函数,从而导致派生类无法实例化出对象,这就意味这个类是无法被继承的。如下所示:
cpp
class Person
{
private:
Person() {}
};
class Studnet : public Person
{
public:
Studnet() {}
public:
};

不过这个做法是有点不优美的,因为它有个致命的缺陷:"家贼难防",如果给派生类悄悄的开一个后门:friend通行证(友元),那么此时派生类依然可以调用基类的拷贝构造函数:
cpp
class 薛定谔的保险箱 {
private:
薛定谔的保险箱() {}
friend class 量子穿墙术; // 偷偷塞钥匙
};
class 量子穿墙术 : public 薛定谔的保险箱 {
// 竟然成功继承!科学伦理委员会震怒
};
这个方法的局限性很大,所以准确来说:通过私有化构造函数可以制造伪·不可继承类,但需要配合杜绝friend后门,而C++11的final关键字才是正宗的绝育手术刀~(≧∇≦)/~(这就是方法二)。
方法二
C++11新增加了一个关键字:final关键字,final修改基类,那么派生类就无法继承了,它的用法如下所示:
cpp
class Person final
{
public:
Person() {}
};
class Studnet : public Person
{
public:
Studnet() {}
public:
};

2.继承和友元
友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员。这个很好理解,下面我就一个有趣的例子来给各位说明一下:
cpp
class 祖传咸鱼配方 { // 传男不传女的独门秘方
private:
int 祖传盐量 = 999; // 传家宝级别的盐值
friend class 大厨; // 授予传功长老权限
};
class 改良版咸鱼配方 : public 祖传咸鱼配方 {
private:
int 科技盐量 = 666; // 偷偷加了海克斯科技
};
class 大厨 {
public:
void 烹饪秘术(祖传咸鱼配方& 老坛) {
老坛.祖传盐量 = 9527; // 畅通无阻(毕竟有friend通行证)
}
void 黑暗料理(改良版咸鱼配方& 新品) {
// 新品.科技盐量 = 1314; // 报错!编译器怒斥:你只是他爹的基友!
}
};
// 剧情彩蛋:就算逆天改命也不行!
class 逆子配方 : public 祖传咸鱼配方 {
friend class 大厨; // 试图继承爹的社交圈
private:
int 叛逆盐量 = 233;
};
// 大厨试图搞事情:
void 偷天换日() {
逆子配方 黑化版;
黑化版.叛逆盐量 = 666; // 依然报错!编译器冷笑:父辈的friend不是你的ATM机!
}
这波操作生动诠释了:
-
友元关系比钢铁直男还直------绝不拐弯继承(派生类不会自动获得基类友元)
-
友元权限比小区门禁还严------只认身份证原件(即便派生类主动示好,基类友元也摸不到派生类的私有成员)
-
想开后门?除非上演《无间道》------在派生类里重新声明friend(但这样可就背叛革命了)
3.继承和静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。通过一个简单的例子就可以知晓这个定义:
cpp
class 魔法始祖 {
public:
static int 魔力源泉; // 全魔法界共享的充电宝
};
int 魔法始祖::魔力源泉 = 100; // 在霍格沃茨地窖初始化
class 火系法师 : public 魔法始祖 {};
class 水系法师 : public 魔法始祖 {};
class 风系法师 : public 魔法始祖 {};
int main() {
火系法师 甘道夫;
水系法师 梅林;
风系法师 萨尔;
// 所有法师共享同一个魔法池
甘道夫.魔力源泉 += 50; // 火法师给充电宝续费
cout << 梅林.魔力源泉 << endl; // 输出150(水系躺着蹭网)
萨尔.魔力源泉 -= 70; // 风系法师偷偷下载小电影
cout << 魔法始祖::魔力源泉 << endl; // 输出80(祖宗家底被败光)
// 见证奇迹的时刻!
cout << &甘道夫.魔力源泉 << " ←火法地址\n"
<< &梅林.魔力源泉 << " ←水法地址\n"
<< &萨尔.魔力源泉 << " ←风法地址" << endl;
// 三个地址完全相同,实锤共享内存
}
这段代码生动诠释了:
-
静态成员就像家族银行账户------所有子孙刷的都是同一张卡
-
无论通过基类还是派生类访问,操作的都是同一个内存地址
-
任一派生类搞事情,全家族成员都要背锅(值同步变化)
4.多继承以及其菱形继承问题
4.1.继承模型
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承。这个是比较好理解的,因为它的定义就和它的名字一样,下面我给出示例图:

单继承算是一个比较正常的继承了,但是如果单继承变为了多继承,那么会很逆天!
(敲黑板)各位程序员请注意,咱们今天要聊的是C++里的"多重身份危机"------多继承和它的狗血连续剧"菱形继承"!
第一幕:多继承の修罗场
想象一下,你是个时间管理大师:
cpp
class 社畜 : public 打工人, public 乙方舔狗, public 深夜码农 {};
这就是多继承------一个类同时认多个爹。内存布局就像叠罗汉,谁先继承谁在下面:
cpp
[打工人] → [乙方舔狗] → [深夜码农] → [社畜的私人数据]

但问题来了------如果多个爹有同名成员,编译器直接懵逼:
cpp
社畜 张三;
张三._存款 = -10086; // 编译器怒吼:你三个爹都有存款,我该改哪个?!
第二幕:菱形继承の家族伦理剧
来看这张祖传关系图:

结果Assistant继承时,老祖宗的基因复制了两份!这就是菱形继承的血泪史:
cpp
Assistant 小王;
小王.Student::_name = "学渣";
小王.Teacher::_name = "教授";
// 实际小王体内住着两个老祖宗,精分现场!
内存布局宛如祖传玉佩摔成两半:
cpp
[Student版Person] → [Teacher版Person] → [Assistant数据]
此时访问_name
就像问妈妈和老婆掉水里救谁------必须指定爹名:
cpp
cout << 小王.Student::_name; // "学渣"
cout << 小王.Teacher::_name; // "教授"
第三幕:虚继承の公证处协议
C++祭出大招:虚继承!相当于给老祖宗做公证:
cpp
class Student : virtual public Person {}; // 公证声明
class Teacher : virtual public Person {}; // 同上
此时孙辈的内存布局变成:
cpp
[虚表指针] → [Student数据] → [虚表指针] → [Teacher数据] → [老祖宗Person] → [Assistant数据]
虽然解决了数据冗余,但代价是:
- 构造顺序堪比宫廷剧------孙辈得直接给老祖宗上供:
cpp
Assistant::Assistant()
: Person("工具人"), Student(), Teacher() {} // 必须亲自初始化老祖宗
- 访问速度像去公证处盖章------多绕一层指针
终幕:多继承の终极考题
来看这道送命题:
cpp
Basel b1; Base2 b2;
class 缝合怪 : public Basel, public Base2 {};
缝合怪 obj;
Basel* p1 = &obj;
Base2* p2 = &obj;
void* p3 = &obj;
指针地址关系是?
-
A: p1 == p2 == p3 ❌(想得美)
-
B: p1 < p2 < p3 ❌(内存不是等差数列)
-
C: p1 == p3 != p2 ✅(Basel先继承,地址最低)
-
D: p1 != p2 != p3 ❌(p3和p1指向同一起点)
总结陈词: 多继承就像同时认多个干爹------给钱时很爽,争家产时头大。而菱形继承则是家族内斗的终极形态,虚继承虽然能维稳,但操作难度堪比处理婆媳关系。珍爱生命,远离菱形!(Java笑而不语)
4.2.彩蛋:IO库中的菱形虚拟继承
其实我们日常使用iostream库就是一个菱形虚拟继承,其结构图如下所示:

5.继承和组合
各位读者请注意,最后咱们要聊的是面向对象界的"婆媳关系"------继承和组合!这可是代码界的"到底该听妈的还是听媳妇的"终极难题!
第一回合:继承------代码界的家族企业
继承就像你爹开公司,你直接当太子爷继承皇位:
cpp
class 富二代 : public 土豪爹 {
// 自动获得爹的别墅、跑车、黑卡
};
优点:
-
是亲生的(is-a关系),直接拿爹的全部家当(包括私房钱)
-
白嫖式开发(白箱复用),连爹的日记本都能翻
缺点:
-
爹改遗嘱(修改基类),儿子当场破产(代码爆炸)
-
耦合度堪比连体婴,爹感冒儿子必发烧
经典翻车现场:
cpp
class 爹 {
public:
void 传家宝() { cout << "洛阳铲"; }
};
class 儿子 : public 爹 {};
// 某天爹考古入魔:
class 爹 {
public:
void 传家宝() { cout << "盗墓笔记"; } // 从工具升级成知识
};
// 儿子:???我铲子呢???
第二回合:组合------代码界的乐高大师
组合就像自己开公司,雇个专业经理人:
cpp
class 打工人 {
class 肝帝程序员 员工; // 组合一个996战士
class AWS云服务器 设备; // 再组个烧钱神器
};
优点:
-
是老板(has-a关系),只关心KPI(接口),不管员工私生活(黑箱复用)
-
耦合度堪比塑料友情,随时换掉摸鱼员工(维护性好)
经典操作:
cpp
class 特斯拉 {
Battery 电池; // 想换宁德时代?换!
Motor 电机; // 想用国产?换!
Autopilot 智驾; // 想用华为?换!(马斯克震怒)
};
// 组合就是:没有什么是换零件解决不了的
第三回合:继承 vs 组合の世纪对决
对比项 | 继承 | 组合 |
---|---|---|
关系 | 你是我儿子(is-a) | 你是我工具人(has-a) |
耦合度 | 脐带级绑定 | 点赞之交 |
封装性 | 爹的内裤都被看光 | 打码级保护 |
改需求伤害 | 家族式团灭 | 换个零件就能活 |
适用场景 | "狗是动物"这种铁律 | "车有轮胎"这种可拆卸关系 |
终极大招:面向对象の生存法则
-
能组合就别继承------少认爹少背锅,多个爹多个坟头
-
非要继承时:
-
确认关系铁如"企鹅是鸟"(虽然它不会飞)
-
准备迎接"爹动一下,儿改十行"的刺激生活
-
-
多态是继承の免死金牌------当你要召唤"虚函数"神龙时,该认爹还得认
总结陈词:
-
继承像结婚------高风险高回报,且行且珍惜
-
组合像恋爱------合则来不合则换,自由无负担
-
记住:代码不是血缘关系,少继承,多组合,你的头发会感谢你!
6.总结
本文到这里也就结束喽,今天我们完成了继承的全部内容的讲解,下面我简单的总结一下本文讲解的内容。
本章节の知识点爆米花
-
默认成员函数:四个"祖传家产"(构造/拷贝/析构/赋值),教你如何打造C++界的丁克家族(final大法)
-
继承与友元:塑料兄弟情------基类友元不会遗传,就像你爹的兄弟不会给你压岁钱!
-
静态成员:全家族共享的祖传充电宝(static),一人改参数,全家炸电路
-
多继承:修罗场の生存指南------内存布局叠罗汉,菱形继承精分现场,虚继承公证处の骚操作
-
组合vs继承:认爹不如雇工具人(has-a),代码界的乐高哲学------宁可多拼积木,少背族谱
前方高能预警
你以为继承的狗血剧这就结束了?Too young!下一章我们将迎来面向对象三幻神の最终形态------多态!
届时您将看到:
-
虚函数:C++版的"我变秃了也变强了"
-
动态绑定:运行时の分身术,让对象学会影流之主の秘技
-
纯虚函数:抽象类の"画饼大法",堪比老板的年终奖承诺
-
多态の黑暗面:虚表指针の内存迷踪,性能刺客の背刺警告
(剧透小剧场)
cpp
class 打工人 {
public:
virtual void 摸鱼() = 0; // 老板:这是纯虚函数,你必须实现!
};
class 程序员 : public 打工人 {
public:
void 摸鱼() override {
cout << "在GitHub刷绿格子" << endl; // 老板:这TM也算工作?!
}
};
};
准备好迎接"一个接口,千种姿势"的神奇世界了吗?下期我们将用多态实现:同一行代码,白天当社畜,晚上变蝙蝠侠的魔法操作!(≧∇≦)ノ
各位大佬下篇文章见啦!