C++之继承最详讲

1.继承的概念

  • 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
  • 讲的通俗一点就是说,我们实际应用中许多不同的对象有着共同的特征,例如动物中狗,猫,猪他们都有年龄,体重,肤色,但是这些对象又同时有着自己的独特的特征,例如狗忠诚,猫高傲,猪可爱。那么当我们用计算机的语言去描述他们的时候很多的特征就会重复,如果我们一类动物写一个代码那么代码的复用性无疑是很差的,这里我们可以用类继承的特性来继承那些类中公共的部分。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。对于这句话举个例子来说就是说,例如动物类中有狗,狗中又有二哈这种狗,这种狗又有着自己的特点,我们对于这种对某一种特定类型的狗的描述的认知从动物到狗类,再到这种的狗类的具体特征,然后具体到一个真正的个体这一过程就体现了我们对一个个体又简单到复杂的认知过程。

2.继承的定义

  • class 子类:继承权限 父类

3.继承权限

  • 3.1、子类以public方式继承父类

  • 总结:
    • 1.public继承方式下基类中public和protected修饰的成员在子类中的权限没有发生变化
    • 2.基类中private修饰的成员在子类中不能直接被访问----不可见
    • 3.那么我们在设计类的时候访问权限的设置就可以用如下的考虑方式:
    • 4.成员如果想要在类外被访问,将其设置为public
    • 5.如果成员不想在类外直接被访问,但是要在子类中直接被访问,可以将其设置为protected
    • 6.如果成员不想在类外直接被访问,也不想在子类中被直接访问,可以将其设置为private
  • 3.2、子类以protect方式继承

  • 总结:
    • protected继承方式下:
    • 1.基类中public修饰的成员变量在子类中的权限为protected
    • 2.基类中protected修饰的成员变量在子类中的权限为protected,也就是说权限没有变化
    • 3.基类中private修饰的成员变量在子类中不可见,及不能被直接访问。
  • 3.3、子类以private方式继承

  • 总结:
    • 1.private继承方式:父类中public修饰的成员在子类的访问权限是
    • 2.private可以在子类中访问子类中访问,但是它的子类不可以直接访问
    • 3.private继承方式:父类中protected修饰的成员在子类的访问权限是
    • 4.private可以在子类中访问子类中访问,但是它的子类不可以直接访问
    • 5.父类中private成员变量在子类中不可以被访问
  • 3.4、class关键字没有给出继承方式默认的继承方式是私有的继承方式

  • 3.5、struct关键字没有给出继承方式默认的继承方式是公共的继承方式

  • 3.6、那么我们可以就此总结一下class和struct的区别

    • 1.struct和class都可以定义类,但是struct成员的默认权限是共有的为了兼容C语言,二class的默认权限是私有的
    • 2.模板参数列表中只能用class来定义模板参数例如template<class T>而不可以用struct来定义
    • 3.在继承中class默认的继承方式是private而struct默认的继承方式是public
  • 3.7、关于继承方式大总结

    • 1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
    • 2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
    • 3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
    • 4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
    • 5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

4.基类和派生类对象赋值转换

  • 4.1、派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用

  • 这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。基类对象不能赋值给派生类对象

  • 4.2、可以使用基类在指针指向子类的对象,但是不可以反过来(不能直接使用子类的指针指向基类对象)如果一定要指向必须进行强转

  • 4.3、可以使用基类的应用去应用子类的对象,但是反过来不可

  • 这里我们可以理解为:从public继承方式来看:可以将子类看作是一个基类对象,例如狗一定是个动物,但是动物不一定是狗
  • 从对象模型上来看:对象中成员变量在内存中的布局形式将其称为对象模型。

  • 赋值理解了那我们就可以理解为什么指针不可以用基类的指针指向子类而子类的指针可以指向基类了,当基类的指针指向子类时,用基类的指针访问基类中的成员函数和对象都是可以访问的,因为子类对象中都有这些东西,但是如果可以用子类的指针指向基类的对象的活,那么要想用子类的指针访问子类中特有的成员,但是此时指针指向的是一个基类的对象,那么此时因为基类中没有子类中特有的成员,此时就可能导致程序崩溃。那么引用也是同样的道理。

