【C++ ——— 多态】


一、多态概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。不同的人去做这件事结果不一样。

二、多态的定义即实现

2.1 多态的构成条件

在继承中构成多态的两个必须前提有
1. 必须是父类的指针去调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

在这里插入图片描述

2.2 虚函数

虚函数:被即被virtual修饰的类成员函数

class Person {
public:
 virtual void Function() { cout << "山羊" << endl;}
};

2.3虚函数的重写

继承关系父子的两个虚函数,需要符合三同(子类虚函数与父类虚函数的返回值,参数列表,函数名字完全相同
三同例外:协变->返回值可以不同,但是返回值必须是父子类关系的指针或者引用。
补充:virtual只能修饰成员函数。

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

 //子类不加virtual也构成虚函数重写
 //void BuyTicket() { cout << "买票-半价" << endl; }


};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
 return 0;
}

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

1.虚函数中析构函数的重写

我们来观察以下代码:

class Person {
public:
 ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	 Person* p1 = new Person;
	 Person* p2 = new Student;
	 delete p1;
	 delete p2;
}

在这里插入图片描述可以看出即使p2指针指向Student子类对象但是还是会调用~Person的析构函数。
这是因为delete是根据类型去调用的析构函数,p2指针是Person类型的指针。
但是我们期望这里实现的是指向哪个对象,就去调那个对象的析构函数,这里也可以去实现多态,已完成我们想要的功能。

但是需要完成多态又需要完成“三同”的前提,不用担心,编译器在这里对析构函数名字做出了特殊处理,就是为了构成多态。
现在只剩唯一一个条件,那就是这两个函数要都是虚函数。

class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	 Person* p1 = new Person;
	 Person* p2 = new Student;
	 delete p1;
	 delete p2;
}

在这里插入图片描述
现在就完成了析构函数的重写。
继承和多态中的析构函数一定是要先析构子类对象再析构父类对象。

例题强化记忆:
以下程序的输出结果是什么?

  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;
   }
// A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

解析:
1.B类型指针p在调用A类中test时调用的是A* this,这里是因为test原本是A类对象的成员函数,被B类对象继承下来,一样还是A类对象的函数。
2.A* this -> func()[所以这里构成父类指针调用虚函数,这里是多态调用,],注意这里A和B的func我们认为参数是相同的[三同我们看的是形参的类型]
3.又因为p指针指向的是B类对象所以这里调用的是B类对象的func().
所以按一般来说我们都会选D选项B->0,
但是这道题选B选项B->1.
为什么呢?
因为虚函数的重写继承的是父类对象函数的声明,
重写的是函数的实现。

2.重载、重写(覆盖)、重定义(隐藏)的区别

在这里插入图片描述

2.4 C++11 override 和 final

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

class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};

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

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

三、抽象类

3.1抽象类概念

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

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;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}

3.2 接口继承和实现继承

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

四、多态的原理

4.1虚函数表

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

在这里插入图片描述
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr(是一个虚函数指针数组)放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function在计算类的大小时,只要这个类有虚函数我们就要考虑,这个类有虚函数表的问题。
在这里插入图片描述
虚函数编译以后在内存中储存在代码段中。只是单独把地址放到了虚函数表中。

下面Base类中有两个虚函数, 又一个派生类继承了Func1的虚函数,我们可以观测虚函数表中怎么存放虚函数的地址

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func1()" << endl;
	}

private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1" << endl;
	}
};
int main()
{
	//cout << sizeof(Base) << endl;
	Base b;
	Derive d;
}

在这里插入图片描述
我们通过调试可以发现以下几点:
1.虚表指针实际上储存的是,虚函数的地址

2.派生类d中只有一个由父类继承下来的虚表指针(父类和子类的虚表不是同一个,可以理解为子类的虚表是拷贝过来的),不过我们对虚函数Func1做出重写操作时,d类中的Func1地址就会改变,所以d的虚表是继承了b的 Func2 地址和重写 Func1 地址。所以这也就是为什么函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。
3.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

总结以下派生类中的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中。

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

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

补充:同一个类的虚函数表是一样的,共用一张虚表

4.2虚表内存的存储位置

