当前位置: 首页 > news >正文

继承与多态

继承与多态的分析

  • 继承
    • 继承与访问限定比较
    • 派生类和基类关系
      • 派生类的构造顺序
      • 基类对象(指针)派生类对象(指针)的转换
      • 重载和隐藏
    • 虚函数
    • 静态绑定与动态绑定
      • 指针调用
      • 其他调用的绑定方式
      • 虚函数实现的依赖
  • 多态

继承

继承的本质:

  • 实现代码的复用
  • 在基类中提供统一的虚函数接口,可以让派生类进行重写,就可以使用多态了。(具体看本文多态章节)。
class A
{
public:A(int a1 = 1, int a2 = 2, int a3 = 3) :a1_(a1), a2_(a2), a3_(a3) {}~A(){}int a1_;
protected:int a2_;
private:int a3_;};class B:public A
{
public:B(int b1 = 10, int b2 = 20, int b3 = 30) :b1_(b1), b2_(b2), b3_(b3) {}~B() {}int b1_;
protected:int b2_;
private:int b3_;};

B类继承了A中的成员变量和方法,如下图所示。并且由于在派生的时候会在变量前面加入基的限定符,如A:a1_,所以即使A类和B类的成员变量和方法同名,也不会冲突。
在这里插入图片描述

	B b;b.show();//当调用show方法时,优先调用B类自己定义的成员变量和方法

继承与访问限定比较

public:可以在类的内部和外部访问。
protected:只能在类的内部以及派生类(子类)中访问。
private:只能在类的内部访问。
Tips:在c++中,默认情况下,类的成员(属性和方法)的访问权限是private。
class(默认private)、struct(默认public)。
在这里插入图片描述
访问权限顺序:public>protected>private
继承方式:public、protected、private

重点:
基类到派生类的访问权限是不能大于基类的访问权限的,比如说protected继承方式,那么基类中的public成员变量只能变成protected方式

派生类继承访问权限分析
这里以公有继承为例:

#include <iostream>
using namespace std;
class A
{
public:A(int a1 = 1, int a2 = 2, int a3 = 3) :a1_(a1), a2_(a2), a3_(a3) {}~A(){}int a1_;
protected:int a2_;
private:int a3_;};class B:public A
{
public:B(int b1 = 10, int b2 = 20, int b3 = 30) :b1_(b1), b2_(b2), b3_(b3) {}~B() {}//void show(){cout << "基类公有变量a1_:"<<a1_;//可以访问cout << "基类保护变量a2_:" << a2_;//可以访问cout << "基类私有变量a3_:" << a3_; //不能访问}int b1_;
protected:int b2_;
private:int b3_;};
int main()
{//外部访问基类变量权限限定B b;cout << "基类公有变量a1_:" << b.a1_; //可以访问cout << "基类保护变量a2_:" << b.a2_; //不能访问cout << "基类保护变量a3_:" << b.a3_; //不能访问return 0;
}

依次改变B的继承方式,总结可以得到下表
在这里插入图片描述
注意:派生类从基类继承private的成员,但是派生类无法直接访问。

class定义派生类,默认继承方式是private继承;
struct定义派生类,默认继承方式是public继承。

派生类和基类关系

派生类的构造顺序

  • 派生类的构造和析构函数,负责初始化和清理派生类部分
  • 派生类从基类继承来的成员,由基类的构造和析构函数负责初始化和清理工作

下面是代码验证

class Base
{
public:Base(int data) :dataa_(data) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }
protected:int dataa_;
};class Derive :public Base
{
public:Derive(int data):datab_(data), Base(data){cout << "Derive()" << endl;}~Derive() { cout << "~Derive()" << endl; }private:int datab_;
};
int main()
{Derive b(200);return 0;
}

执行结果为
在这里插入图片描述
派生类的完整生命流程如下
1、派生类调用基类的构造函数,初始化从基类继承的成员
2、调用派生类自身的构造函数,初始化派生类成员
3、调用派生类的析构函数,析构派生类成员
4、调用基类析构函数,释放派生类中从基类继承来的成员

基类对象(指针)派生类对象(指针)的转换

