08.多态与虚函数

1.多态的概念

编译时多态(静态多态):函数重载、函数模版
运行时多态(动态多态):函数接收不同对象时完成不同的行为

2.多态的定义和实现

(1)多态的构成条件

  1. 两个类存在继承关系
  2. 必须指针或引用调用虚函数
    • 并且是基类的指针或者引用
    • 虚函数必须被子类重写或者覆盖
  • 被调用函数必须是虚函数
    代码示例:
#include<iostream>
using namespace std;

class Person{
public:
    virtual void BuyTicket(){
        cout << "全价购买" << endl;
    }
};
class Student:public Person{
public:
    virtual void BuyTicket(){//[1]
        cout << "半价购买" << endl;
    }
};

void fun(Person* ptr){
    ptr->BuyTicket();
}
int main(){
    Person person;
    Student stu;
    fun(&person);
    fun(&stu);
    return 0;
}

运行结果:

全价购买
半价购买

从代码中可以看出,ptr指针调用BuyTicket函数时并没有因为它是Person类的指针而直接调用Person类的函数,而是在运行时调用了ptr所指向的stu对象的BuyTicket函数,而这就建立在满足所有动态的构成条件。

注意:子类的在重写父累;类的虚函数时可以不写"virtual"如 [1]可以写做void BuyTicket

多态场景的一个习题
以下程序输出结果是什么()
A:A->0B:B->1C:A->1D:B->0E:编译出错F:以上都不正确

class A
{ 
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
	virtual void test(){ func();}
};
class B : public A
{ 
public:
	void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->test();
	return 0;
}

这道题的答案选B,首先p是指向B的一个B指针,然后调用test()函数,但因为class B没有重写这个函数,故会调用A类中的函数,又因为这里将指向B类的地址传给了test()的this指针,这样就满足了多态构成条件——父类指针指向子类对象,所以又调用了B中的func(),但调用后会发现,行参是0,为啥输出的是1,这是因为,虚函数的重写只会重写父类的函数体,等于这里val实际的行参是1,故输出了B->1

与其他虚函数的定义不同的是基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,这里并不关心基类的虚函数是否与子类的虚函数的函数是否相同,基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写。
代码示例

class Person{
public:
    virtual void BuyTicket(){
        cout << "全价购买" << endl;
    }
     ~Person(){
        cout << "~Person()" << endl;
    }
};
class Student:public Person{
public:
    virtual void BuyTicket(){
        cout << "半价购买" << endl;
    }
    ~Student(){
        cout << "~Student()" << endl;
        delete[] ptr;
    }

private:
    int *ptr = new int[10];
};
int main(){
    Person* person =new Person;
    Student* stu =new Student;
    delete person;
    delete stu;
}

运行结果:

~Person()
~Student()
~Person()

运行结果表示Person的析构函数被调用了两次,这是因为Student类被调用后会自动在调一次父类析构函数,确保属于父类的成员变量被析构。
如果去掉父类的virtual关键字呢?
那么只会调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
注意:这就是为啥基类中的析构函数建议设计为虚函数。

(3)override和final关键字

override :用于帮助⽤⼾检测是否重写。
final:禁止派生类重写这个虚函数。
代码示例:

class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}

(3)重载/重写/隐藏的对⽐

重载:在一个作用域里,函数名相同,参数不同,参数类型或者个数不同,返回值可以相同也可以不相同。
重写/覆盖:两个虚函数在处于继承关系的不同作用域里,函数名参数返回值相同,协变和析构函数除外。
隐藏:两个函数分别在处于继承关系的父类和子类的不同作用域中,函数名相同,没有构成重写就是隐藏,除函数外父子类的成员变量也构成隐藏。

3.纯虚函数和抽象类

定义:纯虚函数是不需要被父类定义的函数,存在的意义只是为了让子类重写。抽象类是成员函数中包括纯虚函数的类,抽象类无法实例化对象。同样的,如果只是继承抽象类而不对抽象类中的抽象函数进行重写,那子类也只是抽象类,不能进行实例化。

class Car
{ 
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{ 
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{ 
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
	Car car;
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

4.多态的原理

(1)虚函数的指针

下⾯编译为32位程序的运⾏结果是什么()
A.编译报错B.运⾏报错C.8D.12

class Base
{ 
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

结果是12
因为除了存储_b和_ch,还需要存储虚函数表,这个虚函数表用于存储对应虚函数的地址。
多态2

那么什么是虚函数表呢?

(2)虚函数表

#include<iostream>
using namespace std;

class Person {
public:
    virtual void BuyTicket() {
        cout << "全价购买" << endl;
    }
protected:
    string _name;
};
class Student :public Person {
public:
    virtual void BuyTicket() {
        cout << "半价购买" << endl;
    }
private:
    int _id;
};
class Solider :public Person {
public:
    virtual void BuyTicket() {
        cout << "优先购买" << endl;
    }
private:
    string _codename;
};

void fun(Person* ptr) {
    ptr->BuyTicket();
}
int main() {
    Person pe;
    Student st;
    Solider so;
}

多态3

当把student类的虚函数删除结果会有什么不同吗?

#include<iostream>
using namespace std;

class Person {
public:
    virtual void BuyTicket() {
        cout << "全价购买" << endl;
    }
protected:
    string _name;
};
class Student :public Person {
private:
    int _id;
};
class Solider :public Person {
public:
    virtual void BuyTicket() {
        cout << "优先购买" << endl;
    }
private:
    string _codename;
};

void fun(Person* ptr) {
    ptr->BuyTicket();
}
int main() {
    Person pe;
    Student st;
    Solider so;
}

多态4

从第一张图可以看出,当每个子类都对父类的虚函数进行重写,对应的子类对象中都会存放一个二级指针,而二级指针所指向的函数地址也不同。从第二张图可以看出,当子类对象直接继承父类的虚函数时,二级指针所指向函数地址也与父类相同。
而这个二级指针所指向的就是虚函数表,用来存储虚函数的地址。
虚函数表的总结如下:

  • 基类对象的虚函数表中存放基类所有虚函数的地址
  • 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独立的。
  • 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
  • 派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地址三个部分。通过下面的示例说明。
class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
};
class Derive : public Base
{
public :
    // 重写基类的func1
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func1" << endl; }
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

多态5

如图所示,派生类d的虚函数表有存储了三个虚函数地址,func1是重写的父类的func1,func2是继承的父类的func2,而d自己的func3只能通过内存查看。
在了解了虚函数表之后,就能知道多态到底是如何实现了。

(3)多态是如何实现的

还是Student和Person的例子

 class Person{
 public:
     virtual void BuyTicket(){
         cout << "全价购买" << endl;
     }
     string name;
 };
 class Student:public Person{
 public:
     virtual void BuyTicket() {
         cout << "半价购买" << endl;
     }

 private:
     int id;
 };

 void fun(Person* ptr){
     ptr->BuyTicket();
 }
 int main(){
     Person pe;
     Student st;
     fun(&pe);
     fun(&st);
 }

在这里插入图片描述

在这里插入图片描述
从图可以看出满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。

(4)动态绑定和静态绑定
  • 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
  • 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值