【C++】继承、多态、抽象类


秃头侠们好呀,今天来说 继承和多态

面向对象是什么

  • 首先面向对象的3大特征是封装、继承、多态。
  • 说到面向对象,就要和面向过程联系起来谈,C语言就是面向过程的,面向过程强调事件的过程和顺序,做什么,怎么做,把事情分解成过程,每一步调用相应的函数方法,整个过程循序渐进,相互配合完成任务。
  • 而面向对象强调事件的对象角色主体,更贴近人的思维,把现实世界的实体抽象为一个类。
  • 封装的特性,就是将事物的属性和行为封装到一个类中,这里对应成员变量和成员函数,便于管理和拓展,未来有什么新的属性或行为,加进来就行了。
  • 继承的特性使类之间产生关系,我想用其他类的成员或方法,可以继承,这样提高了代码的复用性。
  • 对于多态意味调用成员函数时,会根据调用对象的不同执行不同的函数。
  • 举个简单的例子,就拿我的一天来说,我起床刷牙洗脸骑车上班,这是一套顺下来的流程,这就是面向过程;那面向对象是这样的,主人公是谁,是我,我有什么,有床,牙刷,车,我的行为有什么,起床,刷牙,骑车上班。
  • 这里就发现,面向过程和面向对象并不互斥,面向对象内也可以有面向过程的设计,面向对象是基于面向过程的。这就是我对面向对象的理解。

面向对象的三大特征

  1. 面向对象的三大特征是:封装、继承、多态。
  2. 封装:将成员变量和成员方法结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理:比如观看兵马俑,如果能随意进来,那么很容易出乱子。所以我们需要建围墙将景区围起来,我们的目的不是不让人进去,你可以通过买票,合理行为游玩。 C++通过公有私有保护三个关键字来控制成员变量和成员函数的访问权限等级。 private 只能在本类中访问 ;protected 只能在本类或者子类中访问;public 是哪儿都可行。 封装的好处:隐藏实现细节,提高了代码的复用性,提高了安全性。
  3. 继承:C++最重要的特征是代码重用,通过继承父类,子类不仅拥有父类的成员,还拥有新定义的成员。从父类继承过过来的表现其共有特性,而新增的成员体现了其个性。 继承的好处:提高代码的复用性;提高代码的拓展性;同时也是多态的前提。
  4. 多态:在面向对象中,多态是指通过父类的指针或引用调用虚函数,通过传入不同的对象,调用不同的虚函数。当父类指针(引用)指向 父类对象时,就调用父类的虚函数;否则调用子类的。多态提高了代码的灵活性。

继承

  1. 继承是类设计层次的复用,子类对父类成员的复用,在保持原有类特性的基础上进行扩展,增加功能。
  2. 继承后父类的成员函数和成员变量都会变成子类的一部分,这里体现了复用。
  3. 保护和私有在父类没有区别,在子类中私有不可见,保护可见。
  4. 父类=子类,赋值兼容规则,切片/切割(公有继承),这里不存在类型转换,语法天然支持。
person p;
student s;
p=s;  person*ptr=&s;  person& ref=s;
  1. 如果是私有、保护继承不支持切片,因为权限改变。
  2. 子=父,对象不可以,指针引用可以,但是有越界风险。
  3. 子类和父类中有同名成员,子类成员将屏蔽父类同名的成员,这叫隐藏(重定义),不过你可以通过指定类域访问父类的同名成员。
  4. 只需要函数名相同就构成隐藏与其他无关。
  5. 函数重载要求同一作用域,构成隐藏的父类子类那个同名函数不在同一作用域。
  6. 默认生成的子类的构造和析构,继承下来的父类成员会去调父类的构造和析构,自己的就内置类型不处理,自定义类型调它的构造和析构。拷贝和赋值同理。
  7. 继承下来的都调父类的,自己的就按普通类处理。自己实现也要这样实现。
  8. 什么情况自己必须写?1、当父类没有默认构造,需要我们显示写。2、子类有资源需要释放,自己写析构。3、子类有浅拷贝问题。
  9. 子类析构结束时会自动调父类的析构,构造时先构造父类。
  10. 友元关系不能继承。
  11. 静态成员会继承下来,但是还是只有一份(取地址相同)。
  12. 多继承 class person:public student,public teacher{};

