2.5 虚函数重写特殊场景

在这里插入图片描述

2.5.1 协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。属于父子类继承关系即可。

即基类虚函数返回值基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A{};
class B : public A {};

class Person 
{
    public:
   // virtual A* f() {return new A;}
    virtual Person* f(){return new Person;}
};
class Student : public Person
{
    public:
    //virtual B* f() {return new B;}
    virtual Student* f(){return new Student;}
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

2.5.2 析构函数重写

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字还是析构函数名不同,都与基类的析构函数构成重写。

虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor函数名

多态及其析构函数考察(常考)

class Person
{
    public:
     ~Person() {cout << "~Person()" << endl;}
};

class Student : public Person 
{
    public:
     ~Student() { cout << "~Student()" << endl; }
};

int main()
{
    Person* p1 = new Person;
    Person* p2 = new Student;
    delete p1;
    delete p2;
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

分析问题

如果是单纯定义Person p,Student s话,不管析构函数是不是析构函数不重要,也不会出现什么问题。但是对于上面这种父类指针类型指向开辟父类或者子类的空间,如果Person和Student析构函数没有完成虚函数的重写就会出现问题

Person 的析构函数不是虚函数,因此,如果你使用 delete p2; 释放 p2 指向的 Student 对象,只会调用 Person 类的析构函数,而不会调用 Student 类的析构函数。这样会导致Student 对象中的资源没有得到正确释放,重复析构是一种未定义的行为,可能导致内存泄漏或者程序崩退。

具体说明:

如果析构函数没有完成虚函数的重写,会根据对象的类型,去调用析构函数,因为编译器只知道这个是一个指向基类的指针。就会调用基类的析构。

这里从底层来看,编译器只能通过指针类型去推断调用对应的析构函数

2.5.3 派生类可以不加virtual

重写基类虚函数,派生类虚函数不加virtual关键字修饰,可以构成重写。由于继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,但是该种写法不是很规范,不建议这样使用。

2.6 大坑题(深度理解)

在这里插入图片描述

具体解析:

三、C++ 11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但事实有些情况下**由于疏忽,可能会导致无法构成重写,而着这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结构才来debug会得不偿失。**对此C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

引入场景:让父类不能被子类继承

C++98方法:父类构造函数设置为私有,子类的构造无法生成和实现,导致子类对象无法实例化。

class Car
{
    public:
    
    private:
   	Car(){}
};
class Benz :public Car
{
    public:
};
int main()
{
 	Benz b;
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

C++11方法:采用final关键字表示最终类

3.1 final与override使用

final:修饰虚函数,表示该虚函数不能再被重写。通俗一点比喻:老爹不给你留家底了,想子类体会下人生。

class Car
{
    public:
    virtual void Drive() final {}
};
class Benz :public Car
{
    public:
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

在这里插入图片描述

override:检查派生类虚函数是否重写基类某个虚函数,进行语法检查,如果没有发生重写将会编译报错。注意这个只是检查。

class Car
{
    public:
    virtual void Drive(){}
};
class Benz :public Car 
{
    public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

四、重载、覆盖(重写)、隐藏(重定义)区分

在这里插入图片描述

在这里插入图片描述

关于隐藏与重写语言存在重叠,可以看作重写属于隐藏的自己。因为构成重写需求比隐藏多。

五、抽象类

5.1 抽象类概念

在虚函数的后面写上 = 0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化对象。

派生类继承后也不能实例化出对象。只当重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外虚函数更体现出了接口继承。

class Car
{
    public:
    virtual void Drive() = 0;
};

class Benz : public Car
{
};

int main()
{
    Benz z;
    z.Drive();
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

5.2 抽象类与override区别

六、实现继承与接口继承

实现继承:

接口继承:

对此,如果不实现多态,不要把函数定义成虚函数。

七、多态的原理(重点)

7.1 虚函数表

场景引入:计算sizeof(Base)大小

class Base
{
    public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
    private:
    int _b = 1;
};

结果:sizeof(Base) == 8
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

在这里插入图片描述

具体解析:

7.2 派生类继承基类成员

class Base
{
    public:

    virtual void Func1()
    {
        cout << "Base::Func1()" << endl;
    }

    virtual void Func2()
    {
        cout << "Base::Func2()" << endl;
    }
    //非虚函数
    void Func3()
    {
        cout << "Base::Func3()" << endl;
    }
    private:
    int _b = 1;
};
class Derive : public Base
{
    public:
    //虚函数func1的重写
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
    private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

通过观察和调式:

总结派生类的虚表生成:

7.3 多态的原理

在这里插入图片描述

class Person
{
    public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
    public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
    p.BuyTicket();
}
int main()
{
    Person Mike;
    Func(Mike);
    Student Johnson;
    Func(Johnson);
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

7.3.1 基类指针或引用进行调用虚函数理由

对于多态来说,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket。

在这里插入图片描述

具体说明:

7.3.2 不满足多态情况

在这里插入图片描述

如果出现不满足多态的情况,编译链接根据调用对象类型,确定调用函数及其函数地址

小结:

7.3.3 反汇编中情况

void Func(Person* p)
{
    ...
        p->BuyTicket();
    // p中存的是mike对象的指针,将p移动到eax中
    001940DE mov eax,dword ptr [p]
        // [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
        001940E1 mov edx,dword ptr [eax]
        // [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
        00B823EE mov eax,dword ptr [edx]
        // call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
        001940EA call eax
        001940EC cmp esi,esp
}
int main()
{
    ...
        // 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
        mike.BuyTicket();
    00195182 lea ecx,[mike]
        00195185 call Person::BuyTicket (01914F6h)
        ...
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

八、虚函数与虚表存储内存区域

问题:

错误答案:虚函数存在虚表,虚表存在对象中,这里答案是错误的。

接下来我们可以通过打印地址来观察,这样是一种小技巧

int main()
{
    int i = 0;
    static int j = 1;
    int* p1 = new int;
    const char* p2 = "xxxxxxxx";
    printf("栈:%p\n", &i);
    printf("静态区:%p\n", &j);
    printf("堆:%p\n", p1);
    printf("常量区:%p\n", p2);

    Person p;
    Student s;
    Person* p3 = &p;
    Student* p4 = &s;

    printf("Person虚表地址:%p\n", *(int*)p3);
    printf("Student虚表地址:%p\n", *(int*)p4);

    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

从打印结构来看,关于上面两个问题,我们可以得到答案

答案:

九、动态绑定与静态绑定

十、单继承与多继承的虚函数表

10.1 单继承的虚函数表

class Base
{
    public :
    virtual void func1() { cout<<"Base::func1" <<endl;}
    virtual void func2() {cout<<"Base::func2" <<endl;}
    private :
    int a;
};
class Derive :public Base
{
    public :
    virtual void func1() {cout<<"Derive::func1" <<endl;}
    virtual void func3() {cout<<"Derive::func3" <<endl;}
    virtual void func4() {cout<<"Derive::func4" <<endl;}
    private :
    int b;
};
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

调试窗口进行观察(不够准确)

在这里插入图片描述

子类继承了父类虚表,得到了Func2虚函数及其Func1完成了虚函数的重写;问题在于监视窗口观察不到Func3和Func4,这里是编译器的监视窗口故意隐藏。

内存窗口进行观察
在这里插入图片描述

如果通过内存窗口来观察的话,虽然我们可以大致确定就是Func3和Func4虚函数的地址,但是如何证明呢?这里就需要使用到了打印虚表中函数了

10.2 打印虚表中函数

通过调式窗口来看,虚表指针是存储在头4字节上的,虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

如果是函数指针数组话,类型是难以书写,可以使用typedef对于类型重定义typedef void(*VFPTR) (); (这里数组指针和函数指针重定义写法是比较特殊的)

如果需要取头4个字节,能不能直接强转为int类型就行。这里强转是没有用的,只有相同类型才能进行强制类型转化,那么怎么办?

10.2.1 指针高级用法

打印虚表中虚函数地址实现步骤

步骤:

//得到数据,重新定义个函数指针数组
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);

VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

打印虚表中虚函数地址函数逻辑:

void PrintVTable(VFPTR vTable[])
{
    // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

在这里插入图片描述

10.3 多继承中虚函数表

class Base1 
{
    public:
    virtual void func1() {cout << "Base1::func1" << endl;}
    virtual void func2() {cout << "Base1::func2" << endl;}
    private:
    int b1;
};
class Base2 
{
    public:
    virtual void func1() {cout << "Base2::func1" << endl;}
    virtual void func2() {cout << "Base2::func2" << endl;}
    private:
    int b2;
};
class Derive : public Base1, public Base2
{
    public:
    virtual void func1() {cout << "Derive::func1" << endl;}
    virtual void func3() {cout << "Derive::func3" << endl;}
    private:
    int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
int main()
{
    Derive d;
    VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
    PrintVTable(vTableb1);
    VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
    PrintVTable(vTableb2);
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

从上面的可以观察出来,多继承体制中派生类是继承了两张虚表,同时继承下来的虚函数是不同的,至于为什么不放在一张虚表,可以想一下切片,如果只有一个切片,如何实现多态的指向谁调用谁的逻辑呢?

10.3.1 打印多继承中第二张虚表中虚函数的地址

第一种办法

VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1))
                           PrintVTable(vTableb2);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

第一种办法使用指针运算法则进行移动指针指向位置,但是只适应不考虑内存对齐等因素情况下。由于内存对齐等因素,可能会导致会导致指向错误。更加推荐下面通过取地址直接访问的办法

第二种方法:

在这里插入图片描述

十一、菱形继承、菱形虚拟继承

实践种我们不建议设计出菱形继承、菱形虚拟继承,一方面太复杂容易出现问题,另一方面这样的模型,访问基类成员有一定性能损耗。所以继承、菱形虚拟继承继承虚表情况,我们不就不需要看了,一般我们也不需要研究清楚,实践中也很少用,如果需要了解通过下面两篇链接文章。

C++ 虚函数表解析 | 酷 壳 - CoolShell

C++ 对象的内存布局 | 酷 壳 - CoolShell

在这里插入图片描述

11.1 菱形虚拟继承(简单了解)

菱形虚拟继承,每个类都有一个虚函数,除了虚表指针也有我们的虚基表指针。这里虚基表有存储两个偏移量一个是距离虚表的偏移量和距离共享虚基类A的偏移量。

这里由于虚基类A是共享的,B C类的虚函数不能放进去,所以只能单独建立虚表。没有继承父类的虚表,这里是不能利用父类的虚表,不能放放我自己的虚函数,A是共享,派生类单独建立虚表

十二、相关面试题

  1. inline函数可以是虚函数吗?
  1. 静态成员可以是虚函数吗?
  1. 构造函数可以是虚函数吗?
  1. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
  1. 对象访问普通函数快还是虚函数更快?
  1. 虚函数表是在什么阶段生成的,存在哪的?

以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!
请添加图片描述

data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/2302_79177254/article/details/141816848","extend1":"pc","ab":"new"}">>
注:本文转载自blog.csdn.net的是店小二呀的文章"https://blog.csdn.net/2302_79177254/article/details/141816848"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接

评论记录:

未查询到任何数据!