本博客梳理C++面向对象三大特性的多态特性

一、多态的概念

  • 多态就是多种形态,分为编译时多态(静态多态)和运行时多态(动态多态)
  • 编译时多态:函数重载和函数模板,在编译时就完成了实参传给形参的参数配置
  • 运行时多态:运行时调用函数产生的多种形态

二、多态的定义与实现

1.多态的构成条件

(1)必须基类的指针或者引用调用虚函数(这样才能传不同类型的对象)
(2)被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖

2.虚函数

类的成员函数前面加virtual修饰,则这个成员函数被称为虚函数

3.虚函数的重写/覆盖

(1)重写/覆盖:派生类中有一个跟基类完全相同的虚函数(两者返回值类型、函数名、参数列表(只看参数类型,不看缺省值)完全相同【三同】),称派生类的虚函数重写了基类的虚函数
(2)重写基类虚函数时,派生类虚函数不加virtual也构成重写
(3)派生类虚函数即使私有,通过基类指针也能在多态下进行调用——因为:重写写的是虚函数的实现,声明用的是基类的,基类声明公有就能调用了

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

class Student : public Person 
{
private:
    //virtual void BuyTicket()
    void BuyTicket() 
    { cout << "买票-打折" << endl; }
};

void Func(Person& ptr)
{
    ptr.BuyTicket();
}

int main()
{
    Person ps;
    Student st;
    Func(ps);
    Func(st);

    //st.BuyTicket();//此函数要在Student的BuyTicket函数公有时才能调
    return 0;
}

虚函数的重写/覆盖

4.重写/覆盖的相关问题

(1)协变
派生类重写基类虚函数时,基类虚函数返回基类对象的指针or引用,派生类虚函数返回派生类对象的指针or引用,称为协变
(2)析构函数的重写

  • 编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成desturctor
  • 因此基类析构函数加了virtual,派生类析构函数就构成重写
class A
{
public:
    virtual ~A()
    {
            cout << "~A()" << endl;
    }
};
class B : public A {
public:
    ~B()
    {
            cout << "~B()->delete:" << _p << endl;
            delete _p;
    }
protected:
    int* _p = new int[10];
};
// 只有派生类的析构函数重写了基类的析构函数,下⾯的delete对象调用析构函数,才能
// 构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
    A* p1 = new A;
    A* p2 = new B;
    delete p1;
    delete p2;
    return 0;
}

析构函数的重写

(3)override和final关键字

  • C++11提供override【推翻】关键字,可以帮助检测是否重写;若不想让派生类重写这个虚函数,可以用final修饰
    override和final关键字

5.重载、重写、隐藏对比

重载、重写、隐藏对比

三、纯虚函数和抽象类

  • 在虚函数后面加上=0,则函数为纯虚函数,包含纯虚函数的类叫抽象类
  • 纯虚函数不需要定义实现,只要声明(语法上可以定义实现,但没意义,因为要被派生类重写)
  • 抽象类不能实例化出对象,因此派生类不重写纯虚函数也是抽象类,纯虚函数某种程度上强制了派生类进行重写,不重写就没法实例化对象
class A
{
public:
    virtual void func() = 0;
};
class B : public A {
public:
    virtual void func()//派生类要重写纯虚函数,否则派生类也是抽象类 
    {
        cout << "B::func()" << endl;
    }
};

int main()
{
    //error C2259:“A”: 无法实例化抽象类
    A a;

    A* pB = new B;
    pB->func();//抽象类不影响多态使用
    return 0;
}

四、多态的原理

1.静态多态(函数重载)的原理

在Linux下查看两个函数的底层调用,可以看出编译器在区分两个不同的函数时,添加了i和d作为后缀,以示区分,i对应int,d对应double
静态多态的原理

2.动态多态的原理

(1)底层原理
ptr在调用BuyTicket函数时,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向对象的虚函数表中确定对应虚函数的地址,也解释了基类指针/引用既能调用基类虚函数,又能调用派生类虚函数的现象
底层原理

(2)动态绑定与静态绑定
静态绑定:不满足多态条件的函数调用在编译时绑定,编译时就确定函数地址
动态绑定:满足多态条件的函数调用在运行时绑定,运行时到指向对象的虚函数表中找到调用函数的地址
(3)虚函数表
①派生类先把基类的虚表拷贝下来,重写的虚函数地址完成覆盖,然后加上自己的虚函数地址,不会自己在生成虚表指针
②同类型的对象共用一张虚表,不同类型的对象有各自的虚表
③派生类的虚表中包括:a.基类的虚函数地址;b.派生类重写的虚函数地址;c.派生类自己的虚函数地址
④虚表本质是一个存虚函数指针的指针数组
⑤虚函数存在代码段,虚表在VS下存在代码段(常量区)

class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
    string _name;
};
class Student : public Person {
public:
    virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
    string _id;
};
class Soldier : public Person {
public:
    virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
    string _codename;
};
void Func(Person* ptr)
{
    ptr->BuyTicket();
}
int main()
{
    // 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
    // 多态也会发生在多个派生类之间。
    Person ps;
    Student st;
    Soldier sr;

    Func(&ps);
    Func(&st);
    Func(&sr);
    return 0;
}

虚函数表

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