ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++ 核心进阶:继承详解——从基础语法到菱形继承与虚继承

C++ 核心进阶:继承详解——从基础语法到菱形继承与虚继承 文章目录前言一、继承的基本概念二、继承的基本语法三、三种继承方式1. 基类成员本身的访问权限2. 派生类采用的继承方式3.1 public 继承3.2 protected 继承3.3 private 继承3.4 private 成员到底有没有被继承四、class 和 struct 的默认继承方式五、继承类模板六、基类和派生类之间的转换6.1 派生类对象赋值给基类指针6.2 派生类对象赋值给基类引用七、对象切片 / 对象切割八、基类对象能不能赋值给派生类九、继承中的作用域与隐藏十、成员函数的隐藏十一、派生类的默认成员函数十二、派生类构造函数十三、派生类拷贝构造十四、派生类赋值运算符十五、派生类析构函数十六、如何让一个类不能被继承16.1 C98 的方式16.2 C11final十七、继承与友元十八、继承与静态成员十九、单继承与多继承19.1 单继承19.2 多继承二十、菱形继承二十一、菱形继承中的二义性二十二、虚继承二十三、虚继承对象的构造二十四、多继承中的指针偏移二十五、C IO 库中的虚继承二十六、继承和组合26.1 继承is-a26.2 组合has-a二十七、为什么通常优先使用组合前言在 C 面向对象中继承Inheritance是实现代码复用的重要机制之一。继承允许我们在一个已经存在的类的基础上继续扩展新的属性和成员函数从而得到新的类。其中原来的类称为基类 / 父类新得到的类称为派生类 / 子类简单来说函数可以实现函数级别的代码复用而继承则实现了类设计层面的代码复用。例如Student和Teacher都具有姓名、年龄、地址、电话等共同属性。如果分别写在两个类中就会产生大量重复代码。我们可以将这些公共成员抽取到Person类中再让Student和Teacher继承Person。一、继承的基本概念先来看一个简单例子classPerson{public:voididentity(){cout身份认证endl;}protected:string _name;string _address;string _tel;int_age;};classStudent:publicPerson{public:voidstudy(){cout学习endl;}protected:int_stuid;};classTeacher:publicPerson{public:voidteaching(){cout授课endl;}protected:string _title;};此时Student s;Teacher t;s.identity();t.identity();虽然identity()并没有分别定义在Student和Teacher中但由于它们继承了Person因此可以直接使用基类中的成员函数。继承关系可以理解为Person / \ / \ Student Teacher其中Person → 基类 / 父类 Student → 派生类 / 子类 Teacher → 派生类 / 子类二、继承的基本语法继承的基本格式如下class派生类:继承方式 基类{};例如classStudent:publicPerson{};这里Person → 基类 Student → 派生类 public → 继承方式C 中有三种继承方式publicprotectedprivate也就是classStudent:publicPerson{};classStudent:protectedPerson{};classStudent:privatePerson{};三、三种继承方式继承时需要区分两个概念1. 基类成员本身的访问权限publicprotectedprivate2. 派生类采用的继承方式public继承protected继承private继承两者共同决定基类成员继承到派生类后的访问权限。3.1 public 继承classStudent:publicPerson{};规则如下基类成员权限继承到派生类后publicpublicprotectedprotectedprivate不可直接访问可以记成public → public protected → protected private → 不可直接访问3.2 protected 继承classStudent:protectedPerson{};规则如下public → protected protected → protected private → 不可直接访问3.3 private 继承classStudent:privatePerson{};规则如下public → private protected → private private → 不可直接访问3.4 private 成员到底有没有被继承需要特别注意基类中的private成员派生类虽然不能直接访问但它依然存在于派生类对象中的基类部分。也就是说不能访问 ≠ 不存在如果一个成员不希望类外访问 但是希望派生类能够访问那么通常应该定义为protected这也是protected在继承中的重要作用。四、class 和 struct 的默认继承方式如果使用class默认继承方式是private例如classStudent:Person{};等价于classStudent:privatePerson{};而如果使用struct默认继承方式是public例如structStudent:Person{};等价于structStudent:publicPerson{};实际写代码时建议显式写出继承方式。五、继承类模板继承同样可以用于模板类。例如templateclassTclassstack:publicstd::vectorT{public:voidpush(constTx){vectorT::push_back(x);}voidpop(){vectorT::pop_back();}constTtop(){returnvectorT::back();}boolempty(){returnvectorT::empty();}};这里stackT继承了vectorT在模板继承中访问基类成员时经常需要显式指定类域vectorT::push_back(x);而不是直接写push_back(x);因为模板存在按需实例化的特性。六、基类和派生类之间的转换假设classPerson{protected:string _name;string _sex;int_age;};classStudent:publicPerson{public:int_No;};一个Student对象可以理解为Student对象 ┌────────────────┐ │ Person部分 │ │ _name │ │ _sex │ │ _age │ ├────────────────┤ │ Student部分 │ │ _No │ └────────────────┘6.1 派生类对象赋值给基类指针Student s;Person*ps;这里的p实际上指向的是Student对象中的Person部分6.2 派生类对象赋值给基类引用Student s;Personrefs;同样ref绑定的是Student对象中的Person部分因此Person*或者Person既可能指向真正的Person对象也可能指向某个派生类对象中的基类部分。七、对象切片 / 对象切割还可以这样写Student s;Person ps;这种操作也是允许的。但是这里只会保留Student对象中的Person部分Student 自己扩展的成员会被舍弃。例如Student _name _sex _age _No赋值给Person后Person _name _sex _age其中_No没有了。这种现象叫做对象切片 / 对象切割Object Slicing因此Person ps;本质上只复制了派生类中的基类部分。八、基类对象能不能赋值给派生类下面这种写法是不允许的Person p;Student s;sp;原因很简单Person对象中只有Person部分而Student对象除了Person部分还有Student自己的成员所以仅仅一个基类对象无法完整构造出一个派生类对象。九、继承中的作用域与隐藏基类和派生类拥有独立的作用域。例如classPerson{protected:int_num111;};classStudent:publicPerson{protected:int_num999;};这里Person::_num和Student::_num是两个不同的成员。派生类中的_num会把基类中的同名_num隐藏起来。这种现象叫做隐藏如果想访问基类中的成员可以显式指定作用域Person::_num例如voidPrint(){cout_numendl;coutPerson::_numendl;}十、成员函数的隐藏再看一个例子classA{public:voidfun(){coutA::fun()endl;}};classB:publicA{public:voidfun(intx){coutB::fun(int)endl;}};这里A::fun()和B::fun(int)并不构成函数重载。而是隐藏关系原因是基类和派生类属于不同作用域。所以B b;b.fun(10);// 正常b.fun();// 编译错误如果想调用基类中的版本b.A::fun();需要记住在继承体系中只要函数名相同就会发生隐藏与参数列表是否相同没有关系。十一、派生类的默认成员函数继承体系中派生类的默认成员函数需要额外处理基类部分。主要包括构造函数 拷贝构造函数 operator 析构函数基本原则基类部分 → 交给基类自己处理 派生类部分 → 派生类自己处理十二、派生类构造函数例如classPerson{public:Person(constchar*name):_name(name){}protected:string _name;};派生类classStudent:publicPerson{public:Student(constchar*name,intnum):Person(name),_num(num){}protected:int_num;};其中Person(name)负责初始化Student对象中的Person部分而_num(num)负责初始化Student自己的成员对象的构造顺序是先构造基类 ↓ 再构造派生类也就是Person() ↓ Student()十三、派生类拷贝构造例如Student(constStudents):Person(s),_num(s._num){}其中Person(s)负责拷贝Student对象中的Person部分而_num(s._num)负责拷贝Student自己的成员所以派生类拷贝构造 基类拷贝构造 派生类成员拷贝十四、派生类赋值运算符例如Studentoperator(constStudents){if(this!s){Person::operator(s);_nums._num;}return*this;}为什么必须写Person::operator(s);因为Student::operator会隐藏Person::operator所以调用基类版本时必须显式指定作用域。十五、派生类析构函数构造顺序基类 ↓ 派生类析构顺序正好相反派生类 ↓ 基类例如构造 Person() Student() 析构 ~Student() ~Person()可以类比成盖楼构造 先打地基 ↓ 再盖上层析构先拆上层 ↓ 再拆地基十六、如何让一个类不能被继承16.1 C98 的方式可以把基类构造函数设置为private因为派生类构造时必须调用基类构造函数。如果基类构造函数是私有的派生类就无法正常构造。16.2 C11finalC11 提供了更加直接的方式classBasefinal{};此时classDerive:publicBase{};会直接编译失败。所以现代 C 中如果不希望某个类被继承可以直接使用final十七、继承与友元需要记住友元关系不能继承。例如classPerson{friendvoidDisplay();protected:string _name;};即使classStudent:publicPerson{};Display也不会自动成为Student的友元。也就是说Person的友元 ≠ Student的友元如果Display需要访问Student的private或protected成员那么Student也需要单独声明它为友元。十八、继承与静态成员如果基类中存在静态成员classPerson{public:staticint_count;};派生类classStudent:publicPerson{};那么Person::_count和Student::_count使用的是同一份静态成员。也就是说整个继承体系中静态成员只有一份。例如Person p;Student s;coutp._countendl;couts._countendl;打印出来的地址是一样的。但是普通成员_name每个对象都会有自己独立的一份。十九、单继承与多继承19.1 单继承如果一个派生类只有一个直接基类A ↓ B例如classStudent:publicPerson{};这种关系叫单继承19.2 多继承如果一个派生类同时拥有多个直接基类classAssistant:publicStudent,publicTeacher{};则称为多继承关系如下Student Teacher \ / \ / Assistant多继承对象中的成员通常按照先继承的基类部分 ↓ 后继承的基类部分 ↓ 派生类自己的成员进行布局。二十、菱形继承多继承会带来一个经典问题菱形继承例如Person / \ / \ Student Teacher \ / \ / Assistant对应代码classPerson{public:string _name;};classStudent:publicPerson{};classTeacher:publicPerson{};classAssistant:publicStudent,publicTeacher{};此时Assistant中会存在两份Person成员。也就是Assistant Student部分 Person::_name Teacher部分 Person::_name因此会产生两个问题1. 数据冗余 2. 二义性二十一、菱形继承中的二义性例如Assistant a;a._namePeter;这里会编译错误。因为编译器不知道你访问的是Student中的Person::_name还是Teacher中的Person::_name可以显式指定a.Student::_nameAAA;a.Teacher::_nameBBB;这样可以解决访问二义性但是数据冗余依然存在。二十二、虚继承为了解决菱形继承中的数据冗余 二义性C 提供了虚继承写法如下classStudent:virtualpublicPerson{};classTeacher:virtualpublicPerson{};classAssistant:publicStudent,publicTeacher{};关键是virtualpublicPerson此时Assistant对象中只会保留一份Person于是Assistant a;a._namePeter;就不会再出现原来的二义性问题。二十三、虚继承对象的构造虚继承中还有一个需要注意的地方。例如classPerson{public:Person(constchar*name):_name(name){}string _name;};中间层classStudent:virtualpublicPerson{public:Student(constchar*name,intnum):Person(name),_num(num){}protected:int_num;};classTeacher:virtualpublicPerson{public:Teacher(constchar*name,intid):Person(name),_id(id){}protected:int_id;};最终派生类classAssistant:publicStudent,publicTeacher{public:Assistant(constchar*name1,constchar*name2,constchar*name3):Person(name3),Student(name1,1),Teacher(name2,2){}};由于Person是虚基类最终由最派生类Assistant负责初始化那一份唯一的Person。所以Assistanta(张三,李四,王五);真正决定Person::_name初始化结果的是Person(name3)也就是王五二十四、多继承中的指针偏移例如classBase1{public:int_b1;};classBase2{public:int_b2;};classDerive:publicBase1,publicBase2{public:int_d;};创建对象Derive d;可以简单理解为Derive对象 低地址 ┌────────────┐ │ Base1::_b1 │ ├────────────┤ │ Base2::_b2 │ ├────────────┤ │ Derive::_d │ └────────────┘ 高地址然后Base1*p1d;Base2*p2d;Derive*p3d;其中p1 → Base1部分 p2 → Base2部分 p3 → 整个Derive对象因此派生类指针转换为不同基类指针时地址可能发生偏移。二十五、C IO 库中的虚继承C 标准 IO 库中就使用了虚继承。例如basic_ostream和basic_istream都会虚继承basic_ios大致关系如下ios_base | basic_ios / \ / \ basic_istream basic_ostream \ / \ / iostream简化代码templateclassCharT,classTraitsclassbasic_ostream:virtualpublicbasic_iosCharT,Traits{};templateclassCharT,classTraitsclassbasic_istream:virtualpublicbasic_iosCharT,Traits{};二十六、继承和组合学习完继承后还需要解决一个设计问题一个类想复用另一个类应该继承还是组合核心区别是继承is-a 组合has-a26.1 继承is-a例如classBMW:publicCar{};BMW 是一种 CarBMW is a Car所以这里适合使用继承。26.2 组合has-a例如classTire{};classCar{private:Tire _t1;Tire _t2;Tire _t3;Tire _t4;};这里Car has Tire一辆车有轮胎。因此Car和Tire之间更适合使用组合。二十七、为什么通常优先使用组合继承中基类和派生类联系非常紧密如果基类实现发生变化可能会影响大量派生类。因此继承耦合度较高而组合一般只通过对象提供的接口进行使用classA{};classB{A a;};这样B不需要过度了解A的内部实现所以组合耦合度较低因此工程中经常有一个设计原则优先使用组合而不是继承。当然这不是绝对的。如果类之间就是典型的is-a那么使用继承是合理的。如果继承和组合都可以通常优先考虑组合
返回列表