C++多态

Posted ℳℓ白ℳℓ夜ℳℓ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++多态相关的知识,希望对你有一定的参考价值。

C++多态

多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个例子:比如说买票,普通人是全价买,学生是半价,退伍军人是优先。

多态的定义与实现

多态的构成条件与虚函数

多态很重要的前提就是先继承。
并且要去用基类的指针或者是引用去调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

#include<iostream>
using namespace std;
class Person 
public:
	virtual void BuyTicket()  cout << "买票-全价" << endl; //成员函数前面加一个virtual就成为虚函数
;
class Student:public Person

public:
	//这里是虚函数的 重写/覆盖
	virtual void BuyTicket()  cout << "买票-半价" << endl; //条件是三同:返回值和函数名还有参数相同
;
int main()

	Person s1;
	Student s2;
	Person* p = &s1;
	p->BuyTicket();
	p = &s2;
	p->BuyTicket();
	return 0;

这里也叫做多态调用。
之前的调用都是普通调用,一直都和对象的类型有关。
多态调用是跟指向的对象有关。
如果改成普通调用就是类型是谁就去调用谁的成员函数,多态调用就是指向的对象是谁就去调用谁的虚函数。

虚函数的重写

子类虚函数可以不加virtual

#include<iostream>
using namespace std;
class Person 
public:
	virtual void BuyTicket()  cout << "买票-全价" << endl; 
;
class Student:public Person

public:
	//这里是虚函数的 重写/覆盖
	void BuyTicket()  cout << "买票-半价" << endl; //只要三同,子类不加virtual也是虚函数

int main()

	Person s1;
	Student s2;
	Person* p = &s1;
	p->BuyTicket();
	p = &s2;
	p->BuyTicket();
	return 0;


不过这里建议都加上virtual。
协变
三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用。

#include<iostream>
using namespace std;
class Person 
public:
	virtual Person* BuyTicket()  cout << "买票-全价" << endl; return this; 
;
class Student:public Person

public:
	virtual Student* BuyTicket()  cout << "买票-半价" << endl;  return this; 
;
int main()

	Person s1;
	Student s2;
	Person* p = &s1;
	p->BuyTicket();
	p = &s2;
	p->BuyTicket();
	return 0;


正常运行。
析构函数的重写

#include<iostream>
using namespace std;
class A

public:
	~A()
	
		cout << "delete s1" << endl;
		delete[] s1;
	
protected:
	int* s1 = new int[20];
;
class B :public A

public:
	~B()
	
		cout << "delete s2" << endl;
		delete[] s2;
	
protected:
	int* s2 = new int[20];
;
int main()

	A a;
	B b;

	return 0;


目前看来确实没什么问题,都是正常调用,来看看如下的情况:

这里导致了内存泄漏,因为析构函数不是虚函数,只能完成普通调用,所以最好在析构面前加一个virtual。

这下子就可以了。
其实子类不加virtual这里更合适,更方便。
所以在实现父类的时候,最好无脑的给析构函数加virtual。

C++11 override 和 final

final
如何实现一个不被继承的类?
C++11提供了一个关键字,类定义的时候加final:

如果放在父类的某个虚函数后面就是不让这个虚函数被重写。

但是这个情况很少见。
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

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

抽象类

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

#include <iostream>
using namespace std;
class A//抽象类

public:
	virtual void add() = 0 ;//纯虚函数
;
class B:public A

public:
	void add()
;
int main()

	B s;//但是A仍然不能实例化
	return 0;


这就是说给某个函数必须进行重写。
抽象类一般用于,比如说车,他是一个概念,但是他有自行车,电动车,跑车等等,然后还被分为好多的品牌,所以车必须要分类出来。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
下面程序输出什么?

#include <iostream>
using namespace std;
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;


首先,创建的是子类对象,子类对象去调用虚函数test(),然后里面是调用func(),这里要注意,是一个多态调用,因为test成员函数是属于A类的,调用func函数是通过this指针去调用(就算是test函数被子类继承了,内部的this指针也不会被更换,还是A类的this指针),并且func函数也进行了重写,在main函数中调用的也是子类对象,所以走向的是B类中的func函数。
这里最让我们疑惑的就是为什么是1不是0,这里就涉及到了只继承接口,所以val的缺省值还是1。
但是子类的缺省参数并不是一点用处都没有,当普通调用的时候这个缺省参数就可以使用了。
再看一个程序:选哪个?

#include <iostream>
using namespace std;
class Base1 
 
public:
	int _b1; 
;
class Base2 

public:
	int _b2; 
;
class Derive : public Base1, public Base2 

public: 
	int _d; 
;
int main() 

	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;

A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
数据模型大概是这样的:

所以选C。
这里注意一下:其实继承的对象在内存里是从下面开始放,因为下面是低地址,上面是是高地址,我们经常能看到一个数组,用数组名+n就能到对应的位置,这就是为什么从低地址放的原因,加就代表要到高地址。

多态原理

虚函数表

先来研究一下这个类的大小:32位环境下

#include<iostream>
using namespace std;
class Base

public:
	virtual void Func1()
	
		cout << "Func1()" << endl;
	
private:
	int _b = 1;
;
int main()

	cout << sizeof(Base) << endl;
	return 0;


这里明明只有一个成员变量,之前说过成员函数并不在类中,可是为什么结果是8呢?

