C++多态

多态的概念

多态的概念

通俗来说,多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

举个例子:我们去一些商场买东西,当普通人去买的时候,是按照原价购买的。而会员就能打个折。这就是不同的对象去做同样一件事情,而结果却不一样。

多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Vip继承Person。person对象买东西的时候是原价购买,Vip则是打折后的价格。

要在继承中构成多态还有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

在这里插入图片描述

虚函数

虚函数就是被virtual关键字修饰的类成员函数

class Person
{
public:
    virtual void Buy()
    {
        cout << "原价" << endl;
    }
};
虚函数的重写

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

class Person
{
public:
    virtual void Buy()
    {
        cout << "原价" <<endl;
    }
};

class Vip:public Person
{
public:
    virtual void Buy()
    {
        cout << "半价" <<endl;
    }  
};

int main()
{
    Person ps;
    Vip vip;
    
    Person& rps = vip;
    Person* pps = &vip;
    Person vps = vip;
    
    rps.Buy();
    pps.Buy();
    vps.Buy();
    return 0;
}

在这里插入图片描述

可以看见我们上面代码的执行结果,当用基类的引用或指针指向派生类时。它们调用Buy函数时是调用派生类重写的函数,构成了多态

虚函数重写的两个例外

我们知道要满足多态有两个条件,一个是必须由基类的指针或引用调用虚函数,第二个是派生类必须重写虚函数。而重写虚函数需要派生类的虚函数的返回值、函数名和参数列表都跟基类的虚函数一模一样。而两个例外,一个叫协变,满足协变的虚函数在重写时可以和基类虚函数的返回值类型不一样。另一个是析构函数,即使派生类的析构函数名和基类的析构函数名不同,它们也构成重写。

协变(基类和派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的 指针或者引用,派生类虚函数返回派生类对象的 指针或者引用时,成为协变

class A{};
class B:public A{};

class Person{
public:
    virtual A* f(){return new A};
};

class Vip :public Person{
public:
    virtual B* f(){return new B};
}
析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数的名字不同,看起来违背了重写的规则,其实,这里可以理解为编译器对析构函数做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Person{
public:
    virtual ~Person()
    {
        cout << "~Person()" << endl;
    }
};
class Vip:public Person{
public:
    ~Vip()
	{
    	cout << "~Vip()" << endl;
	}	
};
int main()
{
    Person* p1 = new Person;
    Person* p2 = new Student;
    // 进行delete操作,会自动调用析构函数
    delete p1;
    delete p2;
    return 0;
}

在这里插入图片描述

C++11中的两个关键字 override 和 final

从上面的例子可以看出来,C++对于重写的要求比较严格,但是有些情况下会疏忽,可能会因为返回值类型不同,参数列表不同等原因而无法构成重载,而这种错误在编译期间是不会报错的 。只有在运行时发现得到的不是预期的结果,因此c++11提供了两个关键字override和final,可以帮助用户检测是否重写。

1. final:修饰虚函数,表示该虚函数不能被重写,修饰类,表示类不能被继承

final关键字修饰虚函数

在这里插入图片描述

final关键字修饰类

在这里插入图片描述

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

在这里插入图片描述

重载、覆盖(重写)、隐藏的对比

重载

  • 两个函数在同一作用域
  • 函数名相同,参数不相同

重写

  • 两个函数分别在基类和派生类的作用域
  • 函数名、参数、返回值都必须相同(协变例外)
  • 两个函数必须都是虚函数

隐藏

  • 两个函数分别在基类和派生类的作用域
  • 函数名相同
  • 两个基类和派生类的同名函数不构成重写就是隐藏

抽象类

概念

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

在这里插入图片描述

接口继承和实现继承

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

多态的原理

在这里插入图片描述

通过我们上面的程序,我们发现在Person类的对象中,除了_num成员,还多一个 _vfptr,对象中的这个指针我们叫做虚函数指针表(v代表virtual,f代表function),一个含有虚函数的类中都至少有一个虚函数指针表,因为虚函数的地址要被放到虚函数表中,虚函数表也称虚表。


#include<iostream>
using namespace std;
class Person  // 创建一个person类
{
public:
    virtual void Speak()
    {
        cout << "I can speak" << endl;
    }

    virtual void function1()
    {
        cout << "Person:: virtual function1()" << endl;
    }

    void play()
    {
        cout << "Person:: play()" << endl;
    }
private:
    int _num;
};

class Student : public Person // 创建一个student类,student类继承自person类
{
public:
    virtual void Speak() override
    {
        cout << "student::Speak()" << endl;
    }
private:
    int _sid;
};

int main()
{
    Person p;
    Student stu;
    return 0;
}

通过调试上面的代码,我们可以发现下面的问题:

  1. 派生类对象stu中也有一个虚表指针,stu对象有两部分构成,一部分是父类继承下来的成员,虚表指针也被继承了下来。

  2. 基类p对象和派生类stu对象虚表是不一样的,这里我们发现Speak()完成了重写,所以stu的虚表中存的是重写Student::Speak(),所以虚函数重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。

在这里插入图片描述

  1. function1()函数继承下来后是虚函数,所以放进了虚表,play也继承下来了,但是不是虚函数,所以不会放进虚表

  2. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面都放了一个nullptr

在这里插入图片描述

在这里插入图片描述

  1. 总结一下派生类的虚表生成:

    • 先将基类中的虚表内容拷贝一份到派生类虚表中

    • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

    • 派生类自己新增加的虚函数按其在派生类中声明次序增加到派生类虚表的最后

  2. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针

多态就是,在用子类对象赋值给父类指针时,会把子类的虚表拷贝给父类指针,所以在用父类指针去访问虚函数时,就是从拷贝的派生类虚表中找函数,这就是多态的原理。

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

单继承和多继承关系的虚函数表

单继承中的虚函数表
#include <iostream>
using namespace std;
class Parent
{
	virtual void fun1()
	{
		cout << "Parent::fun1()" << endl;
	}

	virtual void fun2()
	{
		cout << "parent::fun2()" << endl;
	}
};

class Child : public Parent
{
	virtual void fun1()
	{
		cout << "Child::fun1()" << endl;
	}

	virtual void fun3()
	{
		cout << "Child::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Child::fun4()" << endl;
	}
};

// 打印虚表
typedef void(*VPTR)();
void printvptr(VPTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vptr[%d] %p->", i, table[i]);
		VPTR f = table[i];
		f();
		//cout << endl;
	}
}

