C++?多态!!!

一、引言

大家都知道,C++具备封装、继承和多态这三大特性,此前的文章已经对封装和继承进行了详尽阐释,今天我们要一同探究多态相关知识。若想了解封装、继承的相关内容,可跳转至以下链接:

1、封装:C++?类和对象(上)!!!-CSDN博客

2、继承:C++?继承!!!-CSDN博客

二、多态的概念

1、概念

简单来讲,多态意味着存在多种表现形态。也就是说,当面对不同类型、有着各异特点的对象时,处理同一个问题会采用不同的方式,进而产生不同的结果,这便是多态。

2、分类

实际上,多态细致划分有两种类型,分别是静态多态和动态多态。我们日常所说的多态一般指的是动态多态,这也是我们本次重点要探讨的内容,在深入了解多态的相关知识后,我们再来剖析这两个概念。

3、从实际层面认识多态

前面阐述了多态的概念,我们可以依此为线索,大致列举一些日常生活中常见的多态实际应用:

(1).打滴滴

在打滴滴的时候,新用户往往能享受到较大的优惠幅度。我记得我首次打滴滴时,价格优惠到了4元,那次的路程还挺长的,要是放在现在可能得十元往上。这里就用到了多态相关知识(推测),当新用户和老用户同样调用“打车”接口时,却对应不同的优惠力度,这恰好契合多态的概念。

(2).买票系统

我们日常生活中会进行各式各样的买票操作,比如去各个景点或者购买回家的车票。不难发现,常见的对象在平台上会被划分为不同类别,像普通身份、学生、军人等。当这些对象同样调用买票接口时,普通身份的人会全价买票,学生能半价买票,军人通常可以优先买票,很明显,不同的对象调用同一个接口,产生了不同的效果,这就对应了多态的概念。通过这两个常见事例,我们能体会到多态相关知识在生活中无处不在。

三、多态的定义及实现

1、虚函数

虚函数就是被virtual关键字修饰的函数,例如:

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

2、虚函数的重写

虚函数的重写指的是:派生类中有一个函数与基类的虚函数拥有相同的函数名、函数参数以及函数返回值,那么就称该派生类重写(覆盖)了基类的虚函数。比如:

class Person
{
public:
    virtual void buy_t()
    {
        cout << "全价购票" << endl;
    }
};
class Student : public Person
{
public:
    void buy_t()
    {
        cout << "半价购票" << endl;
    }
};

上述这种情况就可以说Student类重写了Person类中的buy_t函数。

不过需要留意的是,虚函数重写存在以下两个例外情况:

(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;
    Person* p2 = new Student;
    delete p1;
    delete p2;
}
return 0;

在上面的代码中,p1p2都是Person*类型的变量,随后调用delete来释放这两个动态申请的空间,实际上delete对于自定义类型会调用对应类的析构函数,这就出现了一个问题:两个空间都会调用Person的析构函数,这不是我们想要的。我们期望的是p1调用Person的析构函数,p2调用Student的析构函数。

仔细观察我们的需求,好像就是用基类的指针来调用同一个函数,并且希望对于不同的对象能产生不同的效果,这正是我们前面讨论多态时的需求。现在只有一个条件还没满足,就是函数名不一样,所以我们很自然地想到让编译器对析构函数名进行特殊处理,这样当把基类的析构函数写成虚函数时,就能解决上面的问题了。

3、多态的构成条件

多态是在继承关系中,不同的类对象调用同一个函数,会产生不同的行为。比如Student继承了Person,此时Person对象全价买票,Student对象半价买票,所以首先多态存在于继承关系中。

在继承关系中要构成多态还有两个条件:
(1).必须通过基类的指针或者引用调用函数。
(2).被调用的函数必须是虚函数,同时派生类要对基类的虚函数进行重写。

下面是构成多态的一个完整例子:

#include <iostream>
using namespace std;
//多态
class Person
{
public:
    virtual void buy_t()
    {
        cout << "全价购票" << endl;
    }
};
class Student : public Person
{
public:
    void buy_t()
    {
        cout << "半价购票" << endl;
    }
};
void func(Person& rp)
{
    rp.buy_t();
}
int main()
{
    Person p;
    Student s;
    func(p);
    func(s);

    return 0;
}

这段代码的运行结果如下:
C++?多态!!!

四、C++11中提供的两个相关的关键字:overridefinal

通过上面的讲解,我们发现C++中构成重写从而构成多态的过程非常严格,在日常代码工作中很容易出现一些错误,比如大小写问题、字母顺序问题等,这些问题出现时很难发现,因为不会有编译、链接错误,只是没有构成重写,让人很头疼。所以在C++11中提供了overridefinal两个关键字,它们可以帮助我们检查这类问题。