菱形继承:

在这里插入图片描述问题:菱形继承存在数据冗余和二义性问题。
解决:通过虚继承解决。在public A前面加上virtual。
Class A称为虚基类。

虚继承解决菱形继承的原理

让class B 和 class C共享相同的基类成员。在自己的类中增加一个虚基表指针,该指针指向一个虚基表,虚基表内存放的是各自相对于公共基类成员变量的偏移量。
class D中把公共的class A的变量放一份在最后,B、C要找A就用偏移量找就行了。

在这里插入图片描述看似class D多存了两个虚基表指针,但是当数据量很大时就体现出来优势了,因为不需要存两份的公共class A的成员变量。只需要存2个指针的大小就够了。

多继承中会有多个虚函数表,几重继承就会有几个虚函数表。这些虚函数表会按照派生的顺序依次排列。如果子类改写了父类的虚函数,那么就会用子类自己的虚函数覆盖相应的父类虚函数;如果子类有新的虚函数,那么就添加到第一个虚函数表的末尾。

继承和组合

继承和组合都是一种复用

class A{//这是继承
    int a;
};
class B:public A{
    int b;
}
class A{//这是组合
    int a;
};
class B{
    A a;
    int b;
}
  • 继承是is-a的关系,父类成员的实现细节对子类是暴露的,也就是说每个派生类对象都是一个基类对象,比如男人,女人是人的派生,男人是人,女人也是人。
  • 组合是has-a的关系,B组合了A,B对象内中有A,使用A去构成更完整的B,B只是能正常的使用A,但A的实现细节并不暴露给B,比如头组合眼睛嘴巴鼻子耳朵,但你不能说头是眼睛嘴巴鼻子。
  • 继承是白盒复用,组合是黑盒复用。白盒测试要求更高。
  • 对于继承,子类对父类的保护和公有是可以直接访问的,破坏了封装。
  • 对于组合,只能访问其公有。

UML,类之间,模块之间的关系最好是:高内聚,低耦合

1、高内聚指与该类的内部成员关系非常紧密,跟我这个类没关系的成员别往我这里放。
2、低耦合指类之间关系越弱越好,依赖性越弱越好,因为这样方便维护,比如你之后添加新功能或者修改数据,对于继承的子类,你父类修改了就可能导致子类受到影响,牵一发而动全身,而组合你改你的保护私有啥的不会影响其他类。
3、多态是建立在继承基础上的。

多态

  • 多态是同一个事物在不同场景下的多种形态。
  • 在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为,与之相对应的编译时绑定函数称为静态绑定。所以多态分为静态多态和动态多态。
  • 静态多态: 静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等。
  • 动态多态: 动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。
  • 动态多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。
  • 实现动态多态的条件:要有派生类对基类的虚函数进行重写,三同+虚函数virtual构成重写(覆盖),(被 virtual 声明的函数叫虚函数),要有父类指针(父类引用)指向子类对象 。
  • 动态多态的实现原理:当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表就是一个指针数组,虚函数表存储类虚函数的地址指针, 虚函数表是在构造函数初始化列表阶段由编译器自动生成的。virtual 成员函数会被编译器放入虚函数表中。存在虚函数的类,每个对象中都有一个指向虚函数表的指针(虚表指针vptr)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。
  • 重写覆盖:三同(函数名,参数,返回值)+virtual。
  • 非静态成员函数前+virtual就是虚函数。
  • 重写要求返回值相同有一个例外:协变,要求返回值是父子关系的指针或引用。
  • 同类对象的虚函数表是相同的一张表。
  • 虚函数表是存在代码段上的。
  • 取对象头上的4个字节就是虚函数表指针地址。
  • 一个类对象指针为空,如果去调的成员函数不是虚函数且函数实现不涉及this指针的解引用就可以,否则则越界,因为虚函数需要通过this指针拿得虚函数表指针。

