C++ 多态讲解

多态的概念

多态其实就是多种形态,当不同对象做相同的事情时,得到的不同的状态。多态其实可以分为两个,一个静态式多态,一个动态式多态。函数重载就是静态式多态的一种,在编译器期间就确定了程序的行为。而动态多态是在程序运行期间,根据自己拿到的类型确定程序的具体行为。简单来说就是一个提前知道要干嘛,一个边走边看。多态的前提是继承。

多态的定义及实现

class Person
{
public:
    //用关键字virtual修饰的成员函数叫做虚函数
    virtual void BuyTicket()    {cout << "Person--买票-全价" << endl;}
};
class Student : public Person
{
public:
    virtual void BuyTicket()    {cout << "Student--买票-半价" << endl;}
};

这个代码中有三个类,我们可以看到这两个函数名字相同,那么它是否是构成了隐藏。这里是不构成隐藏的,函数名前加了个virtual 表示这是一个虚函数,这两个函数构成重写(也叫做覆盖)。

重写的条件(重写是重写实现)

1.要有关键字virtual来修饰

2.满足三同(函数名字相同,返回类型相会,参数类型相同)

但是这两个类并不构成多态的条件

构成多态的条件

1.必须是虚函数

2.父类的指针或引用来调用虚函数

//这里用的父类引用
void func(Person& p)
{
    p.BuyTicket();
}
void Test()
{
    Person p;
    Student s;
    func(p);
    func(s);
}

结果:

如果用的不是父类的引用

void func(Person p)
{
    p.BuyTicket();
}

结果:

如果是指针调用

void func(Person* p)
{
    p->BuyTicket();
}

结果:

若父类和子类都不加virtual输出的结果:

普通调用和多态调用

这里其实要讲两种调用,分别是普通调用和多态调用

普通调用:根据调用的类型有关

多态调用:根据指针或者引用指向的对象有关

多态的两个例外

例外一

父类函数前加virtual, 子类不加,也构成多态。

class Person
{
public:
    virtual void BuyTicket()    {cout << "Person--买票-全价" << endl;}
};
class Student : public Person
{
public:
    void BuyTicket()    {cout << "Student--买票-半价" << endl;}
};
void func(Person& p)
{
    p.BuyTicket();
}
void Test()
{
    Person p;
    Student s;
    func(p);
    func(s);
}

结果:

例外二

协变:三同中的返回值可以不同,但是要求返回值必须是父子关系的指针或引用

class Person
    {
    public:
        //void BuyTicket()
        virtual Person* BuyTicket()
        {
            cout << "Person--买票-全价" << endl;
            return this;
        }
    };
    class Student : public Person
    {
    public:
        virtual Student* BuyTicket()
        {
            cout << "Student--买票-半价" << endl;
            return this;
        }
    };

结果:

知识补充

补充一

析构函数建不建议设为虚函数,有如下类:

class A
{
public:
    ~A()
    {
        cout << "~A()" << endl;
        delete[] _pa;
    }
protected:
    int* _pa = new int[5];
};
class B : public A
{
public:
    ~B()
    {
        cout << "~B()" << endl;
        delete[] _pb;
    }
protected:
    int* _pb = new int[10];
};

我们正常使用场景:

void Test()
{
    A a;
    B b;
}

结果:

有如下使用场景:

void Test()
{
    A* ptr1 = new A;
    A* ptr2 = new B;
    delete ptr1;
    delete ptr2;
}

结果:

此时可以看到结果,调用了两次A的析构函数,B的析构函数并没有调用,这种代码会造成内存泄漏

原因:

这里是普通调用,普通调用跟对象的类型有关,这里的对象类型都是A*,那么就只会调用A类

解决办法:

多态调用,多态调用根据指针或引用指向的对象有关,虽然都是A*,但是一个指向父类,一个指向的是子类,那么根据多态,它两就分别能去调用自己的析构函数

如何改为多态调用?

在析构函数前加上virtual,在上篇继承中已经说明析构函数会被特殊处理成destructor函数名,那么子类和父类的析构函数满足三同。

修改代码之后的结果:

补充二

关键字final和override

final关键字有两个作用

一是用来让一个类不能被继承,当我们不想让我们的一个类被继承就可以用这个关键字

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

结果:

二是用来让虚函数不能被重写

class A
{
public:
    virtual void TestCode() final
    {}
};
class B : public  A
{
public:
    virtual void TestCode()
    {}
};

结果:

override关键字的作用是检查该子类虚函数是否被重写