一般来说,派生类相对基类来说是占用更大的内存空间的,基于这一点理解以下结论。

  • 将派生类对象赋值给基类对象,可以,赋值后的基类对象只能返回基类的成员
  • 将基类对象赋值给派生类对象,错误,(因为基类可能没有包含派生类独有的那部分数据)
  • 将派生类对象指针(引用)给基类对象指针(引用),可以,(基类指针是指向派生类对象的,但是由于基类指针的限定,只能访问派生类中的基类成员。)
  • 将基类对象指针(引用)给派生类对象指针(引用),错误

在这里插入图片描述

总结:在继承结构的转换,一般只支持从派生类到基类的转换

重载和隐藏

重载关系:一组函数要重载,必须要处在同一个作用域当中;并且函数名字相同,参数列表不同
隐藏关系:在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏调用了。

class Base
{
public:void show() { cout << "Base::show()" << endl; }void show(int) { cout << "Base::show(int)" << endl; }};class Derive :public Base
{};
int main()
{Derive b;b.show();b.show(10);return 0;
}

代码的执行结果为,调用的是基类的方法
在这里插入图片描述

class Base
{
public:void show() { cout << "Base::show()" << endl; }void show(int) { cout << "Base::show(int)" << endl; }};class Derive :public Base
{
public:void show() { cout << "Derive ::show()" << endl; }
};
int main()
{Derive b;b.show();//因为派生类定义了show方法,所有基类的同名方法show()和show(int)被隐藏//只能通过b.Base::show()、b.Base::show(10)调用基类的show方法//b.show(10); return 0;
}

在这里插入图片描述

因为重载是要处于同一作用域内才起作用,而基类和派生类处于不同作用域,当派生类定义了与基类同名的函数,基类中的与其同名及其重载函数就被隐藏了。

虚函数

①如果一个类里面定义了虚函数,那么在编译阶段,编译器会给这个类产生一个唯一的vftable,即虚函数表,其中主要存储的就是RTTI(Run-Time Type Information,运行时类型识别)指针和虚函数的地址。
当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区),不可更改

class Base
{
public:Base(int data=10):dataa_(data){}//虚函数virtual void show() { cout << "Base::show()" << endl; }virtual void show(int) { cout << "Base::show(int)" << endl; }
protected:int dataa_;
};

如Base类中定义了两个虚函数,其生成的虚函数表如下
在这里插入图片描述
②如果使用带有虚函数的类定义一个对象,其大小为成员变量的大小+虚函数指针的大小(一般为8字节),该虚函数指针指向虚函数表。(该Base类因为定义了虚函数,所以有一个用户不可见的vfptr指针)。
一个类型定义的多个对象,他们的vfptr都是指向同一个虚函数表。

Base b1;
Base b2;
Base b3;
//这三个对象的vfptr都是指向Base类的虚函数表

③一个类里面的虚函数的个数,不会影响对象的内存大小(不是说多个虚函数,就需要多个vfptr,vfptr始终指向的是虚函数表);虚函数的个数影响的是虚函数表的大小

基类是虚函数对派生类的影响

如果派生类中的方法,和从基类中继承的某个方法,满足:

  • 返回值、函数名、参数列表都相同
  • 基类的方法是虚函数

则派生类的这个方法,会被处理成虚函数

class Derive :public Base
{
public:Derive(int data=20):Base(data),datab_(data){}void show() { cout << "Derive ::show()" << endl; }
protected:int datab_;
};

这里派生类的show()方法满足上述条件,发生覆盖(在虚函数中,派生类的Derive::show()覆盖了基类中的Base::show())。
在这里插入图片描述

静态绑定与动态绑定

指针调用

class Base
{
public://普通的show函数,是静态绑定//void show() { cout << "Base::show()" << endl; }//virtual show函数,是动态绑定virtual void show() { cout << "Base::show()" << endl; }
private:int dataa_;
};class Derive :public Base
{
public:void show() { cout << "Derive::show()" << endl; }
private :int datab_;
};
int main()
{Derive d;Base* pb = &d;pb->show();cout << "基类Base大小:" << sizeof(Base) << endl;cout << "派生类Derive大小:" << sizeof(Derive) << endl;cout << typeid(pb).name() << endl;cout << typeid(*pb).name() << endl;return 0;
}

pb是基类指针,但其指向的是派生类对象,由于基类指针的限定,当调用pb->show()方法时,就自动去派生类中的基类部分去找show方法的实现。
1.如果Base::show()是普通方法,则进行静态绑定(call Base::show())

