【C++】多态与虚函数

虚函数

virtual只能存在于类定义的函数声明中,定义函数的时候不用

image-20200605123120968

多态

多态的表现形式一

派生类的指针可以赋值给基类指针。

通过基类指针调用基类和派生类中的同名虚函数时,如果指针指向的是基类对象,则调用基类的虚函数;反之,调用派生类的虚函数

image-20200605123422998

多态的表现形式二

派生类的对象可以赋值给基类引用

通过基类引用调用基类和派生类中的同名虚函数时,若该引用指向的是一个基类对象,那么调用基类虚函数;反之,调用派生类的虚函数。

image-20200605123628124

多态的作用

增强了程序的可扩充性。

构造函数和析构函数中调用虚函数,不是多态,。在编译时就可以确定。调用的函数是自己的类或者基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。我们不建议这样做,因为:1. 构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别。2.即使构造函数或者析构函数如果能成功调用虚函数, 程序的运行结果也是不可控的

image-20200605124050210

在非构造、非虚构函数的成员函数中调用虚函数,是多态

image-20200605124002079

关于上述两种情况的例子

image-20200605171943676

一点解释:第一次在构造函数中调用虚函数因为son中有hello这个虚函数,所以直接调用自己的;最后的析构函数中,先执行groundson的析构函数,再执行son的析构函数,因为son本身不含虚函数,所以调用基类的虚函数,也就是“bye from myclass”。


image-20200605123847082


派生类和基类中同名的虚函数,不用加virtual关键字

虚函数的访问权限

image-20200605174931244

多态的实现原理(虚函数表)

image-20200605175021655

image-20200605181316835

每一个有虚函数的类(或有虚函数的派生类)都有一个虚函数表,实例化一个对象,该对象的第一个存储单元存放的必定是虚函数表的指针p_table,这个指针指向虚函数表table,而虚函数表table中存放的是对应的各种虚函数的地址。当一个类调用通名虚函数时,就去虚函数表中找对应类的虚函数。

image-20200605181158879

image-20200605181245010

代码

更换类指针指向的东西(全部,虚函数表+类成员)

注意:其中的拷贝赋值函数“* p2=* p1”,是一个深拷贝,因为经过测试,指向的完全是两块内存。

关于深拷贝、浅拷贝,参考我的这篇博客

/**
 *  测试替换虚函数表以后的类
 * */

#include <iostream>

using namespace std;

class A{
    public:
        A(){b=44;}

        virtual void Func(){
            cout<<"A::Func"<<endl;
        }


        int b;

        void plus()
        {
            b+=1;
        }

};          // class A

class B:public A
{
    private:

    public:
        B();
        ~B();

        virtual void Func(){
            cout<<"B::Func"<<endl;
        }

        virtual void func(){
            cout<<"B::func"<<endl;
        }
};

B::B(/* args */)
{
    b=55;
}

B::~B()
{
}

int main()
{
    A a;
    A* pa=new B();
    cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;
    pa->Func();

    // 使用long long类型的指针对象,因为long long对象正好占8个字节
    long long* p1=(long long*)&a;           // 指针对象,存放虚函数表的地址
    long long* p2=(long long*)pa;
    cout<<"\nobject address:\n";
    cout<<"&a: "<<&a<<", "<<"pa: "<<pa<<endl;
    cout<<"p1: "<<p1<<", "<<"p2: "<<p2<<endl;
    // cout<<"sizeof(address): "<<sizeof(p2)<<endl;

    cout<<"\nVtable address:\n";
    cout<<"*p1 Vtable: "<<*p1<<", "<<"*p2 Vtable: "<<*p2<<endl;           // 虚函数表的地址
    cout<<"\nAfter changed Vtable address:\n";
    *p2=*p1;                                // 将基类的虚函数表的地址赋值给派生类的虚函数表地址
    cout<<"*p1 Vtable: "<<*p1<<", "<<"*p2 Vtable: "<<*p2<<endl;           // 虚函数表的地址

    pa->Func();

    // cout<<sizeof(A)<<","<<sizeof(B)<<endl;
    // cout<<sizeof(*pa)<<endl;
    cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;

    a.plus();
    cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;
    
    pa->plus();
    cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;

    return 0;

}
/* 
a.b: 44, pa->b: 55
B::Func

object address:
&a: 0x7ffff0f827b0, pa: 0x7fffe95f3e70
p1: 0x7ffff0f827b0, p2: 0x7fffe95f3e70

Vtable address:
*p1 Vtable: 140703602580792, *p2 Vtable: 140703602580760

After changed Vtable address:
*p1 Vtable: 140703602580792, *p2 Vtable: 140703602580792
A::Func
a.b: 44, pa->b: 55
a.b: 45, pa->b: 55

 */

