c++ || 多态和虚函数

本文详细探讨了C++中的多态性,包括动多态与编译时多态,阐述了虚函数的概念及其作用,如虚析构函数、虚函数表(vftable)和vfptr。讲解了虚函数的声明、纯虚函数和抽象类的应用,以及动多态的实现条件和调用过程。此外,还涉及RTTI和静态、动态联编的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多态

在这里插入图片描述

动多态

定义:“一个接口、多种方法”,在程序运行过程中才决定调用的函数
允许将子类类型的指针赋值给父类类型的指针
多态性在c++中通过虚函数(Virtual Function)实现。
重载与多态无关,覆盖与多态有关
封装(代码模块化)和继承(扩展已存在的代码)的目的都是为了代码重用,而多态的目的是为了接口重用

对象名.方法说明需要在编译时就确定,和那个函数对应,所以是采用静态编译
指针,引用 调用方法时是在执行时才确定调用哪个方法 ,是动态编译

多态性是允许将父对象设置成为和它一个或更多的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

编译时多态 早绑定

通过函数重载和运算符的重载来实现的
编译时确定关系
在这里插入图片描述

运行时多态性 晚绑定

程序执行前,无法根据函数名和参数来确定要调用那一个函数

  • 必须通过
    1.类继承关系public(公有继承表示“是一个”)
    2.函数是虚函数,
    3.以指针或引用调用虚方法来实现的
    ,目的是建立一种通用的程序

虚函数

  1. 虚函数是一个类的成员函数。仅用于继承关系中的对象,只能定义在类中,不能再类外定义
  2. 一个继承关系中只有最顶层的类中有一个虚表指针vfptr
  3. 虚函数表存在数据区,虚函数表中存放的是虚函数指针
  4. 虚函数的虚就虚在所谓“推迟联编”或“动态联编”上,一个类函数的调用并不是编译时刻确定的,而是在在运行时才能被确定
  5. 带有虚函数的类中的每一个对象都有一个虚指针指向该类的虚函数表。
  6. 声明格式: virtual 返回类型 函数名 ()
  • 当某个类的一个类成员函数被定义为虚函数,则由该类派生出的所有派生类中,该函数始终保持虚函数的特征
  • 虚函数的定义如果在类外,则virtual只能加在函数声明之前,而不能加在函数定义前面

什么函数不能声明为虚函数

全局函数、静态函数(静态函数是被所有对象所共享的函数)、友元函数、构造函数(调用构造函数时还没有进行实例化)、内联函数都不能定义为虚函数

虚析构函数:在类的继承时,防止释放内存时忘记释放派生类的对象。

析构函数可以定义为虚函数

在没有继承关系时,可以不写成虚函数,但如果存在继承关系,则基类的析构函数要写成虚函数。
基类的指针或引用指向子类的对象时,会调用基类和子类的构造函数,如果子类的构造函数在堆上开辟了空间的话,当这个对象需要析构时,如果基类的析构函数不是虚函数,那么对象在析构时就只会调用基类的析构函数而不调用子类的析构函数,这就导致子类对象被构造了,但是没有被析构,这样会导致内存泄漏的情况出现。

  • 纯虚析构函数
    声明之后,在类外实现的析构函数

class A{
	public:
		A(){
			cout<<"构造函数A"<<endl;
		}
		virtual ~A() = 0;
};
 
class B: public A{
	public:
		B(){
			cout<<"构造函数B"<<endl;
		}
		~B();
};
A::~A(){
	cout<<"纯虚析构函数A"<<endl;
}
B::~B(){
	cout<<"析构函数B"<<endl;
}
int main(){
	A *a = new B;
	delete a;
}

虚函数的传递性

  • 虚函数会有传递性 父类析构函数是虚函数 则子类的析构函数也变成虚函数
    父类中如果有虚函数 子类中对应的相同的函数会被传递为虚函数
    相同的函数:同返回值 同函数名 同参数列表

vftable vfptr

  • vftable :编译时期发现类中有虚函数 就会对这个类生成vftable(虚函数表) 将当前类当中的虚函数指针放到vftable
    vftable放到 .rodata段(只读数据段)
  • vfptr: 当对象构造的时候,如果发现该类有vftable 就会将vftable的地址写入到这个类对象(vfprt)当中

虚函数表

编译器利用虚函数表解决虚函数的问题
虚函数表实际上是一个数组,存储了为类对象进行声明的虚函数的地址

虚函数表的地址从哪里找到

A: 在声明一个虚函数类对象的同时,这个对象就被添加了一个隐式成员,该成员保存了指向虚函数地址数组(虚函数表)指针vfptr
函数地址数组:每个函数都是有地址的,将函数的地址存放在一个数组中,这个数组就是虚函数表vftable,成员对象中就是保存的指向这个数组的指针

在这里插入图片描述

vftable 的个数

只要包含虚函数的类就会有一个虚函数表,当这个类是基类时,其派生类也会有相应的虚函数表,当一个类有多个对象时,这些对象共享一个虚函数表

基类对象包含一个指针,该指针指向基类中所有虚函数的地址表,派生类将包含一个指向独立地址表的指针。两个表是相互独立的!当派生类重新实现了基类中的虚函数,那么派生类的虚函数表将保存新的函数地址。如果派生类并未对虚函数做改动,那么虚函数表将保存原始函数地址。也就是说,派生类的虚函数表进行改动并不会对基类的虚函数表有影响。

