C++--12.多态

多态的概念
多态的定义及实现
抽象类
多态的原理
单继承和多继承关系中的虚函数表

多态的概念

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

举个简单的例子,比如我们去买票,普通人去买就是全价买票,学生去就是半价学生票,这就是两种不同的对象去调用买票这个函数时得到的不同结果,又称多态

多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person
Person 对象买票全价, Student 对象买票半价。
那么在继承中要 构成多态还有两个条件
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

我们可以看到,在Person与Student中,都有virtual修饰的BuyTicket函数,其实是子类对父类中的BuyTicket进行了重写,而我们在父类的Fun函数中也用父类的引用调用了我们的虚函数,完成了多态的实现

 虚函数

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

虚函数的重写

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

也就是上图我们的BuyTicket函数,他子类中有一个与父类除过函数体完全一致的虚函数,这就称为子类重写了父类的虚函数

多态的实现

 我们来看这段代码,在我们多态调用中,Func函数的型参类型为Person,但实际上在我们多态中,谁去调就匹配谁的类型,与这里写的无关

 我们再来演示非多态的情况

 我们将多态去除了之后,调用的就都是Person类型了,将指针或者引用去除也是一样的

虚函数重写的两个例外

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

 我们在这里改变了两个虚函数的返回值,分别改成了各自(这里只要是一组满足父子关系的类就可以)的指针(引用也可以),这样也是满足多态的。注意:普通类型不行

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

其实我们不止析构函数,当我们的父类是虚函数,而子类不是虚函数的时候,其实也是可以的,只是这样写并不规范,不建议

 还有一种就是析构函数的重写

 我们可以看到,上面我们没有对析构函数加virtual,在分别创建Person与Student之后,我们发现,在结束时并没有对Studnet进行析构,实际对于p2的过程却是先调用Person,再完成对Student的调用,析构时未对Student进行析构,只对Person进行析构,这就会引起内存泄漏,所以我们需要加上virtual,构成多态,完成析构函数的重写

 这样就加上了对Student的析构,其实我们会有一些疑问,析构函数的函数名也不相同啊,怎么就重写了呢,事实上,编译器在编译后会将析构函数名称都转换为destructor,所以是可以构成重写的

这里我们显示一道题

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: 以上都不正确

这个答案是什么呢?实际上是B,原因是我们的B去继承A时继承了func的返回值,函数名,形参列表,而后再对这个继承过来的函数中的函数体才进行的重写,最后由因为调用的是B中的func,而函数名以及形参列表缺是A的,所以我们得到的结果就是B->1

C++11 override fifinal

从上面可以看出, C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11 提供了 override fifinal 两个关键字,可以帮助用户检测是否重写
1. final:修饰虚函数,表示该虚函数不能再被继承
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {}
};

当我们在virtual函数后加上了final,他就不能被重写,不满足继承了,会产生报错

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
//override检查子类的虚函数是否重写了父类的虚函数
class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

事实上我们如果没有对某个虚函数进行重写,或者我们写的函数并不是重写的,有错误时,此时编译器并不会进行报错,因为它会认为两个函数分别属于不同的类,是可以接受的,但是当我们加上了override关键字时,只要未完成重写,就会报错,想当于加了一个判断闸口

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

 抽象类

概念
在虚函数的后面写上 =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();
}

接口继承和实现继承

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

多态的原理

虚函数表

了解虚函数表之前我们先来看下这段代码

class Base//sizeof(Base)是多少?
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};

当我们以一般类对这个类进行计算时,会得到4的结果,因为类大小=成员变量大小的和(不考虑成员函数,因为成员函数最后都会编译完成放到代码段)

但是因为这个是虚函数,所以在类中还隐藏了一个指向虚函数表的指针,所以正确大小为8

通过观察测试我们发现 b 对象是 8bytes 除了 _b 成员,还多一个 __vfptr 放在对象的前面 ( 注意有些平台可能会 放到对象的最后面,这个跟平台有关 ) ,对象中的这个指针我们叫做虚函数表指针 (v 代表 virtual f 代表 function) 。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析

事实上,我们上面代码的那个指针,叫做虚函数表指针,我们在这里加了一个虚函数,实际内存中虚函数指针是存在一个指针数组中的,第一个函数占[0]位,第二个[1]位,分别存储了每个函数所对应的虚函数表指针

 那么虚函数表指针到底有什么作用呢?其实作用就是存储公共区域--虚函数表位置的指针

 我们见识过上面的代码拷下来,进行分析,实际上,在我们main函数调用中,分别创建了Person与Student的实例化对象,并且调用函数,我们会发现,用Person类去调用Func函数时,调用的就是Person的BuyTicket,访问的就是Person中的虚函数地址,而用Student类去调用Func函数时,编译器就会调Student的虚函数表指针去找Student的虚函数地址,指向谁就去谁的虚表里面去找对应的虚函数

 我们可以看到,当我们将虚函数屏蔽掉,使其不构成多态,没有完成重写时,子类的虚函数表指针和父类是一样的,这其实也说明了重写其实是对于函数的覆盖,当没有重写时,函数从父类中继承下来,是一个函数,而完成重写,则是将父类的函数继承下来,将继承下来的函数覆盖完成重写

