面向对象三大特性之多态(详解)

目录

一.多态的概念

多态

协变

 析构函数的特殊处理

 二.抽象类和override和final

抽象类

接口继承和实现继承

override和final

三.多态的原理

动态绑定和静态绑定

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

虚函数与虚继承

重载,重写和重定义(隐藏)

补充:

一.多态的概念

多态

举例.我们在买票时会有成人票和儿童票,成人票的价格和儿童票的价格不一样,我们需要去区分成人票和儿童票,当然也有可能有其他的票型,这就很麻烦.我们就需要使用多态来处理

概念:当不同对象完成同一个操作是,会产生不同的结果,这就是多态,简单点来说就是,基类指针/引用指向子类对象.
那我们为什么要使用多态呢?(必要性)

原因:我们知道继承和封装都为了实现代码的复用性,多态也可以实现代码复用.还可以解决项目程序中紧耦合的问题,提高程序的可扩展性.代码中尽量使用接口去访问而不是随意使用其成员变量.

多态的好处?

派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性.

那么如何实现多态呢?

实现多态有连两个要点:

  1. 必须通过基类的指针/引用指向子类对象去调用虚函数
  2. 被调用的函数必须是虚函数,而且基类的虚函数被子类重写.

 虚函数

我们将被virtual修饰的函数叫做虚函数.//这里与虚继承使用同一个关键字,但是并没有关系

public:
	virtual void buyTicket()
	{
		cout << "成人票" << endl;
	}

那么重写又是什么呢?

重写:也叫隐藏,是派生类中有一个跟基类完全相同的虚函数称子类的虚函数重写了基类的虚函数。(协变允许返回值不同)

这有一个例子实现多态

#include
using namespace std;
class Person
{
public:
	virtual void buyTicket() {
		std::cout << "成人票" << std::endl;
	}

};
class Student : public Person
{
	public:
		void buyTicket() {
			std::cout << "学生票" << std::endl;
		}
	};

void Func(Person& p) {
	p.buyTicket();
}

void main() {
		Person p;
		Student s;
		Func(p);
		Func(s);
}

 这里不同的身份票买的不一样的.,这里子类的虚函数重写了父类的虚函数,实现了多态

协变

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

面向对象三大特性之多态(详解)_第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;
	return 0;
}

运行结果:我们发现不重写的析构函数是不会调用子类的析构函数的,也就是说当我们开辟一个子类对象,赋值给父类时,当其声明周期结束的时候,子类中的资源是不会释放的,这很可能造成一定的危险。

析构函数重写为虚函数后

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

面向对象三大特性之多态(详解)_第2张图片

运行结果:资源完全释放,系统根据指针指向的对象进行析构函数的调用,而不是根据类型调用

 二.抽象类和override和final

抽象类

顾名思义:就是不能实例化出对象的类.

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

接口继承和实现继承

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

override和final

C++11给我们提供了override和final两个关键字来帮助用户检测是否重写

1.override修饰函数,检查派生类虚函数是否重写了父类的某个虚函数,没有重写则报错

class Person
{
public:
	 virtual void buyTicket() { cout << "Person()" << endl; }
};
class Student :public Person
{
public:
	void buyTicket1()override { cout << "Student" << endl; }//没有重写父类虚函数,报错
};

2.final修饰虚函数,表示该虚函数不能被继承

class Person
{
public:
	virtual void buyTicket()final  { cout << "Person()" << endl; }
};
class Student :public Person
{
public:
	virtual void buyTicket() { cout << "Student" << endl; }//这样编译会报错
};

三.多态的原理

提到多态,那就不得不提到虚表.

操作系统为构成多态的每个类增加了一个虚函数表。这个虚函数表中存放的就是virtual关键词修饰的虚函数的首地址。编译器运行的时候通过虚表中存储的函数首地址去调用对应的函数。从而达到我们多态的目的。

验证虚表的存在:这里sizeof(Base)大小是多少呢?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

因为Base类里面多了一个虚表指针所以结果为8;

 打印虚表中的函数地址,对地址进行分析;

虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。利用这个特性我们进行类型强转,于是可以打印出这个续表中存储的各个函数指针的数值。

class Person {
public:
	virtual void Fun1() {
		std::cout << "Fun1()" << std::endl;
	}
	virtual void Fun2() {
		std::cout << "Fun2()" << std::endl;
	}
	virtual void Fun3() {
		std::cout << "Fun3()" << std::endl;
	}
	virtual void Fun4() {
		std::cout << "Fun4()" << std::endl;
	}
private:
	int _a;
};
class Student : public Person {
public:
	virtual void Fun1() {
		std::cout << "Fun1()" << std::endl;
	}
	virtual void Fun2() {
		std::cout << "Fun2()" << std::endl;
	}
	virtual void Fun3() {
		std::cout << "Fun3()" << std::endl;
	}
	virtual void Fun5() {
		std::cout << "Fun5()" << std::endl;
	}
private:
	int _b;
};

typedef void(*VFPTR) ();
void MyPrint(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	std::cout << " 虚表地址>" << vTable << std::endl;

	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << std::endl;
}
int main() {
	Person s1;
	Student s2;
	VFPTR* vTableb = (VFPTR*)(*(int*)&s1);
	MyPrint(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&s2);
	MyPrint(vTabled);
	return 0;
}

面向对象三大特性之多态(详解)_第3张图片

结果:父子类中都有一张虚表,用来存放虚函数的地址,子类重写父类中的虚函数时,子类虚表会指向新的地址(函数存在代码段),如果没有重写,父子的虚表都指向同一个地址.

动态绑定和静态绑定

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

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

单继承的虚表如上,多继承的虚表有所不同

当存在多继承时,子类中就会存在一张包括n个父类的虚表的虚表,每张虚表都来自与不同的父类

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) ();
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* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

面向对象三大特性之多态(详解)_第4张图片

子类先继承的放在前面,子类中父类没有的函数放在第一张表的末尾.

虚函数与虚继承

虚继承解决了多继承产生的菱形继承问题,底层实际是产生数据冗余和二义性的地方不存储对应数据,而是选择存储一个指向虚基表的指针,通过虚基表中的偏移量来找到对应的数据。
而多态实现了,一个对象指向谁调用谁。底层是在类的起始位置存储了一个虚函数表地址,当程序运行起来时,通过在虚函数表中查找对应的函数,来实现指向谁调用谁。

重载,重写和重定义(隐藏)

面向对象三大特性之多态(详解)_第5张图片

补充:

1.inline函数可以是虚函数吗?不能,因为inline函数没有地址,无法把地址放到虚函数表中

2.虚函数表实际上是一个指针,存的也并非虚函数,而是虚函数的指针,虚函数表在代码段,虚函数也在代码段

3.静态成员函数不能是虚函数,静态成员函数没有this指针,不能用使用类型::成员函数的调用方式无法访问虚函数表

4.构造函数可以是虚函数吗?不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

5.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。

6.对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

7.虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

8.多态如何实现的指向谁调谁? 通过this指针指向的类找到虚函数指针,虚函数表中调用

你可能感兴趣的:(c++,开发语言,后端)