1、final:该关键字有两个作用

(1).修饰虚函数,被修饰的函数不能被重写:
class Person
{
public:
    virtual void buy_t  ()final//final修饰了该函数
    {
        cout << "全价购票" << endl;
    }
};
class Student : public Person
{
public:
    void buy_t()//这个位置会报错:无法重写“final”函数 "Person::buy_t"
    {
        cout << "半价购票" << endl;
    }
};
(2).修饰一个类,被修饰的类不能被继承
#include <iostream>
using namespace std;
//多态
class Person final//使用final修饰这个类
{
public:
    virtual void buy_t()
    {
        cout << "全价购票" << endl;
    }
};
class Student : public Person//这个位置会报错:不能将"final"类类型用作基类
{
public:
    void buy_t()
    {
        cout << "半价购票" << endl;
    }
};

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

class Person
{
public:
    virtual void buy_t()
    {
        cout << "全价购票" << endl;
    }
};
class Student : public Person
{
public:
    void buy_tx() override//override修饰该函数
        //该位置报错:使用override修饰的函数不能重写基类成员
    {
        cout << "半价购票" << endl;
    }
};

五、对比重载、重写(覆盖)、重定义(隐藏)

C++?多态!!!

六、抽象类

1、概念

在虚函数的函数头之后加上=0,此时该函数被称为纯虚函数,包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写了纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,它更能体现出接口继承。

下面的代码体现了这种接口继承的思想:

#include <iostream>
using namespace std;
//多态
class Person
{
public:
    virtual void buy_t() = 0;

};
class Student : public Person
{
public:
    void buy_t()
    {
        cout << "半价购票" << endl;
    }
};
class Teacher :public Person
{
public:
    void buy_t()
    {
        cout << "十倍价钱购票" << endl;
    }

};
void func(Person& rp)
{
    rp.buy_t();
}
int main()
{
    Teacher t;
    Student s;
    func(t);
    func(s);

    return 0;
}

下面是以上代码的执行结果:
C++?多态!!!

2、接口继承和实现继承

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

七、多态的原理

1、虚函数表

(1).引入
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
    int _b = 1;
};

我们先通过打印的方式看一下这个问题的结果是多少?

C++?多态!!!
(2).解决问题

可以看到,结果输出了8(这里要强调一下,小编是在x86环境下输出的,环境或平台不同可能会影响结果),这是为什么呢?或许含有虚函数的类对象进行了一些特殊处理?接下来我们通过调试的方法来看一下该类对象模型是怎样的:

C++?多态!!!

经过上面的调试窗口我们知道,原来在Base类中除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这与平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析。

为了符合多态的情景,我们先对上面的代码做出以下改造:

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
    virtual void Func1()
    {
        cout << "Base::Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "Base::Func2()" << endl;
    }
    void Func3()
    {
        cout << "Base::Func3()" << endl;
    }
private:
    int _b = 1;
};
class Derive : public Base
{
public:
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

接下来我们一起观察这个加强版继承体系的类对象模型,从而说明派生类中的虚表有什么不同?

C++?多态!!!

可以观察到:继承之后的d对象模型中分为两个部分,分别是Base部分和自己的成员,而在Base部分中也有一个_vfptr指针,这意味着d不会生成自己的虚表指针,而是以继承的形式沿用了Base类的指针,而两个指针指向的位置是不同的,这就说明两个类的虚表是不同的,实际上确实如此,派生类会首先继承基类的虚表,然后对于重写过的函数将新的函数指针覆盖原来的函数指针,形成属于自己的虚表。

2、多态的实现

通过上面对虚表指针和虚表的认识,我们大概也能明白多态是如何实现的。

实际上,多态的实现原理就是虚表指针存在于父子类中基类的部分,所以必须使用基类的指针或者引用调用(不能直接使用对象调用是因为对象的切片赋值会丢失信息,而指针和引用的切片赋值不会),同时通过虚表指针就能找到虚表,父子类的虚表不同,找到的函数也就不同,这样就实现了多态调用函数。

3、动态绑定与静态绑定

(1). 静态绑定又称为前期绑定(早绑定),在程序编译期间就确定了程序的行为,也称为静态多态,比如函数重载。
(2). 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

八、结语

这就是本期关于多态的全部内容啦,感谢大家的阅读,欢迎各位和晏、亦菲一起交流、学习、共同进步!!!

版权声明:程序员胖胖胖虎阿 发表于 2025年6月26日 上午1:18。
转载请注明:C++?多态!!! | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...