堆 栈 静态区 代码段
虚函数存储在哪里?
虚函数和普通函数一样,都是存储在代码段,同时把虚函数地址存了一份到虚函数表。
虚函数表存储在哪?
我们想要获取虚函数表的地址该怎么操作呢?(虚表在vs环境下放在对象的头四个字节上)
我们可以用以下操作

//Base是一个包含虚函数的类
Base b1;
printf("虚表地址:%p", *((int*)&b1));

*((int*)&b1)该怎么分析呢:&b1本来是Base整个对象的地址,由int强转之后变成了只指向Base对象开头四个字节的地址,然后再解引用就取到了Base整个对象前四个字节的数据,也就是去到了虚函数表的数据。

虚函数表存储在代码段(常量区)
且补充:所有的虚函数一定是存在虚函数表的。

4.2多态的原理

还记得这里Func函数传Person调用的Person::BuyTicket传Student调用的是Student::BuyTicket
在这里插入图片描述

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

class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
 return 0;
}

1.观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
2.观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。(此时是父类对象指针调用子类对象,会发生切片因为上面代码类中只有虚函数,也就是把虚函数表切出来,但是此时,虚函数表已经是子类对象拷贝过来的虚函数表了)
3.这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

在这里插入图片描述

思考:
为什么Func不能用父类对象传参只能用父类对象指针或引用传参
指针和引用传参:
Person* p = johnson指向子类对象时,是把子类对象中父类那一部分切割出来。
对象传参:
Person p = johnson 切割出子类对象父类中那一部分,成员拷贝给父类,但是不会拷贝虚函数表指针。(如果虚函数表指针拷贝过去就会产生一些问题:1.多态调用时,指向父类,调用的还是父类吗?[如果父类被子类虚函数表赋值过,里面有可能是子类的虚函数表]2.析构函数可能会被调错,因为析构函数也在虚表里面)

4.3 动态绑定和静态绑定

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

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

在多继承关系中我们主要关注的是子类的虚函数表,因为父类的虚函数表在虚函数表中不会跟单继承有多少区别。

5.1多继承中的虚函数表。

我们可以观察多继承中Derive子类的虚函数表。
那么我们该如何查看子类中的虚表呢?

1.首先就是先找到虚表指针。虚表指针一般都在对象的首四个字节处。于是我们可以取出对象的头4个字节,就拿到了虚表的指针
2.取到这个虚表指针之后,往后遍历,直到遇到nullptr为止


class Base1 {
public:
	 virtual void func1() {cout << "Base1::func1" << endl;}
	 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 	 int b1;
};
class Base2 {
public:
	 virtual void func1() {cout << "Base2::func1" << endl;}
	 virtual void func2() {cout << "Base2::func2" << endl;}
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	 virtual void func1() {cout << "Derive::func1" << endl;}
	 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 	int d1;
};
typedef void(*VFPTR) ();//对函数指针进行typedef
void PrintVTable(VFPTR vTable[])
{
	 cout << " 虚表地址>" << vTable << endl;
	 for (int i = 0; vTable[i] != nullptr; ++i)
	 {
	 printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
	 VFPTR f = vTable[i];
	 f();
	 }
	 cout << endl;
}
int main()
{
	 Derive d;
	 				  //为什么要转成VFPTR*类型,因为要与上面PrintVTable函数传参 
	 VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	 PrintVTable(vTableb1);
	 VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
	           							//sizeof(Base1)是跳过了Derive类中base1的大小,去访问Base2的虚函数表。记得要强转为char*类型。
	           							//也可以强转为Base1*再+1
	 //这个写法也OK         							
	 //VFPTR* ptr = &d;
	 PrintVTable(vTableb2);
	
	 return 0;
}

在这里插入图片描述
通过执行后观察调试,我们会发现,Derive在继承两个父类后会产生两个虚表。
那么Derive类中的Func3函数放在哪了?(注意这里的调试窗口看不到Func3函数的地址不是因为没有,只是在vs环境下派生类中的虚函数几乎不会放到虚表里面。)

他的对象模型大概是像下图所展示的一样。
在这里插入图片描述
该子类的虚函数就会放在第一张虚表中

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值