virtual能声明静态成员函数吗?

不能,static成员函数不属于类中任意一个对象的实例,属于类共有的一个函数,没有this指针,因为this指针指向的是具体每个对象实例。对于虚函数,它的调用需要在类中通过拿到虚函数表指针找到对应虚函数表去掉对应函数,你没有this指针怎么找到虚函数地址呢。

虚函数的实现原理?

1、C++ 中的虚函数的作用主要是实现了动态多态,根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。
2、当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表就是一个指针数组,虚函数表存储类虚函数的地址指针, 虚函数表是在构造函数初始化列表阶段由编译器自动生成的。virtual 成员函数会被编译器放入虚函数表中。存在虚函数的类,每个对象中都有一个指向虚函数表的指针(虚表指针vptr),父类的虚表中放的是父类的虚函数,子类因为重写会覆盖父类的虚函数而填写重写的虚函数。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。
3、使用虚函数时,对于内存和执行速度方面会有一定的成本: 1、每个对象都要额外存储虚函数表指针,对于每个类,编译器还会创建一个虚函数表;2、对于每次调用虚函数,都需要额外执行到虚函数表中查找虚函数地址。

为什么将析构函数设置成虚函数?

设置虚析构函数,就是将基类析构函数声明virtual,因为析构函数名会被特殊处理成相同的destructor,此时基类和子类构成重写,虚析构函数就是为了避免内存资源泄漏,如果声明了虚析构函数,当基类指针指向子类对象时,基类指针释放时会调用子类的析构函数完成资源的释放,如果没有声明则只会调基类的析构。尽量是基类和子类的析构都声明virtual,虽然只声明父类的析构就行。

final

1、修饰类表示改类为最终类不可被继承 class A final{};
2、修饰虚函数 virtual void f() final{} 表示该虚函数不可被重写。

override

放虚函数后,检查是否完成重写,没完成就报错。

重载、重写、隐藏的区别

重载:是一种静态多态,两个函数在同一个作用域,函数名相同,函数参数不同,返回值没有要求,则构成重载。
重写:两个函数分别在基类和派生类作用域,函数名/函数参数/函数返回值都必须相同,两个函数必须是虚函数virtual,派生类对基类的虚函数进行了重写。
隐藏:两个函数分别在基类和派生类作用域,只需要函数名相同即可,此时派生类对象在调用函数时,会将同名的基类函数隐藏,不过可以通过指定类域调用。

虚函数表可以改变吗?

虚函数表存在代码段,在编译时期确定写死了,跟const char*的字符串常量一样,不可以更改,强行更改会报错。

抽象类

virtual void f() = 0;纯虚函数,只声明,包含纯虚函数的类叫抽象类,抽象类不能实例化对象,派生类继承,派生类也不行,只能派生类重写后才可,规范纯虚函数派生类必须重写,体现了接口继承。

什么是纯虚函数,有什么作用?

1、格式是:virtual void f() = 0;纯虚函数,只声明。
2、作用:如果在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。例如猫类和狗类的基类是动物类,动物类中有一个吃饭的函数 eat(),那这个 eat() 函数可以是纯虚函数,因为并不能够确定动物吃的东西是什么,具体吃的内容由不同的派生类去实现。
3、包含纯虚函数的类叫抽象类,抽象类不能实例化对象,派生类继承,派生类也不行,只能派生类重写后才可,规范纯虚函数派生类必须重写,体现了接口继承。

请你说说虚函数和纯虚函数的区别?

1、格式区别。
2、虚函数可以有具体的实现,纯虚函数一般没有具体的实现,实现了也没用。 对于虚函数来说,父类和子类都有各自的版本,由多态方式调用的时候动态绑定。 有纯虚函数的类称为抽象类,有纯虚函数的类不能实例化,派生类必须实现纯虚函数的重写才可以实例化。
3、作用:虚函数是 C++ 中用于实现动态多态的机制。 而纯虚函数是当在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。


⭐感谢阅读,我们下期再见
如有错 欢迎提出一起交流

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周周汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值