C++继承和多态特性——继承详解(2)

目录

  • 一、派生类和基类的构造析构关系
    • 1、派生类并不继承基类的构造和析构函数,只继承成员变量和普通成员方法
    • 2、派生类的构造函数一定会调用基类的构造函数,析构也一样
    • 3、为什么派生类的构造(析构)必须调用基类的某个构造(析构)
    • 4、其他几个细节
    • 5、派生类做的三件事
  • 二、派生类和基类的同名成员问题
    • 1、派生类中再实现一个基类中的方法会怎样
    • 2、派生类中如何访问被隐藏的基类方法
    • 3、注意和总结
  • 三、子类和父类的类型兼容规则
    • 1、何为类型兼容规则
    • 2、类型兼容规则的常见情况及演示
    • 3、总结
  • 四、继承的优势与不良继承
    • 1、为什么会有继承
    • 2、何为不良继承
    • 3、如何解决不良继承
  • 五、组合介绍以及与继承对比
    • 1、什么是组合
    • 2、继承与组合的特点对比
  • 六、多继承及其二义性问题
    • 1、多继承
    • 2、多继承的二义性问题1
    • 3、多继承的二义性问题2
    • 4、总结
  • 七、虚继承解决菱形继承的二义性问题
    • 1、虚继承怎么用
    • 2、虚继承的实现原理

一、派生类和基类的构造析构关系

1、派生类并不继承基类的构造和析构函数,只继承成员变量和普通成员方法

(1)不继承,意思是派生类中确实没有,不包含基类的构造和析构函数
(2)派生类自己有自己的构造和析构,规则和之前讲过的完全一样
(3)研究构造和析构函数时,一定要注意默认规则

2、派生类的构造函数一定会调用基类的构造函数,析构也一样

(1)代码验证:在基类和派生类中都显式提供“默认构造”并添加打印信息,通过执行结果来验证
C++继承和多态特性——继承详解(2)_第1张图片
  通过代码执行结果看到的现象总结:派生类的构造函数执行之前,会先调用基类的构造函数,然后再调用自己的构造函数。而在派生类的析构函数之后,会先执行自己的析构函数,再执行基类的析构函数。

(2)代码验证:派生类的任意构造函数,可以显式指定调用基类的任意一个构造函数,通过参数匹配的方式(类似于函数重载)
C++继承和多态特性——继承详解(2)_第2张图片
C++继承和多态特性——继承详解(2)_第3张图片

3、为什么派生类的构造(析构)必须调用基类的某个构造(析构)

(1)牢记构造函数的2大作用:初始化成员,分配动态内存

(2)派生类和基类各自有各自的构造函数和析构函数,所以是各自管理各自的成员初始化,各自分配和释放各自所需的动态内存

(3)继承的语言特性,允许派生类调用基类的构造和析构函数,以管理派生类从基类继承而来的那些成员。

(4)明确:派生类的构造和析构处理的永远是派生类自己的对象,只是派生类对象模板中有一部分是从基类继承而来的而已。

4、其他几个细节

(1)派生类构造函数可以直接全部写在派生类声明的class中,也可以只在clas中声明时只写派生类构造函数名和自己的参数列表,不写继承基类的构造函数名和参数列表,而在派生类的cpp文件中再写满整个继承列表,这就是语法要求

(2)派生类析构函数则不用显式调用,直接写即可直接调用基类析构函数。猜测是因为参数列表问题。

(3)构造函数的调用顺序是先基类再派生类,而析构函数是先派生类再基类,遵循栈规则。

(4)派生类的构造函数可以在调用基类构造函数同时,用逗号间隔同时调用初始化式来初始化派生类自己的成员
C++继承和多态特性——继承详解(2)_第4张图片

5、派生类做的三件事

(1)吸收基类成员:除过构造和析构函数以外的所有成员全部吸收进入派生类中

(2)更改继承的成员。1是更改访问控制权限(根据继承类型还有成员在基类中的访问类型决定) 2是同名覆盖(派生类中同名成员覆盖掉基类中)

(3)添加派生类独有的成员。

二、派生类和基类的同名成员问题

1、派生类中再实现一个基类中的方法会怎样

(1)代码实验:派生类和基类中各自实现一个内容不同但函数原型完全相同的方法,会怎么样?

(2)结论:基类对象调用的是基类的方法,派生类对象调用执行的是派生类中重新提供的方法

(3)这种派生类中同名同参方法替代掉基类方法的现象,叫做:重定义(redefining),也有人叫做隐藏

(4)隐藏特性生效时派生类中实际同时存在2份同名同参(但在不同类域名中)的方法,同时都存在,只是一个隐藏了 另一个

