【C++进阶】C++ 继承家族史,也就那么回事儿

📅 2026/8/7 18:24:02
【C++进阶】C++ 继承家族史,也就那么回事儿
目录1、继承1.1 继承的概念1.2 继承的定义2、父类和子类对象赋值兼容转换3、继承中的作用域4、子类的默认成员函数4.1 四个常见默认成员函数4.2 实现一个不能被继承的类5、继承与友元6、继承与静态成员派生类都继承了基类哪些东西哪些没继承会被继承但有访问限制不会被继承7、多继承及菱形继承问题7.1 继承模型7.2 菱形继承解决方法7.2.1 虚继承7.2.2 接口继承 组合实现7.3 多继承中指针偏移问题8、继承和组合1、继承1.1 继承的概念继承机制是面向对象程序设计使代码可以复用的最重要的手段它允许我们在保持原有类特性的基础上进行扩展增加方法(成员函数)和属性(成员变量)这样产生新的类称子类。继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用继承是类设计层次的复用。classStudent{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证voididentity(){// ...}// 学习voidstudy(){// ...}protected:string _namepeter;// 姓名string _address;// 地址string _tel;// 电话int_age18;// 年龄int_stuid;// 学号};classTeacher{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证voididentity(){// ...}// 授课voidteaching(){//...}protected:string _name张三;// 姓名int_age18;// 年龄string _address;// 地址string _tel;// 电话string _title;// 职称};上面的Student和Teacher中成员函数和成员变量有相同的地方下面我们将公共的部分放到Person中然后让Student和Teacher都继承Person就可以复用这些成员使代码不再冗杂。classPerson{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证voididentity(){// ...}protected:string _namepeter;// 姓名string _address;// 地址string _tel;// 电话int_age18;// 年龄};classStudent:publicPerson{public:// 学习voidstudy(){// ...}protected:int_stuid;// 学号};classTeacher:publicPerson{public:// 授课voidteaching(){//...}protected:string _title;// 职称};1.2 继承的定义Person是父类也称基类Student是子类也称派生类。类成员 / 继承方式public继承protected继承private继承父类的public成员子类的public成员子类的protected成员子类的private成员父类的protected成员子类的protected成员子类的protected成员子类的private成员父类的private成员子类中不可见子类中不可见子类中不可见父类private成员在子类中无论以什么方式继承都是不可见的。不可见是指父类的私有成员还是被继承到了子类中但是语法上限制子类对象不管在类里面还是类外面都不能去访问它父类private成员在子类中是不能被访问的如果父类成员不想在类外直接被访问但需要在子类中能访问就定义为protected。可以看出保护成员限定符是因继承才出现的实际上面的表格我们进行总结就会发现父类的私有成员在子类都是不可见的而父类的其他成员在子类中的访问方式 Min(成员在父类的访问限定符继承方式)public protected private使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public不过最好显示的写出继承方式在实际运用中一般使用都是public继承几乎很少使用protetced/private继承也不提倡使用protetced/private继承因为protetced/private继承下来的成员都只能在子类的类里面使用实际中扩展维护性不强2、父类和子类对象赋值兼容转换public继承的子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用。有个形象的说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去父类对象不能赋值给子类对象父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的classPerson{protected:string _name;string _sex;size_t _age;};classStudent:publicPerson{public:int_no;//学号};intmain(){Student s;Person ps;Person*pps;Personrps;return0;}3、继承中的作用域在继承体系中父类和子类都有独立的作用域子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问这种情况叫隐藏在子类成员函数中可以使用 父类::父类成员 显示访问如果是成员函数只要是函数名相同就构成隐藏在实际的继承体系中最好不要定义同名成员classA{public:voidfun(){coutfunc()endl;}};classB:publicA{public:voidfun(inti){coutfunc(int i)iendl;}};intmain(){B b;b.fun(10);b.fun();return0;};A类和B类中的两个fun函数构成的是隐藏关系不是重载上面的程序编译运行的结果是编译报错因为A类中的fun函数被隐藏B类中的fun函数形参没有缺省值所以主函数中的代码b.fun()会报错如果想要调用A类中的fun函数需要指定作用域b.A::fun();4、子类的默认成员函数4.1 四个常见默认成员函数子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数则必须在子类构造函数的初始化列表阶段显示调用子类的拷贝构造函数必须调用父类的拷贝构造函数完成父类的拷贝初始化。一般来说子类的拷贝构造函数默认生成的就够用了如果有需要深拷贝的资源才需要我们自己实现子类的赋值重载必须调用父类的赋值重载完成父类的赋值规定子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。这样做是为了保证子类对象先清理子类成员再清理父类成员的顺序子类对象初始化先调用父类构造再调用子类构造子类对象析构清理先调用子类析构再调用父类析构因为多态中一些场景析构函数需要构成重写重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理处理成destructor()所以父类析构函数不加virtual的情况下子类析构函数和父类析构函数构成隐藏关系#includeiostreamusingnamespacestd;classPerson{public:Person(constchar*namepeter):_name(name){coutPerson()endl;}Person(constPersonp):_name(p._name){coutPerson(const Person p)endl;}Personoperator(constPersonp){coutPerson operator(const Person p)endl;if(this!p)_namep._name;return*this;}~Person(){cout~Person()endl;}protected:string _name;// 姓名};classStudent:publicPerson{public:Student(constchar*name,intnum):Person(name),_num(num){coutStudent()endl;}Student(constStudents):Person(s),_num(s._num){coutStudent(const Student s)endl;}Studentoperator(constStudents){coutStudent operator(const Student s)endl;if(this!s){// 构成隐藏所以需要显示调用Person::operator(s);_nums._num;}return*this;}~Student(){//子类的析构和父类的析构也构成隐藏关系如果调用需要指定类域//但是子类析构后会自动调用父类析构所以不需要显示调用//Person::~Person();cout~Student()endl;}protected:int_num;//学号};intmain(){Students1(jack,18);Students2(s1);Students3(rose,17);s1s3;return0;}4.2 实现一个不能被继承的类父类的构造函数私有子类的构成必须调用父类的构造函数但是父类的构成函数私有化以后子类看不见就不能调用了那么子类就无法实例化出对象C11新增了一个final关键字final修饰父类子类就不能继承了// C11的⽅法classBasefinal{public:voidfunc5(){coutBase::func5endl;}protected:inta1;private:// C98的⽅法/*Base() {}*/};classDerive:publicBase{voidfunc4(){coutDerive::func4endl;}protected:intb2;};intmain(){Base b;Derive d;return0;}5、继承与友元友元关系不能被继承也就是说父类友元不能访问子类私有和保护成员//前置声明classStudent;classPerson{public:friendvoidDisplay(constPersonp,constStudents);protected:string _name;// 姓名};classStudent:publicPerson{protected:int_stuNum;// 学号};voidDisplay(constPersonp,constStudents){coutp._nameendl;couts._stuNumendl;}intmain(){Person p;Student s;// 编译报错error C2248: “Student::_stuNum”: ⽆法访问 protected 成员// 解决⽅案Display也变成Student 的友元即可Display(p,s);return0;}6、继承与静态成员父类定义了静态成员则整个继承体系里面只有一个这样的成员无论派生出多少个子类都只有一个静态成员实例化classPerson{public:string _name;staticint_count;};intPerson::_count0;classStudent:publicPerson{protected:int_stuNum;};intmain(){Person p;Student s;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的// 说明⼦类继承下来了⽗⼦类对象各有⼀份coutp._nameendl;couts._nameendl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明⼦类和⽗类共⽤同⼀份静态成员coutp._countendl;couts._countendl;// 公有的情况下⽗⼦类指定类域都可以访问静态成员coutPerson::_countendl;coutStudent::_countendl;return0;}派生类都继承了基类哪些东西哪些没继承会被继承但有访问限制成员变量public / protected / private全部继承占用派生类对象内存。public派生类内部、外部均可直接访问protected仅派生类内部、友元可访问private派生类内部不能直接访问但是内存里依然存在普通成员函数非构造、非析构全部继承。同样受访问控制限定能不能调用。静态成员不属于某个对象属于类本身只有一份全局实例无论派生类创建多少对象、多少层派生基类 static 变量唯一一份。不会被继承构造函数包括拷贝构造、移动构造派生类不会继承基类构造函数。派生类构造时自动调用基类构造函数初始化基类部分但派生类不能直接使用基类构造函数C11using Base::Base;是继承构造函数的语法糖不是原生意义上继承。既然基类构造函数会被自动调用为什么还需要using Base::Base;派生类本来确实会自动调用基类构造但默认只能调用【基类无参构造】如果想直接调用【基类带参构造】没有 using Base::Base 的话你必须手动挨个写转发构造函数。classBase{public:Base(intx){}Base(doubled){}Base(conststd::strings){}};classDerived:publicBase{public:usingBase::Base;// 编译器自动生成// Derived(int x) : Base(x) {}// Derived(double d) : Base(d) {}// Derived(const string s) : Base(s) {}};析构函数不会被继承。派生类析构执行完后编译器自动调用基类析构如果基类析构不是virtual多态场景会产生内存泄漏。友元函数 / 友元类友元关系不能继承也不能传递。基类的友元无权访问派生类新增成员派生类不会自动获得基类友元权限。operator赋值运算符默认不继承。派生类默认合成拷贝赋值只会处理派生自己的成员不会自动调用基类赋值如需完整赋值要手动调用Base::operator。7、多继承及菱形继承问题7.1 继承模型单继承一个子类只有一个直接父类时称这个继承关系为单继承多继承一个子类有两个或以上直接父类时称这个继承关系为多继承多继承对象在内存中的模型是先继承的父类在前面后面继承的父类在后面子类成员在放到最后面。菱形继承菱形继承是多继承的一种特殊情况。菱形继承有数据冗余和二义性的问题支持多继承就一定会有菱形继承像Java就直接不支持多继承规避掉了这里的问题实践中不建议设计出菱形继承这样的模型单继承多继承菱形继承classPerson{public:string _name;// 姓名};classStudent:publicPerson{protected:int_num;//学号};classTeacher:publicPerson{protected:int_id;// 职⼯编号};classAssistant:publicStudent,publicTeacher{protected:string _majorCourse;// 主修课程};intmain(){// 编译报错error C2385: 对“_name”的访问不明确Assistant a;a._namepeter;// 需要显⽰指定访问哪个⽗类的成员可以解决⼆义性问题但是数据冗余问题⽆法解决a.Student::_namexxx;a.Teacher::_nameyyy;return0;}7.2 菱形继承解决方法7.2.1 虚继承在中间层继承时增加 virtualclassBase{};classA:virtualpublicBase{};classB:virtualpublicBase{};classC:publicA,publicB{};核心变化C对象只存在唯一一份Base虚基类子对象由最底层派生类C负责构造虚基类BaseA、B不再各自持有独立Base而是通过虚基指针间接指向共享的Base。┌─────────────────────────────────────────────┐ │ C 对象内存布局 │ ├─────────────────────────────────────────────┤ │ BaseA继承来的副本 │ ├─────────────────────────────────────────────┤ │ A自有成员 │ ├─────────────────────────────────────────────┤ │ BaseB继承来的副本 │ ├─────────────────────────────────────────────┤ │ B自有成员 │ ├─────────────────────────────────────────────┤ │ C自有成员 │ └─────────────────────────────────────────────┘ 特点存在两份独立Base实体数据冗余直接访问base成员产生二义性┌─────────────────────────────────────────────┐ │ C 对象内存布局 │ ├─────────────────────────────────────────────┤ │ vbptr_A (虚基类指针) │ ├─────────────────────────────────────────────┤ │ A自有成员 │ ├─────────────────────────────────────────────┤ │ vbptr_B (虚基类指针) │ ├─────────────────────────────────────────────┤ │ B自有成员 │ ├─────────────────────────────────────────────┤ │ C自有成员 │ ├─────────────────────────────────────────────┤ │ 【唯一共享 Base】 │ └─────────────────────────────────────────────┘ 特点只有一份BaseA/B不再内嵌Base通过vbptr查表得到Base偏移地址Q虚继承代价是什么A引入虚基指针、虚基表间接寻址带来少量运行时开销内存布局变复杂。Q虚继承和虚函数的虚表是一回事吗A不是虚函数 → 虚函数表 vtable、虚指针vptr用于多态虚继承 → 虚基表用来定位共享虚基类子对象两套独立机制。Q什么时候不要用虚继承没有菱形结构不要随便加virtual无端增加开销只有出现钻石继承场景才考虑。Qprivate virtual继承合法吗语法合法只是访问权限受到限制。7.2.2 接口继承 组合实现工程上规避菱形继承的主流设计手段接口继承实现使用组合杜绝实现类参与菱形继承。继承只用来继承接口is-a复用实现逻辑优先用组合has-a不要用继承复用实现。// 纯接口structIBase{virtualvoidfunc()0;};// 实现类不参与对外继承structBaseImpl{voidfunc_impl(){// 真实业务逻辑}};// A对外继承接口内部组合持有实现structA:publicIBase{BaseImpl impl;voidfunc()override{impl.func_impl();}};// B同理structB:publicIBase{BaseImpl impl;voidfunc()override{impl.func_impl();}};structC:publicA,publicB{};IBase只是接口真正实现放在BaseImpl由A、B各自组合持有如果想要全局一份实现可以把Impl做成单例或外部传入A、B共享同一个Impl实例从根源上避免让携带数据的实现类进入菱形继承链路自然不需要虚继承。和虚继承的定位区别virtual虚继承是语言层面补救措施用来解决不得已出现菱形继承时的数据冗余问题属于“语法补丁”。接口组合的方式是面向对象设计层面的方案从设计上杜绝产生带数据的菱形继承属于最优工程实践。C开发规范普遍提倡优先用组合复用代码尽量不要依靠继承复用实现。抽象基类就不会有重复继承成员问题吗如果抽象类含有成员变量哪怕带纯虚函数普通菱形继承依然产生多份子对象只有不含任何成员变量的纯抽象接口类才没有重复数据负担但依然存在函数访问歧义。Q什么时候用虚继承什么时候用接口组合A如果历史代码无法改动已经形成菱形继承只能用virtual虚继承兜底新项目优先采用接口继承组合实现从源头避免菱形继承。Q纯虚类抽象类能不能有成员变量A语法允许。只要含有成员变量菱形继承下依旧会产生多份副本。Q多重继承多个无关接口会不会有菱形问题多个独立无继承关系的接口多重继承一般没问题只有多个父类又共同继承同一个基类才形成菱形结构。7.3 多继承中指针偏移问题classBase1{public:int_b1;};classBase2{public:int_b2;};classDerive:publicBase1,publicBase2{public:int_d;};intmain(){Derive d;Base1*p1d;Base2*p2d;Derive*p3d;return0;}先继承的在前面所以三个指针的关系是p1 p3 ! p28、继承和组合public继承是一种is-a的关系也就是说每个子类对象都是一个父类对象组合是一种has-a的关系如果B组合了A则每个B对象中都有一个A对象继承允许我们根据父类的实现来定义子类的实现这种通过生成子类的复用通常被称为 “白箱复用” 。白箱是相对可视性而言在继承方式中父类的内部细节对子类可见所以继承一定程度上破坏了父类的封装父类的改变对子类有很大的影响子类和父类之间的依赖关系很强耦合度高对象组合是类继承之外的另一种复用选择新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口这种复用风格被称为 “黑箱复用” 因为对象的内部细节是不可见的对象只以黑箱的形式出现。组合类之间没有很强的依赖关系耦合度低优先使用组合而不是继承。实际尽量多去用组合组合的耦合度低代码维护性好。不过也不太那么绝对类之间的关系就适合继承(is-a)那就用继承另外要实现多态也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a)就用组合// Tire(轮胎)和Car(⻋)更符合has-a的关系classTire{protected:string _brandMichelin;// 品牌size_t _size17;// 尺⼨};classCar{protected:string _colour⽩⾊;// 颜⾊string _num陕ABIT00;// ⻋牌号Tire _t1;// 轮胎Tire _t2;// 轮胎Tire _t3;// 轮胎Tire _t4;// 轮胎};classBMW:publicCar{public:voidDrive(){cout好开-操控endl;}};// Car和BMW/Benz更符合is-a的关系classBenz:publicCar{public:voidDrive(){cout好坐-舒适endl;}};// stack和vector的关系既符合is-a也符合has-atemplateclassTclassvector{};templateclassTclassstack:publicvectorT{};templateclassTclassstack{public:vectorT_v;};intmain(){return0;}