【C++闯关笔记】详解多态

系列文章目录

上一篇笔记:【C++闯关笔记】map与set底层:二叉搜索树-CSDN博客


文章目录

目录

系列文章目录

文章目录

前言

一、多态是什么?

1.多态的概念

2.多态实现条件

二、多态的一些细节

[1.虚函数的传染性与 override](#1.虚函数的传染性与 override)

2.虚函数重写的一些其他问题

1)多态特例:协变

2)析构函数的重写

3)重载、重写、重定义的细节对比

3.纯虚函数与抽象类

二、多态的原理

1.虚函数指针

2.虚函数表

本文总结



前言

作为C++面对对象编程三架马车的最后一架,多态因为站在前两者的肩膀上,所以比之前的内容知识都略显晦涩。本文将深入讨论多态,从知晓多态的概念到深入理解原理再到最后的实践运用。


一、多态是什么?

1.多态的概念

多态的概念:简单来说,就是多种形态,分为编译时能明确的称为为静态,运行时才能明确的角动态。即多态分为编译时多态(静态多态)和运行时多态(动态多态)

编译时多态(静态多态):函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态。

运行时多态(动态多态): 执行某个函数(行为)时,不同的对象会有不同的结果发生。比如买票这个行为:同样是买票这个行为,成人买成人票,儿童买儿童票,学生买学生票。

静态多态函数重载函数模板在之前的笔记中已经介绍过,这里就不再赘述,本文主要介绍动态多态。

2.多态实现条件

多态在C++中是一个继承关系的特殊情况,当父类对象与子类对象去调用同一函数,会产生不同的行为。

实现多态有三个必要条件

①类与类之间满足继承关系;

②被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖;

③必须是基类的指针或者引用调用虚函数。

让我们来解释一下:

①虚函数:类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

cpp 复制代码
class A
{
public: 
	virtual void test1()
	{
		cout << "A: test1" << endl;
	}
private:
	int _a;
	char _b;
};

②重写/覆盖:派生类中有一个跟基类完全相同的虚函数 (即函数的返回值类型、函数名字、参数类型与个数完全相同)但函数体实现的行为不同,从而隐藏了基类的版本,称派生类的虚函数重写了基类的虚函数。

cpp 复制代码
class A
{
public: 
	virtual void test1()
	{
		cout << "A: test1" << endl;
	}
private:
	int _a;
	char _b;
};

class C :public A
{
public:
	
	virtual void test1()
	{
	    cout << "C: test1" << endl;
	}
private:
	double _c;
};

二、多态的一些细节

1.虚函数的传染性与 override

在C++中,一旦一个函数在基类中被声明为virtual,它在所有派生类中都将保持虚函数的特性 ,无论你是否在子类中显式地写上virtual关键字。

也就是说在派生类中可以不用每次都频繁费事的写virtual关键字,但这带来了一些潜在问题:

cpp 复制代码
class Base 
{
public:
    virtual void test(int x) 
    {
        cout << "Base::test(int)" << endl;
    }
};

class Derived : public Base 
{
public:
    // 程序员本意是想重写覆盖,但写错了函数名!
    void tset(intx) 
    {  
        cout << "Derived::test(int)" << endl;
    }
    // 这实际上没有覆盖Base::test(int),而是创建了一个新函数!
};

像上述写错函数名/变量名这样的小错误,在项目中却容易引起极大的问题。于是自C++11引入了override 关键字。

cpp 复制代码
class Derived : public Base 
{
public:
    //使用override,让编译器帮我们检查
    void tset(int x) override //// 编译器会报错
    {  
        cout << "Derived::test(int)" << endl;
    }
};

所以派生类中

  • 可以省略virtual关键字(因为会自动继承虚特性)

  • 但一定要使用override关键字

  • 对于不希望被进一步重写的函数,可以在基类中可以添加final关键字。

cpp 复制代码
class A
{
public: 

	virtual void test2()final
	{
		cout << "A: test2" << endl;
	}
private:
	int _a;
	char _b;
};

2.虚函数重写的一些其他问题

1)多态特例:协变

原本多态要求重写虚函数时,虚函数的返回值类型要一样,但却有个例外:协变。

协变,即派生类重写基类虚函数时,与基类虚函数返回值类型是不同的:基类虚函数返回基类类型(或其指针/引用),派生类虚函数返回派生类类型(或其指针/引用),并且这些类型之间存在继承关系,这就构成了协变。

关键限制 :必须是指针或引用类型,不能是按值返回。

使用场景与用法如下

引入协变前:

cpp 复制代码
class Animal 
{
public:
    virtual Animal* create() 
    {
        return new Animal();
    }
};

class Dog : public Animal 
{
public:
    // 如果不支持协变,我们只能这样写:
    Animal* create() override 
    {
        return new Dog();  // 必须返回Animal*,即使创建的是Dog
    }
};

void example()
{
    Dog dog;
    Animal* animal = dog.create();  // 返回的是Animal*
    
    // 想要调用Dog特有方法,必须进行向下转型
    Dog* dogPtr = dynamic_cast<Dog*>(animal);
}

