底层软件 | 多态(C++)

概述

这一篇文章主要是想记录一下多态以及虚表的一些理解。

当派生类使用基类方法,可能需要有所改变,也就是当同一个行为(成员函数)在派生类和基类中的行为不同时,我们需要用到多态,同一个方法的行为随着上下文而有差异,有两种机制可以来实现多态公有继承:

  • 在派生类中重新定义基类的方法

  • 使用虚函数。

切入点(普通继承)

#include<iostream>
using namespace std;
class animal{
	public:
		animal(float a, float b){
			animal::a = a;
			animal::b = b;
		}
		virtual void print(){
			cout<<a<<" "<<b<<endl;
		}
		~animal(){
			cout<<"调用基类的析构函数"<<endl;
		}
	protected:
		float a;
		float b;
};
class dog:public animal{
	public:
		dog(float a, float b, float c):animal(a, b){
			dog::c = c;
		}
		void print(){
			cout<<a<<" "<<b<<" "<<c<<endl;
		}
		~dog(){
			cout<<"调用派生类的析构函数"<<endl;
		}
	protected:
		float c;
};
void test(animal &tmp){
	tmp.print();
}

int main(){
	animal a1(1,2);
	a1.print();
	dog d1(4,5,6);
	d1.print();
	test(d1);
}
//输出的结果:
1 2 (这里是调用了animal.print())
4 5 6(这里是调用了dog.print())
4 5 (这里是调用了animal.print())

分析输出结果:

  • 第一行的输出毋庸置疑
  • 第二行的输出可以看到c++是支持派生类重载基类的成员函数,我们调用了dog.print()函数
  • 第三行的输出,由于我们的test(animal &tmp),传入的参数是一个基类对象,派生类也可以看做是基类对象,因此参数的传递不会出错,但是系统分不清传递过来的是一个基类还是一个派生类,在调用成员函数的时候,调用的是基类的print()成员函数。引发了一个问题,这里其实我们是想要调用派生类的成员函数,但由于系统不能正确分辨成员类型,导致出错。

因此,我们需要用到c++提供的多态性来解决这个问题。

  • 先期联编:在编译时就能够确定哪个重载的成员函数被调用的情况
  • 滞后联编(多态性):在系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力
    ##多态(虚函数)
#include<iostream>
using namespace std;
class animal{
	public:
		animal(float a, float b){
			animal::a = a;
			animal::b = b;
		}
		virtual void print(){
			cout<<a<<" "<<b<<endl;
		}
		virtual ~animal(){
			cout<<"调用基类的析构函数"<<endl;
		}
	protected:
		float a;
		float b;
};
class dog:public animal{
	public:
		dog(float a, float b, float c):animal(a, b){
			dog::c = c;
		}
		virtual void print(){
			cout<<a<<" "<<b<<" "<<c<<endl;
		}
		virtual ~dog(){
			cout<<"调用派生类的析构函数"<<endl;
		}
	protected:
		float c;
};
void test(animal &tmp){
	tmp.print();
}
void del(animal *tmp){
	delete tmp;
}
int main(){
	animal a1(1,2);
	a1.print();
	dog d1(4,5,6);
	d1.print();
	test(d1);
}
//输出结果
1 2
4 5 6
4 5 6
调用派生类的析构函数
调用基类的析构函数
调用基类的析构函数

分析输出结果:

  • 第1、2行的分析同上

  • 第3行有所变化,这里调用了派生类的print()成员函数,因为我们用了虚函数,系统成功的分辨出了对象的真实类型,成功的调用了各自的重载成员函数

  • 第4,5行是程序运行完毕后,回收资源。首先delete d1, 先是运行派生类dog的析构函数,然后再运行基类animal的析构函数

  • 第6行是delete a1,运行的是基类的析构函数。

这里值得要注意的是,如果析构函数没有设置为虚函数,那么del(d1)的时候,只会调用基类的析构函数,因为系统不能分辨对象的真实类型。但是如果是delete d1的时候,还是能够分辨派生类和基类。

规则:

    1. 如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。
    1. 只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。
    1. 静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。
    1. 内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。
    1. 构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。
    1. 析构函数可以是虚函数,而且通常声名为虚函数。

构造函数为什么不能是虚函数:

    1. 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
    1. 虚函数表是在构建函数中进行初始化工作,即初始化vptr,让它指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。

为什么析构函数一定是虚函数(为什么抽象类中至少包含一个纯虚函数——析构函数)
如果析构函数不是虚函数,而程序执行的时候又要通过基类的指针去销毁派生类的动态对象,那么用delete来销毁对象的时候,只是调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全。

  • 如果基类的析构函数是虚函数:
    • 析构的时候先调用派生类的析构函数,再调用基类的析构函数,与构造函数相反
  • 如果基类的析构函数不是虚函数:
    • 析构的时候只会调用基类的析构函数。

虚函数表

虚函数的作用主要是实现多态机制。虚函数是通过虚函数表(V-Table)实现的。

在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其内容真实反映实际的函数。

这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

成员函数不覆盖的情况:基类(f(),g(),h()); 派生类(f1(),g1(),h1())
成员函数覆盖的情况:基类(f(),g(),h()); 派生类(f(),g1(),h1())

  • 一般继承(不覆盖): f(),g(),h(),f1(),g1(),h1()
  • 一般继承(覆盖):f()已经被重载,g(),h(),g1(),h1()
  • 1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
  • 2)没有被覆盖的函数依旧。
  • 多重继承(不覆盖)
    子类的成员函数被放到了第一个父类的表中。
  • 多重继承(覆盖)
    只覆盖第一个父类的成员函数
  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
c多态底层实现涉及到虚函数和虚表。在多态中,虚函数用于实现动态绑定,允许在运行时根据对象的实际类型来调用相应的函数。虚函数在父类中被声明为虚函数,子类可以重写这个虚函数。 在底层实现中,每一个含有虚函数的类都有一个虚表(virtual table)。虚表是一个指针数组,其中存储着虚函数的地址。当一个对象被创建时,会为它的虚表分配内存,并将虚函数地址保存在其中。这样,当我们通过一个基类的指针或引用调用虚函数时,程序会根据对象的实际类型查找到正确的虚表,并调用相应的虚函数。 虚表存储在每个对象的前面,可以通过对象的指针或引用来访问。每个对象的虚表指针指向的是该对象所属类的虚表。 因此,多态底层实现依赖于虚函数和虚表的机制。它允许程序在运行时根据对象的实际类型来调用相应的虚函数,实现了动态绑定的特性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【C++多态底层原理](https://blog.csdn.net/m0_72563041/article/details/129912899)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C++多态底层如何实现](https://blog.csdn.net/Ferlan/article/details/82856536)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

TrustZone_Hcoco

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

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

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

打赏作者

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

抵扣说明:

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

余额充值