【C++闯关笔记】详解多态
系列文章目录
上一篇笔记:【C++闯关笔记】map与set底层:二叉搜索树-CSDN博客

文章目录
目录
系列文章目录
文章目录
前言
一、多态是什么?
1.多态的概念
2.多态实现条件
二、多态的一些细节
1.虚函数的传染性与 override
2.虚函数重写的一些其他问题
1)多态特例:协变
2)析构函数的重写
3)重载、重写、重定义的细节对比
3.纯虚函数与抽象类
二、多态的原理
1.虚函数指针
2.虚函数表
本文总结
前言
作为C++面对对象编程三架马车的最后一架,多态因为站在前两者的肩膀上,所以比之前的内容知识都略显晦涩。本文将深入讨论多态,从知晓多态的概念到深入理解原理再到最后的实践运用。
一、多态是什么?
1.多态的概念
多态的概念:简单来说,就是多种形态,分为编译时能明确的称为为静态,运行时才能明确的角动态。即多态分为编译时多态(静态多态)和运行时多态(动态多态)。
编译时多态(静态多态):函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态。
运行时多态(动态多态):执行某个函数(行为)时,不同的对象会有不同的结果发生。比如买票这个行为:同样是买票这个行为,成人买成人票,儿童买儿童票,学生买学生票。
静态多态函数重载和函数模板在之前的笔记中已经介绍过,这里就不再赘述,本文主要介绍动态多态。
2.多态实现条件
多态在C++中是一个继承关系的特殊情况,当父类对象与子类对象去调用同一函数,会产生不同的行为。
实现多态有三个必要条件
①类与类之间满足继承关系;
②被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖;
③必须是基类的指针或者引用调用虚函数。
让我们来解释一下:
①虚函数:类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。
class A
{
public: virtual void test1(){cout << "A: test1" << endl;}
private:int _a;char _b;
};
②重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即函数的返回值类型、函数名字、参数类型与个数完全相同)但函数体实现的行为不同,从而隐藏了基类的版本,称派生类的虚函数重写了基类的虚函数。
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关键字,但这带来了一些潜在问题:
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 关键字。
class Derived : public Base
{
public://使用override,让编译器帮我们检查void tset(int x) override //// 编译器会报错{ cout << "Derived::test(int)" << endl;}
};
所以派生类中:
-
可以省略
virtual关键字(因为会自动继承虚特性) -
但一定要使用
override关键字 -
对于不希望被进一步重写的函数,可以在基类中可以添加
final关键字。
class A
{
public: virtual void test2()final{cout << "A: test2" << endl;}
private:int _a;char _b;
};
2.虚函数重写的一些其他问题
1)多态特例:协变
原本多态要求重写虚函数时,虚函数的返回值类型要一样,但却有个例外:协变。
协变,即派生类重写基类虚函数时,与基类虚函数返回值类型是不同的:基类虚函数返回基类类型(或其指针/引用),派生类虚函数返回派生类类型(或其指针/引用),并且这些类型之间存在继承关系,这就构成了协变。
关键限制:必须是指针或引用类型,不能是按值返回。
使用场景与用法如下
引入协变前:
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);
}
引入协变后:
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,则这个函数为纯虚函数,纯虚函数可以有定义实现(实现没啥意义因为要被派生类重写),但一般只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。
class A
{
public: virtual void test() = 0;
public:char _a;int _b;
};
二、多态的原理
1.虚函数指针
下面代码的输出结果是?
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++多态机制,重点分析动态多态的实现原理。
本文先介绍了多态的概念与实现条件,之后介绍了多态的一些细节如协变、析构函数重写、纯虚函数等,最后通过虚函数表解开多态的原理。
读完点赞,手留余香~