为什么需要虚析构函数?

参考:C++中虚析构函数的作用及其原理分析

因为在删除派生类对象的时候,我们希望可以和构造类似,先调用派生类的析构,在调用基类的析构。

但是实际上当删除基类指针指向的额派生类对象时,仅仅调用了基类的析构函数数。
这样会造成内存泄露。

#include <iostream>
using namespace std;


class son
{
private:
    /* data */
public:
    son(/* args */);
    ~son();
};

son::son(/* args */)
{
}

son::~son()
{
    cout<<"~son"<<endl;
    
}

class groundson : public son
{
private:
    /* data */
public:
    groundson(/* args */);
    ~groundson();
};

groundson::groundson(/* args */)
{
}

groundson::~groundson()
{
    cout<<"~goundson"<<endl;
}

int main(int argc, char const *argv[])
{
    son* p = new groundson();
    delete p;
    return 0;
}

/* 
输出:
~son
仅调用了基类的析构函树
*/

如何解决?

答:将基类的析构函数定义为虚函数。

#include <iostream>
using namespace std;


class son
{
private:
    /* data */
public:
    son(/* args */);
    virtual ~son();
};

son::son(/* args */)
{
    cout<<"son"<<endl;
}

son::~son()
{
    cout<<"~son"<<endl;
    
}

class groundson : public son
{
private:
    /* data */
public:
    groundson(/* args */);
    ~groundson();
};

groundson::groundson(/* args */)
{
    cout<<"groundson"<<endl;
}

groundson::~groundson()
{
    cout<<"~goundson"<<endl;
}

int main(int argc, char const *argv[])
{
    son* p = new groundson();
    delete p;
    return 0;
}

/* 
输出:
son
groundson
~goundson
~son
*/

推荐

  1. 作为基类使用的类,将析构函数设置为虚函数。
    解释一下:当然我们为了保险起见,所有的类的析构函数都设置成虚函数是最好的,但是虚函数是借助虚函数表作用的,这会占据内存空间。因此,只有当一个类作为基类,才将析构函数设置为虚函数.

  2. 一个类中有虚函数,那么将其析构函数设置为虚函数。
    解释:有虚函数,就表明这个类将来会成为父类,同上。

纯虚函数和抽象类

image-20200605183534505

image-20200605183548994

纯虚函数:没有函数体的虚函数

抽象类:包含纯虚函数的类

抽象类不能创建对象,只能作为基类指针或基类引用指向派生类对象。

在抽象类的成员函数内部可以调用纯虚函数(多态),但是在构造函数或析构函数内部不能调用纯虚函数>(因为非多态,在构造函数或者析构函数内部调用同名的虚函数,其结果,一定是当前类的虚函数,但是当前类的虚函数又是纯虚函数,连函数体都没有,所以不能调用)。

如果一个类中从抽象类派生而来,那么只有当他实现了基类中所有的纯虚函数,他才能称为非抽象类。

image-20200605184551841

纯虚函数:没有函数体的虚函数

抽象类:包含纯虚函数的类

抽象类不能创建对象,只能作为基类指针或基类引用指向派生类对象。

在抽象类的成员函数内部可以调用纯虚函数(多态),但是在构造函数或析构函数内部不能调用纯虚函数(因为非多态,在构造函数或者析构函数内部调用同名的虚函数,其结果,一定是当前类的虚函数,但是当前类的虚函数又是纯虚函数,连函数体都没有,所以不能调用)。

如果一个类中从抽象类派生而来,那么只有当他实现了基类中所有的纯虚函数,他才能称为非抽象类。

image-20200605184551841

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Tech沉思录

点赞加投币,感谢您的资瓷~

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

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

打赏作者

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

抵扣说明:

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

余额充值