C++继承
一、什么是继承
-
当遇到问题时,先查看现有的类是否能解决一部分问题,如果有则继承该类,并在此基础上扩展以此解决问题,从而缩短解决问题的时间(代码复用)
-
当遇到一个大而复杂的问题时,可以把大问题拆分成若干个不同的小问题,然后为每个小问题设计一个类来解决,最后通过继承的方式把这些类汇总到一个类中,从而解决大问题,以此降低问题的难度,可以同时让多个程序员共同解决大问题
-
派生类继承基类 子类继承父类
二、继承的语法
-
继承表
class Son : 继承表 [继承方式 父类名1,继承方式 父类名2,...] { 成员变量; public: 成员函数; };
-
继承方式
public
private
protected
三、继承的特点
-
C++中的继承可以有多个父类
-
子类会继承父类的所有内容,是否能用另说
-
子类对象可以向它的父类类型转换(缩小),但父类对象不能向子类类型转换(放大)
父类指针或引用可以指向子类对象,子类指针或引用不能指向父类对象(前提:必须以public继承父类)Base* b = new Son; √ Base& b = son; √ Son* s = new Base; × Son& s = base; ×
-
子类会隐藏父类的同名成员(成员变量、成员函数),不构成函数重载,因为作用域不同
同名成员被隐藏后,在子类中直接访问到的是子类的同名成员
但是可以通过 父类名::同名成员名 的方式指定访问父类同名成员 -
在执行子类的构造函数的初始化列表时,会按照继承表的顺序来执行父类的构造函数,默认执行的是父类的无参构造,但是也可以在子类的初始化列表中显式的调用父类的有参构造,然后再执行类类型成员的构造函数,最后执行子类的构造函数
Son(int num){} //调用Base的无参构造 Son(int num):Base(val){} //调用Base的有参构造
-
在子类的析构函数执行结束后,再调用类类型成员的析构函数,最后按照继承表的逆序调用父类的析构函数
-
当子类执行拷贝构造时,默认只会调用父类的无参构造,但是这是有问题的,因此需要在子类的拷贝构造函数的初始化列表中显式地调用父类的拷贝构造
Son(const Son& that):Base(that){} //父类引用可以指向子类对象
-
当子类执行赋值函数时,默认下不会调用父类的赋值函数,如果需要调用父类的赋值函数,可以在子类的赋值函数中通过域限定符显式地调用父类的赋值函数
Son& operator=(const Son& that) { Base::operator=(that); // 调用Base的赋值操作函数 }
四、继承方式与访问控制属性
-
访问控制属性对成员的访问范围限制:
访问控制属性 访问范围 public 可以在任意位置访问 protected 只能在类内和子类中访问 private 只能在内类访问 -
继承方式的影响
①父类的成员是否能在直接子类中访问,取决于父类中的访问控制属性,不受子类的继承方式影响
②子类的继承方式能决定父类成员被子类继承后,在子类中变成什么样的访问控制属性,详情见表格
③只有以public方式继承父类,父类的指针或引用才能指向子类对象,这也是多态的基础(如果不是public方式继承父类,那么父类中的成员的访问属性在子类中会改变,具体情况见下表,那么此时父类的指针或者 引用再去指向子类对象时,访问属性就会发生改变,不再是原来的父类)父类中的属性 public继承 protected继承 private继承 public public protected private protected protected protected private private private private private
五、多重继承和钻石(菱形)继承
-
多重继承
多重继承是一个类的父类也有父类,一层一层的继承父类,会按照继承表的顺序在子类中排列父类的内容,当使用父类指针指向子类对象,编译器会自动计算出该父类的内容在子类中的位置,并让父类指针指向该位置,所以可能会出现使用同一个子类指针给不同的父类指针赋值后,地址编号不同的情况 -
钻石(菱形)继承
假设有一个类A,类B和类C都分别继承了类A,又有类D继承了类B、类C,一个类的父类有共同祖先类时,形成了钻石(菱形)继承
问题:1、类B、类C中各自都有类A的内容
2、类D会继承类B、类C的所有内容,就导致类D中继承了两份类A的内容
3、当类D对象去访问类A的内容时,编译器不能确定访问的是哪份类A的内容,有歧义,会报错 -
虚继承
使用virtual关键字去修饰继承表时,此时变成虚继承,此时子类中会多出一个成员变量:虚指针,该指针会指向父类的内容,并且当该子类被继承时该虚指针会一起被继承,如果此时形成钻石继承时,孙子类中就会有多个指向相同位置的虚指针,此时编译器会比较这些虚指针指向的内容是否一致,如果是则只继承一份
总结:通过虚继承可以解决钻石继承中的访问共同祖先类内容会有歧义的问题
六、虚函数、虚函数表、虚表指针、覆盖
-
虚函数
在成员函数前面加 virtual 后,该函数就成为虚函数,此时该类就会像虚继承一样多了一个虚表指针(虚函数表指针、虚指针) -
虚函数表
虚表指针指向的是属于该类一张表格的首地址,该表格中记录了该类中所有虚函数的首地址
如果类中没有其他成员变量,通过 ((void(*)(void))((int)b))(); 可以直接通过虚函数表以及虚表指针来访问虚函数表中第一个虚函数void func(void) -
覆盖(重写)是构成多态的基础
当使用 virtual修饰父类的成员函数时,此时父类中就会多一个虚表指针以及一张虚函数表,子类继承父类时,会把父类的虚表指针以及虚函数表一起继承过来,然后编译器会去比较父子类中同名的虚成员函数的格式,如果格式完全相同的虚函数,就会把子类中虚函数表中原来同名父类 -
构成覆盖的条件
①子类以public方式继承父类
②父类中被覆盖的函数必须是虚函数
③子类中必须有与父类虚函数同名的成员函数且该函数的返回值、参数列表、常属性都必须相同
④返回值类型相同,或者子类同名成员函数的返回值类型可以向父类虚函数的返回值类型做隐式转换,且有继承关系
【常考面试题】
重载、覆盖、隐藏、重写的区别?
隐藏:
-
父子类中,如果同名且格式不同,无论是否有virtual修饰都构成隐藏
-
父子类中,如果同名且格式相同,如果没有virtual修饰则构成隐藏
-
隐藏可以隐藏同名成员变量、成员函数,但是覆盖只能覆盖满足条件的成员虚函数
-
在父子类中,同名成员函数要么构成隐藏、要么构成覆盖
-
除去父子类外,其他的不同作用域下同名标识符也构成隐藏
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!