C++ 超全面讲解多态

这篇文章主要介绍了C++多态的原理与实现,多态是一种面向对象的设计思路,本身和C++不是强绑定的,其他语言当中一样有多态,只不过实现的方式可能有所不同。下面来一起了解更多详细内容吧

多态的概念

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

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

多态也分为两种:

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

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

多态的定义及实现

构成条件

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

  • 必须通过基类(父类)的指针或引用调用函数
  • 被调用的函数必须是虚函数,且派生类(子类)必须对积累的虚函数进行重写。

虚函数

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

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

注意:

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

虚函数的重写

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

例:

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"全价票"<<endl;
    }
};
​
class Student :public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    {
        cout<<"半价票"<<endl;
    }
};
​
class Soldier : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    {
        cout<<"优先买票"<<endl;
    }
};
//多态的实现
void f(Person& p)//这块的参数必须是引用或者指针
{
    p.BuyTicket();
}
​
int main()
{
    Person p;
    Student st;
    Soldier so;
    
    f(p);
    f(st);
    f(so);
    
    return 0;
}

注意:这里子函数的虚函数可以不加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;   //不用实现只写接口就行。   
};

纯虚函数不写函数体,并不意味着不能实现,只是我们不写。因为写出来也没有人用。

虚函数的作用

  • 强制子类重写虚函数,完成多态。
  • 表示抽象类。

接口继承和实现继承

普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,就不要把函数写为虚函数。

纯虚函数就体现了接口函数。下面我们来实现一道题,展现一下接口继承。

class A
{
public:
    virtual void fun(int val=0) 
    {
        cout<<"A->val = "<<val <<endl;
    }
    void Fun()
    {
        fun();
    }
};
​
class B:public A
{
public:
    virtual void fun(int val=1)
    {
        cout<<"B->val"<<val<<endl;
    }
};
​
int main()
{
    B b;
    A* a=&b;
    a->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,代表虚表的结束。所以,如果继承了虚函数,那么

  • 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。
  • 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。
  • 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。

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

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

多态的原理

我们现在来看多态的原理

class person
{
public:
    virtual void fun()
    {
        cout<<"全价票"<<endl;
    }
};
class student : public person
{
public:
    virtual void fun()
    {
        cout<<"半价票"<<endl;
    }
};
void buyticket(person* p)
{
    p->fun();
}

这样就实现了不同对象去调用同一函数,展现出不同的形态。 满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。 普通函数的调用是编译时就确定的。

动态绑定与静态绑定

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

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

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

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

#include <iostream>
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++提供的一个回避虚函数的机制

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

到此这篇关于C++ 超全面讲解多态的文章就介绍到这了,更多相关C++ 多态内容请搜索编程学习网以前的文章希望大家以后多多支持编程学习网!

本文标题为:C++ 超全面讲解多态

基础教程推荐