2、派生类中如何访问被隐藏的基类方法

(1)派生类对象直接调用时,隐藏规则生效,直接调用的肯定是派生类中重新实现的那一个

(2)将派生类强制类型转换成基类的类型,再去调用则这时编译器认为是基类在调用,则调用的是基类那一个,隐藏规则被绕过了

(3)在派生类内部,使用父类::方法()的方式,可以强制绕过隐藏规则,调用父类实现的那一个
在这里插入图片描述

3、注意和总结

(1)其实不止成员方法,成员变量也遵循隐藏规则。

(2)隐藏规则本质上是大小作用域内同名变量的认领规则问题,实际上2个同名成员都存在当前派生类的对象内存中的

(3)隐藏(重定义,redefining),与重载(overload)、重写(override,又叫覆盖),这三个概念一定要区分清楚。

三、子类和父类的类型兼容规则

1、何为类型兼容规则

(1)C和C++都是强类型语言,任何变量和对象,指针,引用等都有类型,编译器根据类型来确定很多事

(2)派生类是基类的超集,基类有的派生类都有(但构造函数和析构函数例外),派生类有的基类不一定有,所以这2个类型间有关联

(3)派生类对象可以cast(类型转换相关的关键字)后当作基类对象,而基类对象不能放大成派生类对象,否则就可能会出错

(4)考虑到指针和引用与对象指向后,派生类和基类对象的访问规则就是所谓类型兼容规则。

2、类型兼容规则的常见情况及演示

(1)子类对象可以当作父类对象使用,也就是说子类对象可以无条件隐式类型转换为一个父类对象,但是将父类强转成子类是隐藏危险的。

(2)子类对象可以直接初始化或直接赋值给父类对象

(3)父类指针可以直接指向子类对象

(4)父类引用可以直接引用子类对象

下图程序中:person是父类,man是子类
C++继承和多态特性——继承详解(2)_第5张图片

3、总结

(1)派生类对象可以作为基类的对象使用,但是只能使用从基类继承的成员。

(2)类型兼容规则是多态性的重要基础之一。

(3)总结:子类就是特殊的父类 (base *p = &child;)

四、继承的优势与不良继承

1、为什么会有继承

(1)本质上为了代码复用

(2)继承方式很适合用来构建复杂框架体系

(3)用继承来设计类进而构建各层级对象,符合现实中的需要。举例:描述人的种群
C++继承和多态特性——继承详解(2)_第6张图片

2、何为不良继承

(1)鸵鸟不是鸟问题。因为鸵鸟从鸟继承了fly方法但是鸵鸟不会飞

(2)圆不是椭圆问题。因为圆从椭圆继承了长短轴属性然而圆没有长短轴属性

(3)不良继承是天然的,是现实世界和编程的继承特性之间的不完美契合

3、如何解决不良继承

(1)修改继承关系设计。既然圆继承椭圆是一种不良类设计就应该杜绝。去掉继承关系,两个类可以继承自同一个共同的父类,不过该类不能执行不对称的setSize计算,然后在圆和椭圆这2个子类中分别再设计以区分

(2)所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。

五、组合介绍以及与继承对比

1、什么是组合

(1)composition,组合,就是在一个class内使用其他多个class的对象作为成员

(2)用class tree做案例讲解
C++继承和多态特性——继承详解(2)_第7张图片
(3)组合也是一种代码复用方法,本质也是结构体包含

class A
{
}
class B
{
}
class C
{
}

//用组合的方式实现
class D
{
    A a;
    B b;
    C c;
    short s1;
}
//用继承的方式实现
class C:public A, public B, public C 
{
    short s1;
}

2、继承与组合的特点对比

(1)继承是a kind of(is a)关系,具有传递性,不具有对称性。

(2)组合是a part of(has a)的关系,

(3)继承是白盒复用。因为类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的。

(4)继承的白盒复用特点,一定程度上破坏了类的封装特性,因为这会将父类的实现细节暴露给子类

(5)组合属于黑盒复用。被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小

(6)组合中被包含类会随着包含类创建而创建,消亡而消亡。组合属于黑盒复用,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合(例如上边提到的子类对象可作为一个父类对象使用)。而缺点就是致使系统中的对象过多。

(7)OO设计原则是优先组合,而后继承

六、多继承及其二义性问题

1、多继承

(1)多继承就是一个子类有多个父类

class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,{
<派生类类体>
};

(2)多继承演示

#include 
 
using namespace std;
 
// 基类 Shape
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};
 
