
文章目录一、继承语法和初始化子类怎么调用父类什么是覆盖override什么是隐藏hide子类对象赋值给父类对象会发生什么父类对象赋值给子类对象又会发生什么用指针/引用解决切片问题生命周期管理和默认函数行为多继承和菱形继承二、多态什么是多态为什么需要多态怎么实现多态多态的底层原理抽象类和纯虚函数父类析构函数必须设为虚函数哪些函数不能设为虚函数案例分析三、总结一、继承语法和初始化继承的语法非常简单class Mage : public Hero从物理内存层面来看子类对象本质是包裹着一个完整的父类对象这就决定了生命周期的规则现有父后有子在构建子类自己独特属性之前必须先完整的初始化父类部分直接看代码#includeiostreamclassHero{protected:// 允许子类访问但不允许外部访问std::string _name;int_hp;public:Hero(std::string n,inth):_name(n),_hp(h){std::coutHero基础构造完成: _namestd::endl;}};// 派生出法师类多了个魔法值属性// 构造要接受name、hp、mp参数怎么写构造classMage:publicHero{private:int_mp;// 名字和生命已经继承父类了魔法值新增自己独有public:// 继承了父类存在属性的构造直接调用父类构造// 自己新增的属性直接初始化即可Mage(std::string n,inth,intm):Hero(n,h),_mp(m){std::cout法师构造完成: _namestd::endl;}};子类怎么调用父类什么是覆盖override什么是隐藏hide调用子类天生拥有父类的非私有方法可以直接调用。遇到命名冲突或者显式调用父类版本通过域作用限定符定位-父类名::方法名()覆盖/重写override多态的基石父类用virtual关键字开放了一个虚函数接口子类保持函数签名完全一致进行重写。发生在运行期动态绑定看的是对象在内存中的真实模型隐藏/重定义hide这是C作用域查找规则导致的现象只要子类定义了父类同名的函数不管参数列表是否相同也不管有没有virtual父类的所有同名函数在子类的作用域中都会被遮蔽。发生在编译期静态绑定看的是指针/引用类型给Hero和Mage加上动作直接看代码#includeiostreamclassHero{protected:// 允许子类访问但不允许外部访问std::string _name;int_hp;public:Hero(std::string n,inth):_name(n),_hp(h){std::coutHero基础构造完成: _namestd::endl;}// 普通方法没有virtualvoidattack(){std::cout Hero 平astd::endl;}// 虚方法允许子类重写virtualvoidskill(){std::coutHero 释放一技能std::endl;}};// 派生出法师类多了个魔法值属性// 构造要接受name、hp、mp参数怎么写构造classMage:publicHero{private:int_mp;// 名字和生命已经继承父类了魔法值新增自己独有public:// 继承了父类存在属性的构造直接调用父类构造// 自己新增的属性直接初始化即可Mage(std::string n,inth,intm):Hero(n,h),_mp(m){std::cout法师构造完成: _namestd::endl;}// 1. 隐藏/重定义名字相同遮蔽了父类的attackvoidattack(){std::coutMage 平astd::endl;}// 2. 覆盖/重写重写父类的虚函数// override 让编译器检查函数前面有没有写错voidskill()override{std::coutMage 释放一技能std::endl;}voidcombo(){std::cout---- 连招开始 ----std::endl;skill();// 调用自己的Hero::attack();// 调用父类被隐藏的方法}};子类对象赋值给父类对象会发生什么父类对象赋值给子类对象又会发生什么把子类对象直接按值赋给父类对象会发生什么intmain(){MagemyMage(Medivh,100,200);Hero myHeromyMage;myHero.attack();myMage.skill();myMage.combo();return0;}梳理一下整个流程myMage实例化遵从先有父后有子原则先调用Hero的构造而第一个参数传的是Medivh输出Hero基础构造完成: Medivh父类初始化完毕Mage执行自己的构造输出法师构造完成: Medivh子类myMage赋值给父类myHeromyHero只开辟了能装得下_name和_hp的栈内存myMage强塞入这个空间发生切片_mp直接丢弃myHero是个纯粹的Hero对象调用父类方法输出Hero 平amyMage是法师调用自己重写的skill()输出Mage 释放一技能进入combo调用子类自己的skill()combo内部指明了被隐藏的父类方法Hero::attack()输出---- 连招开始 ----\nMage 释放一技能\nHero 平a为什么第三步myHero.attack()没有表现出法师的属性对象切片发生在按值赋值或按值传参时把一个子类对象赋值给一个父类对象时编译器只会将子类中属于父类的那一部分数据拷贝而子类独有的数据和子类的身份会被丢弃一切为了内存安全myHero栈上只申请了16字节如果强行把24字节的myMage塞进去会导致栈溢出程序当场崩溃切片是按值传递时保护程序正常运行的策略父类对象给子类对象赋值会咋样编译器会直接报错绝对禁止myHero的内存里只有 16 个字节包含_name和_hp但myMage作为一个法师它是 24 个字节它还需要一个_mp魔法值如果把父类硬塞给子类法师的_mp从哪里来根本没有数据可以填补这块内存这会导致未定义的行为用指针/引用解决切片问题子传父值传递会切片父传子会报错那么就可以使用指针/引用解决问题向上转型子类地址传给父类指针voiduseSkill(Heroh){h.skill();// 如果传入的是法师这里会自动调用法师的一技能}intmain(){MagemyMage(Medivh,100,200);useSkill(myMage);}向下转型父类指针转回子类有时候我们拿到了父类指针Hero*但是业务需要用到法师的技能要给法师回蓝了这时候就要把父类指针转回子类注意一定要用dynamic_cast更安全voidrestoreMp(Hero*hptr){Mage*mptrdynamic_castMage*(hptr);if(mptr!nullptr){// 转换成功说明hptr指向的就是法师std::cout是法师准备回蓝std::endl;mptr-setMp(100);}else{std::cout不是法师无法回蓝std::endl;}}生命周期管理和默认函数行为当一个Mage对象走到生命尽头被销毁时到底是先拆外墙先调~Mage()析构还是先拆地基先调~Hero()析构永远是一个原则先构造的后析构这里需要注意一个坑点// 用父类指针去管理 new 出来的子类对象Hero*myPlayernewMage(Medivh,100,200);// ...// 玩家下线销毁对象deletemyPlayer;// 这里隐藏着 C 面试必考的致命 BugmyPlayer是一个Hero*父类指针如果我们在Hero的析构函数~Hero()前面不加virtual关键字这叫“静态绑定”编译期执行看指针类型编译器在执行delete myPlayer时只看到了Hero*只会调用~Hero根本不关心底层是什么类型结果就是根本调用不到~Mage()内存泄漏了所以只要一个类会被继承它的析构函数就必须声明为虚函数再来看一个案例classHero{public:Hero(){}Hero(constHeroother){std::coutHero 拷贝构造std::endl;}};classMage:publicHero{public:Mage(){}Mage(constMageother){std::coutMage 拷贝构造std::endl;}};intmain(){Mage m1;Mage m2m1;// 触发拷贝构造return0;}这份代码有什么问题再次强化规则先有父后有子不论任何构造只要产生子类对象必须先初始化父类部分第一行分配好m1的内存Hero基础部分和Mage专属部分m1是空的因为两者是无参构造第二行m2是**正在诞生的对象之前没有触发拷贝构造**那么还是先调用Hero::Hero()父类基础打好之后调用Mage::Mage(const Mage other)此时的m2是畸形的m2的父类属性是空白的子类属性拷贝了m1的并没有把m1完整的内容拷贝下来修改// 传入 other (其实就是 m1)Mage(constMageother):Hero(other){std::coutMage 拷贝构造std::endl;}other是一个Mage的引用但把它传给了Hero的拷贝构造函数安全的向上转型编译器会切出m1的Hero部分传给父类进行拷贝多继承和菱形继承新增角色classHero{int_hp;};classMage:publicHero{};classWarrior:publicHero{};// 战斗法师classBattleMage:publicWarrior,publicMage{};intmain(){BattleMage bm;// 报错二义性// BattleMage有两个父类Warrior和MageWarrior和Mage都有_hp属性二者冲突//bm._hp 100;// 指明作用域呢逻辑错误一个英雄两个血条// bm.Warrior::_hp 100;// bm.Mage::_hp 200;return0;}BattleMage继承了两份完整的Hero::_hp造成了空间冗余存两份一样的数据导致了二义性想扣血回血编译器指向哪一个引入虚继承解决在产生分叉的那一层也就是Warrior和Mage继承Hero的时候加上virtual关键字classHero{public:int_hp;};classMage:virtualpublicHero{};classWarrior:virtualpublicHero{};classBattleMage:publicMage,publicWarrior{};intmain(){BattleMage bm;bm._hp100;return0;}底层发生了什么对于virtual public Hero时编译器会在Warrior和Mage的内存里塞入一个隐藏指针叫做vbptr(虚基类指针)这个指针指向一张表虚基表表里记录了真正的Hero数据相对于当前位置的偏移量最终组装BattleMage时Hero数据会放在内存最底部Warrior和Mage的vbptr都指向Hero二、多态什么是多态为什么需要多态怎么实现多态多态的本质是同一个指令不同的执行结果普通的函数时早绑定静态绑定编译时就定死了地址多态是晚绑定动态绑定程序跑起来真正拿到那个对象时才知道该执行哪些代码要实现多态需要同时满足血缘必须有继承关系重写父类必须用virtual开放虚函数子类必须完成函数前面一模一样的重写指针/引用必须通过父类指针/引用调用该虚函数直接看代码#includeiostreamclassHero{public:// 1. 父类开放虚函数virtualvoidskill(){std::coutHero skillstd::endl;}};classMage:publicHero{// 2. 存在继承public:// 3. 子类重写voidskill()override{std::coutMage skillstd::endl;}};voidplay(Heroh){// 传值会发生切片h.skill();// 4. 父类指针/引用调用}intmain(){Mage myMage;play(myMage);return0;}为什么需要多态多态是开启开闭原则的钥匙什么是开闭原则对扩展开放策划提出新需求系统很容易加入新功能对修改关闭加入新功能时绝对不能动原本已经测试跑通稳定运行的代码打个比方电脑主板的电路是焊死的对修改关闭但是主板留出了USB接口我们只需要把带USB插头的新设备插上去就行对扩展开放多态就是USB接口协议假设要写一个群体伤害结算引擎要求遍历全场玩家让大家集体释放技能没有多态为了区分角色可以用enum贴上标签然后就是不断地if-else或者switch-case现在新增角色必须在enum的老代码中添加新标价必须添加新的if-else逻辑enumRoleType{WARRIOR,MAGE};classRole{public:RoleType type;// 没有虚函数};// 核心结算引擎代码voidreleaseAllSkills(std::vectorRole*roles){for(inti0;iroles.size();i){// 每次都要判断类型if(roles[i]-typeWARRIOR){// 强转成战士调战士技能...}elseif(roles[i]-typeMAGE){// 强转成法师调法师技能...}}}有多态把角色Role变成一个包含虚函数的父类把释放技能接口skills变成只认接口的虚函数现在新增角色只需要继承Role重写skills即可底层的引擎代码一字不动classRole{public:virtualvoidskill()0;// USB 接口定义好了};classWarrior:publicRole{public:voidskill()override{/* 战士砍人 */}};classMage:publicRole{public:voidskill()override{/* 法师放火 */}};// 核心结算引擎代码永远不需要再修改了voidreleaseAllSkills(std::vectorRole*roles){for(inti0;iroles.size();i){roles[i]-skill();// 直接调多态会通过 vptr 自动查表找到真实的技能}}多态的底层原理只要类带有virtual关键字编译就会修改底层多态由一表一指针支撑虚函数表vtable属于类公用。编译期生成存在内存的只读数据段.redata表里的地址存的是这个类中所有虚函数的内存地址虚表指针vptr属于对象私有。在对象实例化瞬间编译器会在对象内存模型的最前面通常8字节塞入这个指针让它指向自己所属类的vtable当执行父类指针-虚函数()时底层执行寻址顺着父类指针找到内存中的真实对象拔针从对象的头8字节提取出vptr查表顺着vptr找到类对应的vtable调用根据函数在表中的偏移量取出真实的函数地址跳转执行抽象类和纯虚函数纯虚函数在虚函数声明的末尾加上 0并且不写大括号里的实现体。例如virtual void attack() 0;抽象类只要一个类里面包含至少一个纯虚函数这个类就是抽象类注意抽象类一定不能实例化对象抽象类是不完整的纯虚函数没有代码实现无法执行子类继承抽象类必须重写所有的虚函数子类不重写虚函数子类也变成抽象类了不能实例化父类析构函数必须设为虚函数直接看代码// 父类析构必须设置为虚函数classHero{public:Hero(){std::coutHero()std::endl;}~Hero(){std::cout~Hero()std::endl;}};classMage:publicHero{private:int*magicWand;public:Mage(){magicWandnewint[100];std::coutMage()std::endl;}~Mage(){delete[]magicWand;std::cout~Mage()std::endl;}};intmain(){Hero*hnewMage();std::cout-------游戏结束释放资源-------std::endl;deleteh;return0;}输出Hero() Mage() -------游戏结束释放资源------- ~Hero()分析在delete[] magicWand时底层做两件事调用析构函数清理对象内部申请的资源释放内存把对象本身占据的内存释放注意看h是父类指针父类的虚函数没有virtual编译器就会静态绑定编译期执行那么只关注指针/引用发现是Hero*指针直接调用~Hero()编译器根本不管底层的类是什么内存泄漏了加上virtual之后~Hero()进了虚函数表在delete[] magicWand时动态绑定执行期间决议只关注对象本身类型编译器通过h指针找到内存里真实的Mage对象从对象的头部拔出虚表指针查表发现真正的析构函数是~Mage()精准调用~Mage()结束后自动调用父类~Hero()哪些函数不能设为虚函数多态的基础是可以通过对象的this指针找到头部vptr因此没有this指针或者vptr没有准备好的函数绝不能是虚函数构造函数对象正在构造还没完全诞生vptr此时指向的是正在构造的层级不是最终子类多态成立不了静态成员函数static函数属于整个类没有具体的实例化对象连this指针都没有内联函数inline是在编译期把代码直接展开virtual是在运行期查表二者不在同一时间线友元函数友元函数是外人不是类的成员案例分析#includeiostream#includecstringclassHero{public:inthp;Hero(){hp100;}virtualvoidskill(){std::coutHero 基础技能std::endl;}virtual~Hero()default;};classMage:publicHero{public:intmp;Mage(){memset(this,0,sizeof(Mage));mp200;}voidskill()override{std::coutMage 暴风雪std::endl;}};intmain(){Hero*pnewMage();std::cout--- 准备释放技能 ---std::endl;p-skill();deletep;return0;}会发生什么注意看memset(this, 0, sizeof(Mage));this指针指向的空间全是0然后mp最后四字节被赋值为200有虚函数那么就有虚表指针直接看主函数父类指针定义子类对象正常多态p-skill()这一行有多态行为看对象类型是Mage类那么就调用Mage的skill()但是我要找p对象首地址啊我要拿虚表指针读到了0x00000000OS发送SIGSEGV信号杀死进程三、总结