总结起来就是,在满足多态时,为了分情况的调用子类与父类中相同的虚函数,我们在每个类中添加了一个虚函数表指针,在父类调用函数时,去父类中找父类的虚函数表指针,根据虚函数表指针的地址找到父类的虚函数并且调用,子类同理

 这是在汇编中,多态底层实现的代码

 我们对这一过程再进行复盘

 我们可以看到,我们的子类对Func1函数进行了重写,所以Func1在子父类中虚函数地址不同了,Func为进行重写,所以子类的Func2就是从父类上继承下来的,地址相同,Func3非虚函数,所以不进行指针存储,编译时通过类型确定地址

 我们再来看几个问题

1.虚函数在哪里?在代码段,它与其它函数一样,在编译后都变成指令被存在了代码段。误区:虚函数不是存在虚表,虚表中存的是虚函数的指针

2.虚函数表存在哪?也在代码段,因为我们同类型的对象是公用一个虚表的,所以无法存入栈,因为可能会不断复用的缘故,放入代码段中最合适

我们怎么确定的呢?

 动态绑定与静态绑定

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

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

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

单继承中的虚函数表

我们先来看这样一段代码

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
//void(*p)()定义一个函数指针变量
typedef void(*VF_PTR)();//函数指针类型typedef
//打印虚表->虚表本质是一个虚函数指针数组
void PrintVFTable(VF_PTR* pTable)
{
	for (size_t i = 0; pTable[i] != 0; ++i)
	{
		printf("vfTable[%d]:%p->", i, pTable[i]);
		VF_PTR f = pTable[i];
		f();
	}
}
int main()
{
	void(*p)();
	Base b;
	Derive d;
	//取对象中前四个字节存的虚表指针
	PrintVFTable((VF_PTR*)(*(int*)&b));
	cout << endl;
	PrintVFTable((VF_PTR*)(*(int*)&d));
	system("pause");
	return 0;
}
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指
 // 1.先取b的地址,强转成一个int*的指针
 // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
 // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
 // 4.虚表指针传递给PrintVTable进行打印虚表
 // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面
没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好
了。

 

 我们将虚函数表打印了出来,这是我们的运行结果,我们可以看到的是,B的func1与fun2,D覆盖B的func1,D继承B的func2,以及自己的func3,func4

多继承中的虚函数表

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;
};

//void(*p)(); // 定义一个函数指针变量
typedef void(*VF_PTR)(); // 函数指针类型typedef

// 打印虚表->虚表本质是一个虚函数指针数组
//void PrintVFTable(VF_PTR* pTable)
void PrintVFTable(VF_PTR pTable[])
{
	for (size_t i = 0; pTable[i] != 0; ++i)
	{
		printf("vfTable[%d]:%p->", i, pTable[i]);
		VF_PTR f = pTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	cout << sizeof(Derive) << endl;
	Derive d;

	cout << sizeof(Base1) << endl;

	PrintVFTable((VF_PTR*)(*(int*)&d));
	PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));
	system("pause");
	return 0;
}

我们先来问一个问题,d的大小是多少?这里我们直接进入调试窗口

 我们会发现,在d中,包含了B1与B2的两份虚表,所以大小为16+自己的4=20

 我们可以发现,我们的Base2虚表中是没有func3的,说明如果是多继承,自己的func3是往第一个虚表中放的,也说明了先继承的放在前面

 菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。

再次回顾:

//虚函数:->概念:虚函数重写是多态的条件之一
//        ->多态原理:虚函数地址放到对象的虚表(虚函数表)中,多态指向谁,调用本质是运行到对象虚表中找到要调用的虚函数
//2.虚继承. ->概念:解决菱形继承中的数据冗余和二义性
//         ->原理:将虚基类对象放到公共位置(vs放到整个对象尾部),虚基类中存偏移量,来计算虚基类对象的位置
//总结:这里两个地方都用了virtual关键字,但它们之间没有关联,不要联系到一起  
class A
{
public:
	int a;
};
class B : public A
{
public:
	int b;
};
class C : public A
{
public:
	int c;
};
class D : public B, public C
{
public:
	int d;
};

 我们将内存图调出来,发现我们赋值了两次a,在内存中两块地方分别存储,这体现了数据的冗余,我们调用的是同一个a,但是却可以有两个被赋的值,这体现了数据的二义性

我们解决这个问题的方式就是虚继承

class A
{
public:
	int a;
};
class B : virtual public A
{
public:
	int b;
};
class C : virtual public A
{
public:
	int c;
};
class D : public B, public C
{
public:
	int d;
};

在我们的继承语句中加入virtual关键字,代表了其虚继承的关系

 此时我们的a就被单放到了一个公共的区域,不会再放在B或者C中,而是存在整个数据段的最下方内存,我们的B,C访问时就会访问这个公共的a,我们在虚继承中将A通常叫做虚基类

那么我们的B,C类又是如何去找到这个a的呢?取代开辟它的就是在我们的B,C中会分别创建一个虚基表,虚基表中存的是偏移量(找到那个公共的a所需要的内存偏移大小),用来计算虚基类对象的位置(其实这个位置直接存所需对象的地址也可以,不过我们的编译器选择的是存储偏移量)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值