int main()
{
	Parent p;
	Child ch;
	cout << "基类的虚表:" << endl;
	printvptr((VPTR*)(*(int*)&p));
	cout << "派生类的虚表:" << endl;
	printvptr((VPTR*)(*(int*)&ch));
	system("pause");
	return 0;
}

在这里插入图片描述

通过执行上面的代码我们可以看到,子类重写父类的方法后,会将重写后函数的地址覆盖原来的地址,没有重写的fun1,它的地址就是从父类继承过来的地址。而子类自己的虚函数,也会加到虚表后面。

多继承中的虚函数表
#include<iostream>
using namespace std;
class Father
{
	virtual void ffun1()
	{
		cout << "Father::ffun1()" << endl;
	}

	virtual void ffun2()
	{
		cout << "Father::ffun2()" << endl;
	}
};

class Mather
{
	virtual void Mfun1()
	{
		cout << "Mather::Mfun1()" << endl;
	}

	virtual void Mfun2()
	{
		cout << "Mather::Mfun2()" << endl;
	}
};
// 创建一个子类,继承了Father类和Mather类
class Child : public Father, public Mather
{
	virtual void ffun1()
	{
		cout << "Child::ffun1()" << endl;
	}

	virtual void Mfun1()
	{
		cout << "Child::Mfun1()" << endl;
	}
	virtual void Cfun()
	{
		cout << "Child::Cfun()" << endl;
	}
};

// 打印虚表
typedef void(*VPTR)();
void printvptr(VPTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vptr[%d] %p->", i, table[i]);
		VPTR f = table[i];
		f();
		//cout << endl;
	}
}

int main()
{
	Father f;
	Mather m;
	Child c;
	cout << "打印Father的虚函数表" << endl;
	printvptr((VPTR*)(*(int*)&f));
	cout << "打印Mather的虚函数表" << endl;
	printvptr((VPTR*)(*(int*)&m));
	cout << "打印Child的虚函数表" << endl;
    // 从Mather继承的虚表
	printvptr((VPTR*)(*(int*)((char*)&c + sizeof(Father))));
    // 从Fahter继承的虚表
	printvptr((VPTR*)(*(int*)&c));

	return 0;

}

在这里插入图片描述

因为child是多继承,它继承了Father类和Mather类,所以它有两个虚表

在这里插入图片描述

通过这张虚函数表,我们可以看出,多继承的情况下,派生类自己得虚函数地址是放到第一个继承得虚表中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_yiyi_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值