5.继承中的作用域:

  • 5.1、在继承体系中基类和派生类都有独立的作用域

  • 在继承体系中基类和派生类都有独立的作用域。例如我们在基类中定义一个函数,在子类中定义一个相同名字的函数但是参数不同,这里这两个函数是不构成函数重载的,因为函数重载两个函数必须在同一个作用域当中。
  • 5.2、隐藏(重定义) 

  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问(也就是说子类对象访问同名的成员变量时只能访问到自己的),这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用基类::基类成员显示访问)。
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。也就是说如果成员函数同名和函数原型是否相同无关,子类对象直接访问自己的同名成员函数。基类的同名成员函数无法通过子类对象直接访问。因为子类中存在和基类中一样名称的成员函数因此通过子类对象直接访问基类同名成员函数时,编译器会禁止,如果就想要访问,可以在成员函数强加基类的作用域限定符。

  • 注意在实际中在继承体系里面最好不要定义同名的成员。

6.派生类的默认成员函数

  • 6.1、构造函数

  • 1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。(这是因为我们子类对象在构造时分两步去构造,第一步将从基类继承下来的成员初始化完成----需要调用基类的构造函数,第二步再将自己新增的成员初始化完成。而所有的初始化都是在初始化列表位置完成的,那么我们就需要在子类的初始化列表中调用基类的构造函数,而当我们不定义基类的构造函数时,会生成默认的构造函数,子类在调用时会自动调用默认的构造函数,但是当我们定义了构造函数之后就需要我们手动调用,因为编译器只会调用无参的构造函数)
    • 1.如果基类没有显示定义任何构造函数,则子类可以定义也可以不用定义

    • 注意不可以在子类的初始化列表中,初始化基类中的成员对象

    • 2.如果此时基类定义了构造函数,但是基类的构造函数时无参的或者是全缺省的构造函数此时子类的构造函数可以定义也可以不用定义。
    • 3.如果基类显示定义了构造函数,但是基类的构造函数不是无参的或者全缺省的构造函数,此时子类必须要定义自己的构造函数,并且需要在其子类的构造函数初始化列表位置显示调用基类的构造函数,目的就是为了完成从基类中继承下来的成员的初始化。

    • 当然我们如果在基类中将无参的构造函数也定义出来,那么在子类中不调用基类的构造函数也是可以的,这时子类会调用基类的无参构造函数。
  • 6.2、拷贝构造

  •  派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 6.3、operator=重载

  • 派生类的operator=必须要调用基类的operator=完成基类的复制。
  • 6.4、析构函数

  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 6.5、构造和析构函数的调用顺序

  • 6.5.1、派生类对象初始化先调用基类构造再调派生类构造。//这里在描述时候应该说明,创建那个类的对象编译器就要调用那个类的构造函数,是先执行基类对象的构造函数因为在执行基类的函数体时候,执行它的初始化列表时,会调用基类的析构函数,然后再执行子类对象的构造函数。
  • 6.5.2、派生类对象析构清理先调用派生类析构再调基类的析构。//这里完整的原因是,因为在析构子类的对象时调用子类的析构函数,当执行完子类的析构函数之后在子类的析构函数的最后编译器会添加一条汇编指令去调用基类的析构函数。

7.继承与友元

  • 基类当中的友元函数,友元类是不可以被子类继承的,也就是说例如友元函数可以访问基类当中的成员变量(这里的访问可以通过基类的对象访问基类的成员变量,也可以通过子类的对象访问基类的成员变量,但是不可以通过子类的对象访问子类的成员变量),但是不可以访问子类当中的成员变量。