2.如果Base::show()是虚函数,则进行动态绑定,反汇编代码如下
在这里插入图片描述

具体步骤为:

  • 根据pb指向的对象的前四个字节获取虚函数指针vfptr的值(其指向的对象是一个派生类Derive对象)
  • 根据vfptr获取其指向的虚函数表(这里的虚函数表为,Derive的虚函数表)
  • 根据虚函数表得到其对应的虚函数(这里的&Derive::show()虚函数重写了&Base::show(),所以最后调用的是Derive::show()方法)

Base::show()方法是否是virtual的输出比较
在这里插入图片描述

  • 对于普通方法,Base类中有一个int类型成员变量,占4字节;Derive继承基类的成员变量+自身定义的int类型变量,共8字节。
  • 对于Base::show()方法是虚函数,则有一个虚函数指针vfptr,8字节,则基类大小为4+8=12(我这里是64位系统,8字节对齐,变成了16个字节大小);派生类在基类的16字节基础上加上本身的int成员变量4字节,再次内存对齐,共24字节。

对于pb指向类型的理解

	Derive d;Base* pb = &d;pb->show();cout << typeid(*pb).name() << endl;
/*对于这里pb指向的类型:取决于Base有没有虚函数
*如果Base没有虚函数,*pb识别的就是编译时期的类型;
*如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI类型。
*/

前提:pb为Base类型的指针,指向的是Derive的派生类对象:

  • Base没有虚函数,派生类中没有虚函数,则识别的是编译时期的类型,即Base类。
  • Base存在虚函数,存在Derive类的虚函数表,则识别的就是运行时候的RTTI类型,即为Derive类。

其他调用的绑定方式

class Base
{
public:virtual void show() { cout << "Base::show()" << endl; }
private:int dataa_;
};class Derive :public Base
{
public:void show() { cout << "Derive::show()" << endl; }
private :int datab_;
};int main()
{/*************对象本身调用*****************/Base b;Derive d;//这里因为是对象本身访问自己的成员方法,发生的是静态绑定//无论show是不是虚函数,都是发生静态绑定//很好理解,通过对象本身调用,在编译时期就可以确定形式,不用动态绑定b.show(); //静态绑定d.show(); //静态绑定/*************指针方式调用*****************/Base *pb1=&b;pb1->show(); //动态绑定Base *pb2=&d;pb2->show(); //动态绑定/*************引用方式调用*****************/Base &ref1=b;ref1.show(); //动态绑定Base &ref2=d;ref2.show(); //动态绑定return 0;
}

在这里插入图片描述

总结:动态绑定必须当通过指针(引用)调用虚函数,才会发生。

虚函数实现的依赖

  • 虚函数能产生地址,存储与vftable中
  • 对象必须存在(通过vfptr—>>>vftable—>>>虚函数地址)

构造函数不存在虚函数,因为这个时候还没有对象产生(不满足虚函数依赖的条件二)。即使在类的构造函数中,调用了虚函数,也是静态绑定。
static静态成员方法也不能被实现成虚函数方法,(其不依赖于对象,不满足条件二)

虚析构函数的实现

情景:基类的指针(引用)ptr指向堆上new出来的派生类对象时,delete ptr;

class Base
{
public:Base(int data) :dataa_(data) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }
protected:int dataa_;
};class Derive :public Base
{
public:Derive(int data):datab_(data), Base(data){cout << "Derive()" << endl;}~Derive() { cout << "~Derive()" << endl; }private:int datab_;
};
int main()
{Base* pb = new Derive(10);delete pb;return 0;
}

在这里插入图片描述

在本篇 派生类的构造顺序 一小节中提到,派生类的构造顺序是基类构造–派生类构造–派生类析构–基类析构。
但是这里却没有调用派生类的析构函数,如果派生类有指向外部资源,就会造成内存泄露。

这里为什么没有调用派生类的析构函数?

这里pb是Base类型的指针,当调用delete pb的时候,在Base类中找到其对于的析构函数,Base::~Base(),这里发生的是静态绑定

解决方法
1.使用Derive 类型的指针指向开辟的内存(不是本节重点)

	Derive* pb = new Derive(10);delete pb;

2.将基类的析构函数定义为virtual(发生动态绑定)
重点:基类的析构函数是virtual函数,那么派生类的析构函数自动成为virtual函数。