// 基类 PaintCost
class PaintCost 
{
   public:
      int getCost(int area)
      {
         return area * 70;
      }
};
 
// 派生类
class Rectangle: public Shape, public PaintCost
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};
 
int main(void)
{
   Rectangle Rect;
   int area;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
 
   area = Rect.getArea();
   
   // 输出对象的面积
   cout << "Total area: " << Rect.getArea() << endl;
 
   // 输出总花费
   cout << "Total paint cost: $" << Rect.getCost(area) << endl;
 
   return 0;
}

(3)多继承和单继承的原理,效果并无明显区别

(4)多继承会导致二义性问题

2、多继承的二义性问题1

(1)场景:C多继承自A和B,则C中调用A和B的同名成员时会有二义性

(2)原因:C从A和B各自继承了一个同名(不同namespace域)成员,所以用C的对象来调用时编译器无法确定我们想调用的是哪一个

(3)解决办法1:避免出现,让A和B的public成员命名不要重复冲突。但这个有时不可控。

(4)解决办法2:编码时明确指定要调用哪一个,用c.A::func()明确指定调用的是class A的func而不是class B的

(5)解决办法3:在C中重定义func,则调用时会调用C中的func,A和B中的都被隐藏了

(6)总结:能解决,但是都没有很好的解决。

3、多继承的二义性问题2

(1)场景:菱形继承问题。即A为祖类,B1:A, B2:A, C:B1,B2,此时用C的对象调用A中的某个方法时会有二义性

class A{......};
class B1: public A{......};
class B2: public A{......};
class C: public B1, public B2{.....};

(2)分析:c.func()有二义性,c.A::func()也有二义性,但是c.B1::func()和c.B2::func()却没有二义性

(3)解决办法:和问题1中的一样,但是问题2更隐蔽,也更难以避免

4、总结

(1)二义性就是歧义,好的情况表现为编译错误,不好的情况表现为运行时错误,最惨的情况表现为运行时莫名其妙

(2)随着系统的变大和变复杂,难免出现二义性,这不是程序员用不用心的问题,是系统自身带来的

(3)解决二义性问题不能靠程序员个人的细心和调试能力,而要靠机制,也就是编程语言的更高级语法特性

(4)虚函数、虚继承、纯虚函数、抽象类、override(重写,覆盖)、多态等概念就是干这些事的

(5)感慨:欲戴王冠必承其重,要揽瓷器活就得有金刚钻,C++学得越清楚就越能想象将来用C++去解决的都是些什么层次的问题

七、虚继承解决菱形继承的二义性问题

1、虚继承怎么用

(1)场景:菱形继承导致二义性问题,本质上是在孙子类C中有B1和B2中包含的2份A对象,所以有了二义性。

(2)虚继承解决方案:让A和B虚继承D,C再正常多继承A和B即可,虚继承会使得C继承A、B时只选择AB其中的一份D,不会重复,类似于头文件不重复包含

#include 

using namespace std;

//基类
class D
{
public:
    D(){cout<<"D()"<<endl;}
    ~D(){cout<<"~D()"<<endl;}
protected:
    int d;
};

class B:virtual public D
{
public:
    B(){cout<<"B()"<<endl;}
    ~B(){cout<<"~B()"<<endl;}
protected:
    int b;
};

class A:virtual public D
{
public:
    A(){cout<<"A()"<<endl;}
    ~A(){cout<<"~A()"<<endl;}
protected:
    int a;
};

class C:public B, public A
{
public:
    C(){cout<<"C()"<<endl;}
    ~C(){cout<<"~C()"<<endl;}
protected:
    int c;
};

int main()
{
    cout << "Hello World!" << endl;
    C c;   //D, B, A ,C
    cout<<sizeof(c)<<endl;
    return 0;
}

(3)虚继承就这么简单,就是为了解决菱形继承的二义性问题而生,和虚函数(为了实现多态特性)并没有直接关系

2、虚继承的实现原理

(1)虚继承的原理是:虚基类表指针vbptr 和 虚基类表virtual table

  vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

  菱形继承中,两个虚基类表指针vbptr通过指针偏移量来找寻虚基类表(避免了二义性),但通过两个虚基类表最终找到的是同一个基类。

详解参考:https://blog.csdn.net/xiejingfa/article/details/48028491 

注:本文章参考了《朱老师物联网大讲堂笔记》,并结合了自己的实际开发经历以及网上他人的技术文章,综合整理得到。如有侵权,联系删除!水平有限,欢迎各位在评论区交流。

你可能感兴趣的:(从C高级到征服C++,c++,开发语言,后端,嵌入式,编程语言)