一、继承的概念

  继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
  这是两个类,你可以看到的是,这两个类有很多冗余的地方,这会造成空间的浪费,所以我们可以考虑将里面相同的内容单独拿出来放到一个类中去,然后两个类再共同继承这个类,达到我们想要的效果。
都通过公有继承就能够继承Person这个类
                                           
且可以调用Person类中的成员函数
二、继承的定义:
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。
以下是我们的继承方式:
继承分为共有继承,私有继承,保护继承。
以下是我们需要掌握的继承规则:
简单来说就是:1.基类的private成员无论什么继承,都不可见。2.基类的protected成员在public和protected继承中是保护成员,在private继承中是private成员。3.基类的public成员在x继承中是x成员。
访问权限对比:基类的private在派生类中是不能被访问的,但是protected能够在派生类中访问,且protected和private成员都无法在类外访问。
三、派生类和基类之间的转换
1、public继承的派⽣类对象可以赋值给基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
我们从一个例子来引入:
              
  我们来分析这张图片,上面我们用Student定义了sobj这个对象,因为子类可以赋值给父类对象,指针和引用,所以我们用Person和Person*以及Person&定义了三个对象,其中pobj仅仅获得了sobj中继承的Person这个类中的切片,而pp也仅仅指向了sobj中Person类中的内容,同理,rp也仅仅引用sobj中Person的内容。
2、基类对象不能赋值给派⽣类对象,否则会编译报错。
3、基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type
Information)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再
单独专⻔讲解,这⾥先提⼀下)
四、继承中的作用域
1、在继承体系中基类和派⽣类都有独⽴的作⽤域。
2、派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
五、派生类的默认成员函数
1、派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
2、派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
3、派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。
4、派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
5、派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
6、派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
7、因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加
virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
六、继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。
基类的友元无法访问派生类的私有和保护成员,但可以访问基类的私有,可以很清楚的看到报错。
六、继承与静态成员
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。

                                             

我们可以清楚的看到,整个继承体系中static成员变量只有一份,无论是用p访问还是s访问,地址都一样,而像_name成员是两个不同的,所以地址也不一样。

七、多继承及其菱形继承问题

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承。
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。(这里体现的是内存布局中的从上到下,地址由低到高)
                                                    
类似于上面这种
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
上面是一个菱形继承的代码
1.菱形继承的缺点很明显,总是会造成数据冗余的情况,最顶层的公共基类总是存在多份,浪费内存,数据不一。
2.访问歧义:在访问顶层基类成员时,需要指明是哪条继承路径,否则会报二义性错误。
                                  
3.顶层的构造函数被多次调用,逻辑容易出错。

虚继承会解决菱形继承造成的数据冗余,二义报错等情况。

                    

此时Student与Teacher共用一个_name,但这是不好的,仍然不太建议。

八、继承和组合

public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤
(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可
⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),
因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关
系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太
那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