子类中的虚函数表

  • 派生类中的虚函数必须与基类中的虚函数同名外,同参数列表,同返回类型,否则被认为是重载
  • 拷贝Base中的虚函数表 ,如果派生类中有满足和基类相同函数名,返回类型,参数列表的函数,则将拷贝来的虚函数表中的函数指针替换成为自己的虚函数指针,构成自己的虚函数表,并且把虚表指针指向派生类的虚表。==> 同名覆盖

父类析构函数的位置替换成子类析构函数的虚函数,子类中如果还有其他的虚函数就直接添加进虚函数表中 ⇒ 覆盖
为了将父类子类的虚函数 都集中在同一张虚函数表中,所以子类是拷贝父类的虚函数表

在这里插入图片描述sizeof(Object) //4(value) +4(vfptr) 8个
sizeof(Base) //4个
sizeof(Test) //4个

纯虚函数

  1. 纯虚函数是在基类中声明的虚函数,在基类中没有定义。成员函数的形参后面写上=0,则成员函数为纯虚函数
    =0只能出现在类内部的虚函数声明语句
virtual void draw() = 0;   //纯虚函数
  1. 纯虚函数没有函数体

  2. 纯虚函数只有函数的名字而不具备函数的功能,不能被调用

  3. 作用:

在基类中为其派生类保留一个函数的名字,方便派生类进行定义,如果基类中没有保留函数名字,则无法实现多态性
主要就是为了提供接口

  1. 一个类中声明了纯虚函数,在派生类中没有对函数进行定义,则该虚函数在派生类中仍然为纯虚函数,派生类还是一个抽象类
  2. 纯虚函数在派生类中重新定义之后,派生类才能实例化出对象
  3. 带纯虚函数的类叫做虚基类。这样的类只有被继承,重写虚函数后才能使用。

抽象类

  1. 带有纯虚函数的类叫做抽象类
  2. 抽象类不能够实例化对象
  3. 要求限制子类必须覆盖某一个接口,子类是一个非抽象类,要重写父抽象类的所有抽象方法
  4. 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出
  5. 派生类没有重新定义纯虚函数,只是单纯继承了基类的纯虚函数,则这个派生类还是抽象类,如果派生类给出了纯虚函数的定义,则该派生类就不是抽象类,就可以实例化对象
  6. 抽象类的作用

将有关操作作为结果接口组织在一个继承层次结构中,由他来为派生类提供一个公共的根,派生类将具体实现在基类中作为接口的操作

class View  //抽象类
{
public :
	virtual void process() = 0;  //具有纯虚函数
};

class View_insert :public View  //继承抽象类的类要实现其中的纯虚函数
{
public:
	virtual void process()
	{
		cout << "insert" << endl;
	}
};

动多态

  • 动多态:产生:使用指针或者引用调用虚函数 就会产生动多态调用

动多态实现条件

  • 要有public继承关系
  • 要有虚函数重写(被 virtual 声明的函数叫虚函数)
  • 要有父类指针(父类引用)指向子类对象

动多态调用过程

  • 动多态调用过程
    1 使用指针或者引用调用虚函数
    2 在对象中找到vfptr
    3 根据vfptr (虚表指针)找到vftable(存储虚函数指针)
    4 在vftable中找到要调用的函数
    5 调用相应函数

动多态实现原理

  • **动态多态的实现原理 **

类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构,虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,在构造对象时就会将虚函数表存放在对象的虚函数指针中,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址,调用虚函数。

class Base{
virtual ~Base() 
//虚函数 ->生成虚函数表
{
}
};
class Derive:public Base{
~Derive()  //从父类的传递下来 析构也是虚函数
{
}
};
int main()
{
Base* pb = new Derive(); 
delete pb ;
/*子类函数调用完在去调用父类函数的析构函数   
pb是一个指针,新生成的是Derive类对象 使用指针调用Derive析构函数 
调用完后自动调用父类函数的析构函数 */

Base* pb1 = new Base();
delete pb1;//
}
  • 父类pb1指针调用析构函数 是个虚函数,在pb1对象中找到vfptr,再找到vftable 调用Base的析构函数
  • 子类对象调用虚函数,先从父类中继承他的虚函数表,把Base类的析构函数位置修改为自己的析构函数 ==> 覆盖

RTTI

什么是RTTI,

  • Run Time Type Info,是一种指针,指向一个type结构体,结构体中存储的是类型信息
  • 在编译期产生,vftable是在编译时期产生放在只读数据段,不能修改,所以RTTI也是在编译期产生
  • RTTI指针放在vftable里面,类型信息放在.rodata段
  • 一个类有一个RTTI

11 父类指针如何转化成子类指针,转化有什莫条件

dynamic_cast:父类指针为子类指针专用的类型强转
1 必须有RTTI 2 父类指针指向的对象中的RTTI确实是子类对象

Base* p =  new Derive(); 
Derive* pb = dynamic_cast<Derive> (p);
//会判断p是不是Derive类型的,是则可以强转成功,不是则会

静态联编 动态联编

指针或引用指明派生类对象并且调用虚函数(->),则程序动态选择派生类的虚函数,称为动态联编
对象名和 . 运算符引用特定的一个对象来调用虚函数,被调用的虚函数是在编译时期确定的,称为静态联编

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值