class Base
{
public:Base(int data) :dataa_(data) { cout << "Base()" << endl; }virtual ~Base() { cout << "~Base()" << endl; }
protected:int dataa_;
};

在这里插入图片描述
这里发生的是动态绑定,pb是Base类型指针,指向派生类对象(存在虚函数),所以这里发生动态绑定;由于派生类的虚析构函数重写了基类的虚析构函数,所以这里调用的是派生类的析构函数。

多态

静态(编译时期)多态:

  • 函数重载,bool compare(int a,int b),bool compare(double a,double b);
  • 模板(函数模板、类模板)

动态(运行时期)多态:
虚函数机制,调用哪个函数在运行时决定。
基类指针(引用)调用哪个派生类对象,就会调用该派生类对象方法,称为多态。
如代码所示

class Animal
{
public:Animal(string name):name_(name){}virtual void bark() {}
protected:string name_;
};
class Cat:public Animal
{
public:Cat(string name) :Animal(name) {}void bark() { cout << name_ << "bark:miao miao~~~" << endl; }};class Dog :public Animal
{
public:Dog(string name) :Animal(name) {}void bark() { cout << name_ << "bark:wang wang~~~" << endl; }};
class Pig :public Animal
{
public:Pig(string name) :Animal(name) {}void bark() { cout << name_ << "bark:heng heng~~~"<<endl; }};
//animal作为基类指针,当传入派生类的地址后,发生动态绑定,调用对应类的bark方法
void bark(Animal* animal)
{animal->bark();
}
int main()
{Cat cat("加菲猫");Dog dog("汪汪队");Pig pig("佩奇");bark(&cat);bark(&dog);bark(&pig);return 0;
}

这里bark函数根据传入的派生类类型,通过基类指针指向,从而实现调用不同派生类的函数。

抽象类和普通类的区别

普通类是用于抽象一个实体的类型,比如这里的Cat类、Dog类、Pig类等;
而这里的Animal作为抽象类:

//1.string name_;让所有的动物实体通过继承Animal直接复用该属性
//2.给所有的派生类保留统一的覆盖/重写接口class Animal
{
public:Animal(string name):name_(name){}//因为bark在animal中并没有实际的作用,只是为派生类提供一个统一的接口,定义为纯虚函数//纯虚函数virtual void bark()=0;
protected:string name_;
};

拥有纯虚函数的类,叫做抽象类。
抽象类不能实例化对象(抽象类并不是为了抽象某个类型而存在的。),但是可以定义指针和引用变量。

http://www.xdnf.cn/news/757567.html

相关文章:

  • 篇章七 数据结构——栈和队列
  • 查看make命令执行后涉及的预编译宏定义的值
  • Python数学可视化——环境搭建与基础绘图
  • 力扣刷题(第四十四天)
  • 主数据编码体系全景解析:从基础到高级的编码策略全指南
  • GEE:获取研究区的DEM数据
  • RocketMQ 学习
  • 性能优化 - 案例篇:数据一致性
  • 清理 pycharm 无效解释器
  • CVE-2021-28164源码分析与漏洞复现
  • DDD架构
  • 历年西安邮电大学计算机保研上机真题
  • 鸿蒙OS基于UniApp的区块链钱包开发实践:打造支持鸿蒙生态的Web3应用#三方框架 #Uniapp
  • 基于Dify实现各类报告文章的智能化辅助阅读
  • 攻防 FART 脱壳:特征检测识别 + 对抗绕过全解析
  • C++输入与输出技术详解
  • hot100 -- 5.普通数组系列
  • CFTel:一种基于云雾自动化的鲁棒且可扩展的远程机器人架构
  • Domain Adaptation in Vision-Language Models (2023–2025): A Comprehensive Review
  • 2022—2025年:申博之路及硕士阶段总结
  • 小明的Java面试奇遇之智能家装平台架构设计与JVM调优实战
  • 什么是子查询?相关子查询的性能问题?
  • GpuGeek 618大促引爆AI开发新体验
  • Redis缓存存储:从基础到高阶的深度解析
  • STM32G4 电机外设篇(三) TIM1 发波 和 ADC COMP DAC级联
  • 软件无线电关键技术之正交调制技术
  • Java进阶---JVM
  • GraphQL 入门篇:基础查询语法
  • Cinnamon开始菜单(1):获取应用数据
  • Debian上安装PostgreSQL的故障和排除