C++多态

参考:
https://www.cnblogs.com/nbk-zyc/p/12274178.html
https://blog.csdn.net/studyhardi/article/details/90815766

什么是多态

定义:在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。

多态是设计模式的基础,多态是框架的基础。

在这里插入图片描述
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错;

动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。

为什么要引入动态多态

C++中的父子类间的赋值兼容:子类对象可以当作父类对象使用(兼容性);具体表现为:
1、子类对象可以直接赋值给父类对象;
2、子类对象可以直接初始化父类对象;
3、父类指针可以直接指向子类对象;
4、父类引用可以直接引用子类对象;

当发生赋值兼容时,子类对象退化为父类对象,只能访问父类中定义的成员,可以直接访问被子类覆盖的同名成员;

如下代码示例的结果便说明了这一问题。

#include <iostream>
#include <cstring>
using namespace std;

class Parent {
protected:
    char * name;
public:
    Parent() {
        name = new char[20];
	    strcpy(name, "Parent");
        cout << "Parent(): " << this << " "  << name << endl;
    }
    void func() {
        cout << "Parent()::func()" << endl;
    }
    ~Parent() {
        cout << "~Parent(): " << this << endl;
	    delete[] name;
    }
};

class Child : public Parent {
private:
    int * val;
public:
    Child() {
        strcpy(name, "Child");
	    val = new int (strlen(name));
	    cout << "Child(): " << this << " " << name << endl;
    }
    void func() {
        cout << "Child()::func()" << endl;
    }
    ~Child() {
        cout << "~Child(): " << this << endl;
	    delete val;
    }
};

int main() {
    Parent* bp = new Child();
    cout << bp << endl;
    bp->func(); // 对象是子类,指针是父类,调用的是父类的函数
    delete bp;  // 对象是子类,指针是父类,析构的是父类资源
}
结果输出:
Parent(): 0x7fa771d025e0 Parent
Child(): 0x7fa771d025e0 Child
0x7fa771d025e0
Parent()::func()
~Parent(): 0x7fa771d025e0

在面向对象的继承关系中,子类可以拥有父类中的所有属性与行为;但有时父类中提供的方法并不能满足现有的需求,所以必须在子类中重写父类中已有的方法,来满足当前的需求。

然而尽管实现了函数重写(这里是非虚函数重写),但是在类型兼容性原则中也不能出现我们期待的结果(不能根据指针/引用所指向的实际对象类型去调到对应的重写函数)。

我们期望的行为是 根据实际的对象类型来判断如何调用重写函数(虚函数):
1、即当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;
2、 即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数;

为此就需要动态多态。

动态多态相关概念

多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。

在c++中,实现多态的条件如下:
1、要有继承
2、要有虚函数重写(被 virtual 声明的函数叫虚函数)
3、要有父类指针(父类引用)指向子类对象

其中:
虚函数指的是,在类的成员函数前加virtual关键字的函数。

虚函数的重写(覆盖)指的是,派生类中有一个跟基类的完全相同(函数名/参数/返回值)的虚函数,我们就称子类的虚函数重写了基类的虚函数。

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

在虚函数的后面写上 = 0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。

class Car {
public:
	//纯虚函数
	virtual void Drive() = 0;
};

多态的原理

虚函数表与vptr指针

1、虚函数表是一个存储类成员函数指针的数据结构;
2、虚函数表是由编译器自动生成与维护的;
3、virtual成员函数会被编译器放入虚函数表中;
4、存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针),所以有虚函数的对象比没有虚函数的对象占用内存空间要多一个虚函数指针,另外每次调用函数的时候也要通过虚函数指针查找。

而对于派生类的虚表生成:
1、先将基类中的虚表内容拷贝一份到派生类虚表中;
2、如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类的虚表的最后。

多态执行过程:

1、在类中,用 virtual 声明一个函数时,就会在这个类中对应产生一张 虚函数表,将虚函数存放到该表中;
2、用这个类创建对象时,就会产生一个 vptr指针,这个vptr指针会指向对应的虚函数表;
3、在多态调用时, vptr指针 就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址;

如果这个对象是 子类的对象,那么vptr指针就会在 子类的 虚函数表中查找被调用的函数

如果这个对象是 父类的对象,那么vptr指针就会在 父类的 虚函数表中查找被调用的函数

//1.增加一个派生类Derive去继承Base
//2.Derive中重写Func1
//3.Base再增加一个虚函数Fun2和一个普通函数Fun3
class Base {
	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;
	system("pause");
	return 0;
}

在这里插入图片描述
在这里插入图片描述
以上代码中,派生类对象也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
基类b对象和派生类d对象虚表是不一样的,因为派生类中的Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫覆盖。
另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以不放在虚表中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值