C++多态

简介:

@[TOC]

前言:

共识

  1. 基类对象的指针或者引用调用虚函数时,都以多态的流程执行代码。可以说,虚函数就是为实现多态做准备的。
  2. 同一个类的不同对象,共用同一个虚函数表,且虚函数表最可能存放在常量区/代码段

重载

  1. 要求重载的函数在同一作用域,一般是全局作用域
  2. 函数名,参数相同

重定义

  1. 2个函数分别在基类和子类作用域
  2. 重定义也叫做隐藏,基类与子类的同名函数就构成隐藏
  3. 重定义关注的地方是:函数的声明和实现
  4. 同名成员变量也构造隐藏

重写

  1. 2个函数分别在基类和子类的作用域
  2. 要求三同:函数名/参数/返回值必须相同
  3. 2个函数必须是虚函数
  4. 重写就是一种接口继承(会继续基类的属性,这也就是为什么子类可以不写virtual),关注的是函数的实现。
  5. 当基类指针或者引用调用重写函数,可能构造多态,当子类对象调用重写函数,构成隐藏/重定义

重写的2个特殊情况

  1. 协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
  2. 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

重写的一个特殊点

  1. 重写如果在多态调用中就是

C++11 override 和 final

final

  1. 修饰函数:表明该函数不能被重写。
  2. 修饰类:表明该类不能被继承

override

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

一个例题

class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g5kphQjE-1660721146365)(./%E5%A4%9A%E6%80%81.assets/image-20220817095135938-16607010973161.png)]

多态

概念

不同的子类对象,通过基类指针与引用,调用相同的函数,产生不同的效果。

条件

  1. . 必须通过基类的指针或者引用调用虚函数,即保证调用函数的this指针是基类地址
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  3. 当子类对象赋值给基类对象时,编译器只会将成员变量赋值给基类对象,子类的虚函数不会拷贝给基类对象

应用层分析

在这里插入图片描述

通过基类对象的指针或者引用,调用虚函数,且该虚函数完成重写时,构成多态。

原理层分析

通过设置一个虚函数表,实现多态

在这里插入图片描述

虚函数表

  1. 虚函数指针是在对象构造函数初始化队列中初始化的。
  2. 本质是一个数组,存放的是虚函数的地址

虚函数表打印

class Base {
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }

private:
    int b1;
};
class Derive : public Base {
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d1;
};

//typedef void(*) () VFPTR;
//本来重命名函数指针类型时是这样,但是C++语言规定这种是不行的,需要将VFPTR放到内部
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr;++i)
    {
        printf("第%d个虚函数地址 :%p,->", i, vTable[i]);
        //通过this调用函数是在编译阶段的事情,一旦到内存就没有这种限制
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
int main()
{

    Derive d;
    printf("Derive::func1地址%p\n", &(Derive::func1));
    printf("Derive::func2地址%p\n", &(Derive::func2));
    printf("Derive::func3地址%p\n", &(Derive::func3));

    VFPTR* vTableb1 = (VFPTR*)(*((void**)&d));
    //通过强制转换来获得d中前一个指针大小的字节内容。
    //考虑到不同平台指针大小不同,
    //我们指针指针解引用后会得到对应地址后的指向对象大小的内容
    // 即int *p,p解引用后,得到从指向对象地址也就是变量p中的值
    // 开始的sizeof(int)字节大小的内容
    // 
    // 因此将d的地址强转为二级指针,当引用后,
    // 就可以的到对应地址的sizeof(void*)大小的内容
    // 就可以满足不同平台完成同样的需求
    //最后再强制转换即可
    PrintVTable(vTableb1);
    
}

在这里插入图片描述

可以发现,主动打印的函数地址和虚函数表中的地址是不同?

这是因为VS进行了封装,无论是主动打印的还是虚函数表中的地址都不是真正的函数地址,而是一个指令地址。证明如下

请添加图片描述

多继承关系的虚函数表

  1. 多继承中,子类会将自己独有的虚函数,放到继承声明中的第一个基类的虚函数表中
  2. 基类各自拥有着自己的虚函数表

菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的宝宝,可以去看下面的两篇链接文章。

C++ 虚函数表解析

C++ 对象的内存布局

菱形继承

在发生代码冗余与二义性的地方,通过virtual继承,建立一个虚基表,通过相对偏移量实现基类的共享

菱形虚拟继承

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nxFYnksm-1660721146387)(./%E5%A4%9A%E6%80%81.assets/image-20220817151052587-166072025699917.png)]

  1. 菱形虚拟继承中,因为BC共享同一个基类A,但是A的虚函数表只有一个,那么A中的虚函数表该存放那个是没法确定的。
  2. 通过在D中重写func1来解决该问题。同时BC中的虚函数表存放D的func1

问答题

  1. inline函数可以是虚函数吗?答:可以,不过当进行多态调用时编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
  2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式,是无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
  5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
    数表中去查找。
  6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
相关文章
|
11月前
|
存储 人工智能 编译器
c++--多态
上一篇文章已经介绍了c++的继承,那么这篇文章将会介绍多态。看完多态的概念,你一定会感觉脑子雾蒙蒙的,那么我们先以举一个例子,来给这朦胧大致勾勒出一个画面,在此之前,先介绍一个名词虚函数,(要注意与虚拟继承区分)重定义: 重定义(隐藏)只要求函数名相同(但要符合重载的要求,其实两者实际上就是重载);重定义下:在这种情况下,如果通过父类指针或引用调用函数,会调用父类的函数而不是子类。重定义(或称为隐藏)发生的原因是因为函数名相同但参数列表不同,导致编译器无法确定调用哪一个版本的函数。
201 0
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
1523 0
|
编译器 C++
c++中的多态
c++中的多态
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
206 2
C++入门12——详解多态1
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
205 1
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
265 1
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
206 1
【C++】深度解剖多态(下)
|
存储 编译器 C++
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)