class A
{
public:
    virtual void TestCode(int)
    {}
};
class B : public  A
{
public:
    virtual void TestCode() override
    {}
};

结果:


抽象类

含有纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化对象,子类继承之后也不能实例化,除非重写虚函数,子类才能实例化对象。纯虚函数只需在虚函数后加上 =0

class A
{
public:
    virtual void test_code()=0
    {}
};
class B : public A
{
public:
    //此时的A,B类都不能实例化
    //如果将下面取消注释后那么B可以实例化对象,A依然不可以
    //virtual void test_code()
    //{}  
};

接口继承和实现继承

普通函数的继承就是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,子类继承父类函数的接口,目的是为了重写,达成多态,继承的是接口,如果不是多态,就不要把函数定义成虚函数

多态的原理

计算下面这个类的大小(在·32位平台下)

class A
{
public:
    virtual void func()
    {};
protected:
    int _a;
    char _c;
};

结果:

在没有了解过虚表指针的时候可能大家算的应该都是8,但是这里包含了个指针,因此大小是12

如上图这个指针__vfptr。一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。

class A
{
public:
    virtual void func1()    {cout << "A->func1()" << endl;}
    virtual void func2()    {cout << "A->func2()" << endl;}
    void func3()    {cout << "A->func3()" << endl;}
protected:
    int _a;
};

class B
{
public:
    virtual void func1()    {cout << "B->func1()" << endl;}
protected:
    int _b;
};

int main()
{
    A a;
    B b;
}

上述代码通过调试,我们发现子类b对象中也有一个虚表指针,我们观察上图中,我们的func1完成了重写,所以b的虚表中存的就是b::func1,所以虚函数的重写也叫做覆盖,覆盖就是指虚表中的虚函数的覆盖。其中func3也被继承下来但不是虚函数,因此不会放进虚表。

知识补充

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

总结子类虚表的生成

  1. 先将父类的虚表内容拷贝一份到子类虚表中

  1. 如果子类中重写了父类的某个虚函数,那么子类会将自己的虚函数重写到虚表中的对应位置

  1. 子类中自己的虚函数会依此按声明顺序增加到虚表中

单继承和多继承关系的虚函数表

单继承中的虚函数表

class A
{
public:
    virtual void func1()    {cout << "A->func1()" << endl;}
    virtual void func2()    {cout << "A->func2()" << endl;}
    void func3()    {cout << "A->func3()" << endl;}
protected:
    int _a;
};

class B
{
public:
    virtual void func1()    {cout << "B->func1()" << endl;}
    virtual void func3()    {cout << "B->func3()" << endl;}
    void func4()    {cout << "B->func4()" << endl;}
protected:
    int _b;
};

按理来说,依据上述代码在图中b指向的虚表应该包含func3,但是并没有,这里是编译器做了相关的优化,我们将b类对象的虚表打印出来看看

typedef void(*VFPtr)();
void PrintVFT(VFPtr vft[])
{
    for (int i = 0; vft[i] != nullptr; ++i)
    {
        printf("[%d] : %p->", i, vft[i]);
        vft[i]();
    }
    cout << endl;
}

通过打印的方式观察到:

多继承中的虚函数表

class A
{
public:
    virtual void func1()    {cout << "A->func1()" << endl;}
    virtual void func2()    {cout << "A->func2()" << endl;}
protected:
    int _a;
};

class B
{
public:
    virtual void func1()    {cout << "A->func1()" << endl;}
    virtual void func2()    {cout << "A->func2()" << endl;}
protected:
    int _b;
};
class C : public A, public B
{
public:
    virtual void func1()    {cout << "A->func1()" << endl;}
    virtual void func3()    {cout << "A->func2()" << endl;}
protected:
    int _c;
};

我们可以看到c中继承了两个虚表分别都对func1,func2进行了重写,但是C中的func3是存在A中,还是存在B中,我们可以打印出每个对象的虚表。结果如下

int main()
{
    A a;
    PrintVFT((VFPtr*)(*(void**)(&a)));
    B b;
    PrintVFT((VFPtr*)(*(void**)(&b)));
    C c;
    PrintVFT((VFPtr*)(*(void**)(&c)));
    PrintVFT((VFPtr*)(*(void**)((char*)&c + sizeof(A))));
    //下面这个代码是发生了切片是指针指向C中的B
    //B* ptr = &c;
    //PrintVFT((VFPtr*)(*(void**)ptr));
}

我们可以观察到我们的func3是存放在第一个继承类部分的虚表中。

总结:多继承子类未重写的虚函数放在第一个继承父类部分的虚函数表中

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值