C++多态详解

C++多态详解,第1张

c++多态 多态的概念

概念:通俗的来说就是多种形态,具体就是去完成某个行为,当不同类型的对象去完成同一件事时,产生的动作是不一样的,结果也是不一样的。


举一个现实中的例子:买票这个行为,当普通人买票时是全价;学生是半价;军人是不需要排队。


多态也分为两种:

  • 静态的多态:函数调用

  • 动态的多态:父类指针或引用调用重写虚函数。


这里的静态是指在编译时实现多态的,而动态是在运行时完成的。


多态的定义及实现 构成条件

多态一定是建立在继承上的,那么除了继承还要两个条件:

  • 必须通过基类(父类)的指针或引用调用函数

  • 被调用的函数必须是虚函数,且派生类(子类)必须对积累的虚函数进行重写。


 

虚函数

概念:被virtual修饰的类成员函数称为虚函数

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"全价票"<

注意:

  1. 只有类的非静态成员函数可以是虚函数

  2. 虚函数这里virtual和虚继承中用的是同一个关键字,但是他们之间没有关系;虚函数这里是为了实现多态;虚继承是为了解决菱形继承的数据冗余和二义性,它们没有关联

虚函数的重写

概念:派生类(子类)中有一个跟基类(父类)完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),称子类的虚函数重写了基类的虚函数。


例:

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"全价票"<
 

注意:这里子函数的虚函数可以不加virtual,也算完成了重写,但是父类的虚函数必须要加,因为子类是先继承父类的虚函数,继承下来后就有了virtual属性了,子类只是重写这个virtual函数;除了这个原因之外,还有一个原因,如果父类的析构函数加了virtual,子类加不加都一定完成了重写,就保证了delete时一定能实现多态的正确调用析构函数。


虚函数重写的两个例外

1、协变

概念:派生类重写基类虚函数时,与基类虚函数返回值类型不同。


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

例:

class A{};
class B : public A{};
​
class Person
{
public:
    virtual A* f()
    {
        return new A;
    }
};
​
class Student : public Person
{
public:
    virtual B* f()           //返回值不同但是构成虚函数重写
    {
        return new B;
    }
};

2、析构函数的重写

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


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

例:

class Person {
public:
    //建议把父类析构函数定义为虚函数,这样方便子类的虚函数重写父类的虚函数
    virtual ~Person() {cout << "~Person()" << endl;}
};
​
class Student : public Person {
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。


int main() { Person* p1 = new Person;   //这里p2指向的子类对象,应该调用子类析构函数,如果没有调用的话,就可能内存泄漏 Person* p2 = new Student;    //多态行为 delete p1; delete p2;    //只有析构函数重写了那么这里delete父类指针调用析构函数才能实现多态。


return 0; }

C++11 override和finel

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

final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
    virtual void Drive() final {}
};
class Benz :public Car
{
public:
    //会在这块报错,因为基类的虚函数已经被final修饰,不能被重写了
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};  

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

class Car{
public:
    virtual void Drive(){}
};
class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
};  
重载、覆盖(重写)、隐藏(重定义)的对比

 

抽象类 抽象类的概念

纯虚函数:在虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫接口类,抽象类无法实例化对象。


抽象类的子类不重写父类的虚函数的话,也是一个抽象类。


//抽象类的定义
class Car
{
public:
    virtual void run()=0;   //不用实现只写接口就行。


  };

纯虚函数不写函数体,并不意味着不能实现,只是我们不写。


因为写出来也没有人用。


虚函数的作用

  1. 强制子类重写虚函数,完成多态。


  2. 表示抽象类。


接口继承和实现继承

普通函数的继承就是实现继承,虚函数的继承就是接口继承。


子类继承了函数的实现,可以直接使用。


虚函数重写后只会继承接口,重写实现。


所以如果不用多态,就不要把函数写为虚函数。


纯虚函数就体现了接口函数。


下面我们来实现一道题,展现一下接口继承。


class A
{
public:
    virtual void fun(int val=0) 
    {
        cout<<"A->val = "<val"<Fun();
    return 0;
}

结果打印为 :B->val=0

子类对象切片给父类指针,传给Fun函数,满足多态,会去调用子类的fun函数,但是子类的虚函数继承了父类的接口,所以val是父类的0。


多态的原理 虚函数表
class A
{
public:
    virtual void fun()
    {
        
    }
    protected:
    int _a;
};

sizeof(A)是多少?

打印出来是8。


我们定义了一个A类型的对象a,打开调试窗口,发现a的内容如下

 

我们发现出了成员变量_a以外,还多了一个指针,这个指针是不准确的,实际上应该是 _vftptr(virtual function table pointer),虚函数表指针。


在计算类大小的时候要加上这个指针的大小。


虚表就是存放虚函数的地址地方,当我们去调用虚函数,编译器就会通过虚表指针去虚表里查找。


class A
{
public:
    void fun1()
    {
        
    }
    virtual void fun2()
    {}
};
​
int main()
{
    A* a=nullptr;
    a->fun1();//调用函数,因为这是普通函数的调用
    a->fun2();//调用失败,虚函数需要对指针 *** 作,无法 *** 作空指针。


   return 0; }

实现一个继承

class A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
class B : public A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
​
int main()
{
    A a;
    B b;
    return 0;
}

 

子类与父类一样有一个虚表指针。


子类的虚函数表一部分继承自父类。


如果重写了虚函数,那么子类的虚函数会在虚表上覆盖父类的虚函数。


本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。


所以,如果继承了虚函数,那么

  1. 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。


  2. 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。


  3. 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。


 虚函数表放在内存的那个区,虚函数又放在哪?

虚函数与虚函数表都放在代码段。


多态的原理

我们现在来看多态的原理

class person
{
public:
    virtual void fun()
    {
        cout<<"全价票"<fun();
}

 

这样就实现了不同对象去调用同一函数,展现出不同的形态。


满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。


普通函数的调用是编译时就确定的。


动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。


我们说的多态一般是指动态多态。


这里我附上一个有意思的问题:

就是在子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?

#include 
using namespace std;
​
class Base {
public:
    virtual void func() {
        cout << "Base func\n";
    }
};
​
class Son : public Base {
public:
    void func() {
        Base::func();
        cout << "Son func\n";
    }
};
​
int main()
{
    Son b;
    b.func();
    return 0;
}

输出:Base func

Son func

这是C++提供的一个回避虚函数的机制

通过加作用域(正如你所尝试的),使得函数在编译时就绑定。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/634275.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-16
下一篇 2022-04-16

发表评论

登录后才能评论

评论列表(0条)