引入协变后:

cpp 复制代码
class Animal 
{
public:
    virtual Animal* create() 
    {
        return new Animal();
    }
};

class Dog : public Animal 
{
public:
    // 支持协变,可以直接返回Dog*
    Dog* create() override 
    {
        return new Dog();
    }
};

void example() 
{
    Dog dog;
    Dog* dogPtr = dog.create();  // 直接得到Dog*,不需要转型
    
    Animal* animal = dog.create();  // 也可以赋值给基类指针
}

2)析构函数的重写

只要将基类的析构函数设置为虚函数 ,此时派生类析构函数 只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理 destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。

为什么要这样做?

当一个基类的指针或者引用指向一个派生类对象,若派生类准备析构时会调谁的析构函数?基类,还是派生类? 实际上如果基类没有将析构函数函数设置为虚函数,那么调用的是基类的析构函数,此时若派生类对象中有额外资源申请,由于没有调用派生类析构函数就造成了资源泄漏

对构函数的名称统一处理的目的,正是为了在多态情况下能正常析构派生类对象。

3)重载、重写、重定义的细节对比

3.纯虚函数与抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数 ,纯虚函数可以有定义实现(实现没啥意义因为要被派生类重写),但一般只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。

cpp 复制代码
class A
{
public: 
	virtual void test() = 0;
public:
	char _a;
	int _b;
};

二、多态的原理

1.虚函数指针

下面代码的输出结果是?

cpp 复制代码
class A
{
public:
	virtual void test()
	{
		std::cout << "A" << std::endl;
	}
public:
	char _a;
	int _b;
};

int main()
{
	A a;
	std::cout << sizeof(a) << std::endl;
    return 0;
}
	

由于内存对齐,在32位环境下打印出来应该是8字节(1Byte的_a,填充3字节,4Byte的_b,可点击前方蓝字查看内存对齐规则)。可实际打印出的结果是?

虚函数表指针

除了_a和_b成员,还多一个__vfptr放在对象的前面(有些平台可能会放到对象的最后面),对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针。

所以a对象的大小打印出来是12字节(4Byte的指针,1Byte的_a,填充3字节,4Byte的_b)。

2.虚函数表

虚函数表指针指向的自然就是虚函数表了**,虚函数表** **中存放着一个类所有虚函数的地址,**包括从基类继承的,和本类中函数前加virtual的,虚函数表也简称虚表。

这个虚函数表有什么作用呢?

当满足多态条件后,编译器不再是编译时就确定函数的地址,而是在运行时到指向的对象的虚函数表中查找对应的虚函数的地址。若派生类不重写虚函数,那么程序运行时派生类对象与基类对象调用的就是同一个函数地址;若派生类重写虚函数,那么新的虚函数地址就会覆盖继承下来的虚函数地址(这也是为什么叫覆盖的原因),达到同样的函数实现不同行为(因为实际上执行的函数体不同)。

这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

虚函数表的有关细节

①同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以可以理解为每个类都有自己的虚函数表;

②派生类继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是当派生类构造完成后,这个继承下来的虚函数表指针就不再指向基类的虚函数表,而是派生类自己的虚函数表。

③虚函数和普通函数一样的,都是存在代码段的;

④虚函数表存放在代码段,也就是常量区中。


本文总结

本文深入探讨C++多态机制,重点分析动态多态的实现原理。

本文先介绍了多态的概念与实现条件,之后介绍了多态的一些细节如协变、析构函数重写、纯虚函数等,最后通过虚函数表解开多态的原理。

读完点赞,手留余香~

相关推荐
wanglong37133 小时前
STM32单片机PWM驱动无源蜂鸣器模块C语言程序
stm32·单片机·1024程序员节
与己斗其乐无穷3 小时前
C++学习记录(22)异常
学习·1024程序员节
云雾J视界3 小时前
开关电源拓扑工程宝典:从原理到实战的深度设计指南
gan·boost·开关电源·1024程序员节·buck·拓扑电路
FinTech老王3 小时前
国产数据库MongoDB兼容性技术分析与实践对比
mongodb·1024程序员节
小雨青年3 小时前
鸿蒙 HarmonyOS 6|ArkUI(03):状态管理
华为·harmonyos·1024程序员节
墨理学AI4 小时前
Kylin Linux Advanced Server V10 上成功安装 NVIDIA Container Toolkit
1024程序员节
御承扬4 小时前
编程素养提升之EffectivePython(Builder篇)
python·设计模式·1024程序员节
麦麦大数据4 小时前
F032 材料科学文献知识图谱可视化分析系统(四种知识图谱可视化布局) | vue + flask + echarts + d3.js 实现
vue.js·flask·知识图谱·数据可视化·论文文献·1024程序员节·科研图谱
gs801404 小时前
pnpm + webpack + vue 项目依赖缺失错误排查与解决
pnpm·1024程序员节