这里多出来了一个_vfptr,这个叫做虚表/虚函数表,里面储存的是虚函数的地址。

#include<iostream>
using namespace std;
class Base

public:
	virtual void Func1()
	
		cout << "Func1()" << endl;
	
	virtual void Func2()
	
		cout << "Func2()" << endl;
	
	void Func3()
	
		cout << "Func3()" << endl;
	
private:
	int _b = 1;
;
int main()

	cout << sizeof(Base) << endl;
	Base a;
	return 0;


原理与动静态绑定

多态的原理一定跟虚表有着千丝万缕的联系。
再来看看完成重写有什么区别;

#include<iostream>
using namespace std;
class Base

public:
	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;
	return 0;


这里虚表也变了,之前重写也可以叫做覆盖,这里就是覆盖的部分。
其实重写只是语法上的,继承了父类的接口,重写了实现部分。覆盖就是覆盖了父类继承过来重写的虚函数的地址。
那么我们这样调用试一下:



多态调用更长。
这里差别就在于,根本不在乎是指向哪里,因为有虚表的存在,如果指向父类就去父类的虚表中找,如果指向子类就去子类的虚表中找。
在汇编当中eax里面存的就是虚表指针数组。

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

那么虚表是放在哪一个位置呢?

打印出来的地址和常量区非常接近,所以是在常量区。

单继承与多继承关系的虚函数列表

单继承的虚函数表

#include <iostream>
using namespace std;
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;
;
int main()

	Base b;
	Derive d;

	return 0;


在VS当中其实并不能看到虚表当中所有的虚函数,这时VS编译器的一个优化,也可以看作是一个BUG。
这个时候我们可以用内存窗口去看。

这里也将func3和func4的函数地址给显示出来,顺便说一下,在VS编译器下,虚表是以空指针结尾的。
但是这样看有些麻烦,我们想个办法给他打印出来。

#include <iostream>
using namespace std;
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;
;
typedef void(*p)();
void PrintVFTbale(p vft[])//打印虚表

	for (int i = 0; vft[i]; i++)
	
		printf("[%d]:%p->", i, vft[i]);//打印虚表当中每个数组的内容,也就是每个虚函数的地址
		vft[i]();//调用对应的函数
	

int main()

	Base b;
	Derive d;
	PrintVFTbale((p*)(*(int*)&b));//将虚表的地址传过去
	PrintVFTbale((p*)(*(int*)&d));
	return 0;


这里还可以改进,因为有时候是64位和32位,到时候64位就是取头8个字节了。

其实只需要将里面的变成二级指针就行了(任何类型的二级指针都可以),因为二级指针是储存一级指针的,解引用之后再去看解引用多大时,剩下的就是一级指针,一级指针就可以根据平台位数变化了,到时候就对应了64位和32位的平台大小了。

多继承的虚函数表

#include <iostream>
using namespace std;
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(*p)();
void PrintVFTbale(p vft[])//打印虚表

	for (int i = 0; vft[i] != nullptr; i++)
	
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	

int main()

	Base1 b1;
	Base2 b2;
	PrintVFTbale((p*)(*(void**)&b1));
	PrintVFTbale((p*)(*(void**)&b2编程中,以显式接口和运行期多态(虚函数)实现动态多态,在模板编程及泛型编程中,是以隐式接口和编译器多态来实现静态多态。
静态多态本质上就是模板的具现化。静态多态中的接口调用也叫做隐式接口,相对于显示接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成,隐式接口通常由有效表达式组成。
 
动态多态和静态多态的优缺点比较:
静态多态
优点:
由于静多态是在编译期完成的,因此效率较高,编译器也可以进行优化;
有很强的适配性和松耦合性,比如可以通过偏特化、全特化来处理特殊类型;
最重要一点是静态多态通过模板编程为C++带来了泛型设计的概念,比如强大的STL库。
缺点:
由于是模板来实现静态多态,因此模板的不足也就是静多态的劣势,比如调试困难、编译耗时、代码膨胀、编译器支持的兼容性
不能够处理异质对象集合
动态多态
优点:
OO设计,对是客观世界的直觉认识;
实现与接口分离,可复用
处理同一继承体系下异质对象集合的强大威力
缺点:
运行期绑定,导致一定程度的运行时开销;
编译器无法对虚函数进行优化
笨重的类继承体系,对接口的修改影响整个类层次;
不同点
本质不同,静态多态在编译期决定,由模板具现完成,而动态多态在运行期决定,由继承、虚函数实现;
动态多态中接口是显式的,以函数签名为中心,多态通过虚函数在运行期实现,静态多台中接口是隐式的,以有效表达式为中心,多态通过模板具现在编译期完成
相同点
都能够实现多态性,静态多态/编译期多态、动态多态/运行期多态;
都能够使接口和实现相分离,一个是模板定义接口,类型参数定义实现,一个是基类虚函数定义接口,继承类负责实现;
 
 
 
待更新!!!
 
可以看看文章:浅谈C++多态性

以上是关于C++多态的主要内容,如果未能解决你的问题,请参考以下文章

c++中为啥要用new 函数()实现多态?

C++中的多态

C++基础知识 易错点 总结(待补)

[C++]面向对象语言三大特性--多态

[C++]面向对象语言三大特性--多态

浅谈C++多态性