8.C++中不同的继承体系

  • 子类对象模型

    • 8.1、单继承

    • 一个子类只有一个父类。(基类部分在低地址而子类部分在高地址)
    • 8.2、多继承

    • 一个子类有多个父类。(观察下图我们看到基类部分在也在低地址并且基类部分在内存中的分配与继承列表中的基类的出现次序一致。先出现的在低地址,后出现的在高地址。)

    • 注意在多继承当中每个基类前必须要加访问权限,否则其访问权限默认位private。例如这里,A1前面没有加访问权限,那么他默认的访问权限就是private我们在类中无法使用其public成员。

    • 在前面加上继承权限即可

    • 8.3、菱形继承:单继承+多继承

    • 8.4、菱形继承中存在的问题

    • 8.4.1、二义性

      • 最顶层的基类B中成员在最下面的子类D中的存在两份,一份从C1中继承一份从C2中继承就导致D类将最顶层B中的成员出现了两份,如果直接通过D的对象去访问最顶层基类中的成员时,会出现访问不明确,即二义性。即会出现入下的错误:

    • 8.4.2、菱形继承的二义性的解决方式:

      • 方式一:让访问明确化

      • (无法根本解决问题,仍然存在代码冗余问题,浪费空间,而且仍然存在二义性问题。只是让编译器知道访问那个基类中的成员)
      • 这里如果作用域限定符加的是B他仍然会提示报错:说基类B不明确

      • 但是这里可以运行成功我们可以看到他默认访问的是D类继承表的第一个也就是C1类中的"_b"
      • 我们可以将D类中继承的值都赋一个值从而验证我们上面的菱形继承模型:

      • 方式二:采用菱形虚拟继承

      • 采用菱形虚拟继承达到根本解决问题:让最顶层基类中成员在D中只存在一份
      • 1.什么是虚拟继承

      • 我们先来实现一个简单的虚拟继承:

      • 2.我们观察上图也可以看出虚拟继承和普通继承的区别

      • 2.1.对象模型是倒立的
      • 2.2.对象多了四个字节
      • 2.3.如果我们的D中没有显示定义构造函数,则编译器一定会生成,如果 定义了构造函数,则编译器一定会对构造函数进行修改,修改的目的:往对象的前四个字节中填充数据。

      • 2.4.这里为什么会多出四个字节呢?我们来转到汇编一探究竟:

      • 第一条指令的作用:取对象前四个字节当中的内容,当作地址使用

      • 第二条指令的作用:取刚刚取出来的地址空间中往后偏移4字节之后的内容---->这里将取出来的内容当作偏移量使用

      • 第三条指令:取d的地址结合刚刚的偏移量给_d赋值

      • 这里我们看看d对象的前四个字节的指针指向的地址中存放的是什么内容:

      • 3.知道了虚拟继承原理之后来看一下菱形继承的解决

      • 这里我们将C1和C2的继承方式都选为虚拟继承,这里我们可以大致推断出类的模型,如下。大小为24个字节。基类B不能放在C1中也不能放在C2中,因为如果放在C1中而不放在C2中就说不过去了,凭什么你C1中有,我C2中没有。所以我们推断只能放在D中

      • 接下来我们用代码验证一下,可以发现和模型完全正确。

      • 我们再来看一下C1和C2中虚基表的值

9.继承中的问题:

  • 1.所有的类都可以被继承吗?

  • 答案:不是,final说明的类不能被继承。
  • 2.继承是实现代码复用的唯一手段吗?

  • 答案:不是,模板也是代码复用的重要手段。
  • 3.基类那些成员被子类继承了?

  • 答案:
  • 成员变量:
    • 普通成员变量:基类中所有的普通成员变量被子类都继承了。
    • 静态成员变量:被继承了,而且多个子类中拥有的是基类中同一份静态成员变量。
  • 成员函数:
    • 普通成员函数:基类中所有的普通成员函数都被子类继承了。
    • 静态成员函数:被继承了。
    • 默认成员函数:构造/拷贝构造/赋值运算符重载/析构函数 //这个问题说法不一:一种认为继承下来的原因是因为:在子类中的初始化列表中可以直接去调用基类的构造函数。
  • 4.关于基类与派生类对象模型说法正确的是(E)
  • A.基类对象中包含了所有基类的成员变量
  • B.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量
  • C.子类对象中没有包含基类的私有成员
  • D.基类的静态成员可以不包含在子类对象中
  • E.以上说法都不对
  • 答案:A
  • 解析:
    • A.静态变量就不被包含
    • B.同理,静态变量就不被包含
    • C.父类所有成员都要被继承,因此包含了
    • D.静态成员一定是不被包含在对象中的
    • E.很显然,以上说法都不正确
  • 5.下面哪项结果是正确的( )
  • class Base1 { public: int _b1; };
  • class Base2 { public: int _b2; };
  • class Derive : public Base1, public Base2
  • { public: int _d; };
  • int main(){
  • Derive d;
  • Base1* p1 = &d;
  • Base2* p2 = &d;
  • Derive* p3 = &d;
  • return 0;
  • }
  • A.p1 == p2 == p3
  • B.p1 < p2 < p3
  • C.p1 == p3 != p2
  • D.p1 != p2 != p3
  • 答案:

  • 看到这里不如点个赞再走吧!!!

猜你喜欢

转载自blog.csdn.net/weixin_45897952/article/details/124544377