C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表

Posted 跳动的bit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表相关的知识,希望对你有一定的参考价值。

文章目录

【写在前面】

本文我们将学习面向对象三大特性中的最后一个特性 —— 多态。需要声明的是,多态一文包括继承一文中所演示的测试用例都是按编译器 VS2017 来演示的,我们会演示底层细节,如果你换一个编译器,包括 VS 系列,可能展示出来的效果会有一点变动,因为 C++ 并没有规定具体的实现细节。

一、多态的概念

顾名思义,多态就是多种形态,具体就是去完成某种行为时,不同的对象去完成时会产生不同的状态。

举个粟子,比如买票这种行为,当普通人去买票时,是全价买票;当学生去买票时,是半价买票;当军人买票时,是优先买票。

再举个粟子,为了争夺在线支持市场,某软件会经常做一些扫码领红包的活动,其中我们会发现,有的人扫到了七八块,有的人扫到了七八角等,其实这背后就是一个多态的行为,它分析你的帐户数据,比如你是第一次扫码,给你 rand() % 99 块、第二次扫码 rand() % 10 块,第三次扫码 rand() % 1 角 … …,同样都是扫码动作,不同的用户扫到的红包也不一样,本质也是一种多态行为。

✔ 测试用例一:

#include<iostream>
#include<string>
using namespace std;

class Person

public:
	void BuyTicket()
	
		cout << "正常排队-全价买票" << endl;
	
protected:
	int _age;
	string _name;
;
class Student : public Person

public:
	void BuyTicket()
	
		cout << "正常排队-半价买票" << endl;
	
protected:
	//...
;
class Soldier : public Person

public:
	void BuyTicket()
	
		cout << "优先排队-全价买票" << endl;
	
protected:
	//...
;
void Func(Person* ptr)

	ptr->BuyTicket();


int main()

	Person ps;
	Student st;
	Soldier sd;

	Func(&ps);
	Func(&st);
	Func(&sd);

	return 0;

  • 怎么让不同的对象都能传给Person* ptr呢 —— 切片,让 Studnet 和 Soldier 继承 Person,但是这里继承之后,基类和派生类中都有 BuyTicket(),那么派生类就会对基类的 BuyTicket() 隐藏,但是这里 Func 去调用时依然是调用 Person 的。运行程序,可以看到并没有实现多态,都去调用了基类的,这是因为多态的构成需要满足两个条件。

二、多态的定义及实现

💦 多态的构成条件

✔ 测试用例二:

#include<iostream>
#include<string>
using namespace std;

class Person

public:
	virtual void BuyTicket()
	
		cout << "正常排队-全价买票" << endl;
	
protected:
	int _age;
	string _name;
;
class Student : public Person

public:
	virtual void BuyTicket()//重写或覆盖父类的虚函数
	
		cout << "正常排队-半价买票" << endl;
	
protected:
	//...
;
class Soldier : public Person

public:
	virtual void BuyTicket()//重写或覆盖父类的虚函数
	
		cout << "优先排队-全价买票" << endl;
	
protected:
	//...
;
void Func(Person* ptr)//指针

	//多态 - ptr指向父类对象,调用父类的虚函数;指向子类对象,调用子类的虚函数
	ptr->BuyTicket();

void Func(Person& ptr)//引用

	//多态 - ptr指向父类对象,调用父类的虚函数;指向子类对象,调用子类的虚函数
	ptr.BuyTicket();

//void Func(Person ptr)//对象
//
//	ptr.BuyTicket();
//

int main()

	Person ps;
	Student st;
	Soldier sd;

	Func(&ps);
	Func(&st);
	Func(&sd);

	Func(ps);
	Func(st);
	Func(sd);

	return 0;

  • 在继承中要构成多态还有两个条件:a) 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写或覆盖,在继承中我们称这里为隐藏或重定义,注意区分;b) 必须通过基类的指针或者引用调用虚函数,这个条件在测试用例一中就满足了。

    此时就完成了多态,如果没有多态的语法,按以前的理解,Func 就是调用 Person* 类型的。这里说明以前是跟类型有关,现在是跟对象有关,多态就是让调用跟对象有关,不同的对象去做同一件事,达到的行为是不一样的。

  • 注意切片切的是成员变量,与成员函数没有关系,并且这块要实现指向谁调用谁跟切片没有关系,切片只是让父类的指针可以指向子类对象或父类对象,指向子类对象就意味着看到子类对象的那一部分。

  • 多态的两个条件缺一不可,这里可以看到通过基类的对象调用虚函数就不构成多态了,这个问题我们只能先放着,因为这必须了解多态的底层原理才能知晓。

💦 虚函数

class Person 

public:
	virtual void BuyTicket()  cout << "买票-全价" << endl;
;
  • 虚函数就是被 virtual 修饰的类成员函数,它跟虚继承共用了一个关键字 virtual。
  • 注意虚继承和虚函数中的 virtual,并没有关联关系,就像取地址和引用没有半毛钱关系,并不是天下姓王的都是亲戚。

💦 虚函数的重写

//重写(覆盖)
class Person

public:
	virtual void BuyTicket()
	
		cout << "正常排队-全价买票" << endl;
	
;
class Student : public Person

public:
	virtual void BuyTicket()
	
		cout << "正常排队-半价买票" << endl;
	
;

//隐藏(重定义)
class A

public:
	void fun()
	
		cout << "fun()" << endl;
	
;
class B : public A

public:
	void fun(int i)
	
		cout << "fun(int i)" << endl;
	
;
  • 构成多态的条件之一是虚函数的重写,而虚函数也有自己的规则,虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数,即派生类虚函数和基类虚函数的返回值类型、函数名、参数列表完全相同,就称子类的虚函数重写了基类的虚函数。

    注意区分隐藏的概念,隐藏是只要基类函数名和派生类函数名相同即是隐藏或重定义。

  • 虚函数要求三同,但是这三同有些例外,这就恶心了,具体例外看测试用例三四五。

✔ 测试用例三:

#include<iostream>
using namespace std;

//class A ;//AB为无关联的类
//class B ;
class A ;//AB为关联的父子类
class B : public A ;

class Person

public:
	virtual A* BuyTicket()
	
		cout << "正常排队-全价买票" << endl;
		return new A;
	
protected:
	int _age;
	string _name;
;
class Student : public Person

public:
	virtual B* BuyTicket()
	
		cout << "正常排队-半价买票" << endl;
		return new B;
	
protected:
	//...
;
void Func(Person& ptr)

	ptr.BuyTicket();


int main()

	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;

  • 协变 (基类与派生类虚函数返回值类型不同),即重写的虚函数可以不同,但是返回值必须是父子类型指针或引用。

    如果返回值是普通没有关联的类,那么它既不满足三同、也不满足协变,会编译报错。

    如果返回值是有关联的父子类,那么虽然它不满足三同,但是它满足协变这个例外,所以能构成多态。

✔ 测试用例四:

#include<iostream>
using namespace std;

class Person

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

public:
	//~Student()
	virtual ~Student()
	
		cout << "~Student()" << endl;
	
;

int main()

	//普通场景
	Person p;
	Student s;

	//new对象的特殊场景 
	Person* p1 = new Person;
	Person* p2 = new Student;
	
	delete p1;//p1->destructor() + operator delete(p1)
	delete p2;//p2->destructor() + operator delete(p2)

	return 0;

  • 析构函数的重写 (基类与派生类析构函数的名字不同)

    如果是普通的析构, 程序运行没有问题,这里生命周期结束,s 后定义,s 先析构,s 中分为为两个部分,先调用自己的析构,再去调用继承的父类的析构,随后再去调用 p 的析构,如果是虚函数的析构,可以看到结果也同普通的析构。

    虚函数的析构有什么意义 ❓

      普通场景下,虚函数是否重写都是 ok 的;new 对象的特殊场景下,Person 的指针 p1 指向 Person 的对象、Person 的指针 p2 指向 Student 的对象、delete Person 的对象、delete Student 的对象。这里 new Person 调用 Person 的构造函数、new Student 调用 Studnet 的构造函数 + Person 的构造函数都没有问题;这里 delete p1 期望的是 delete 调用 Person 的析构函数、delete p2 调用 Student 的析构函数 + Person 的析构函数,但是析构函数不构成虚函数的话是无法完成的。在继承中我们说过,子类中要去显示的调用父类的析构函数,需要指定作用域,因为所有类的析构函数名都被处理成了 destructor(),所以子类和父类的析构函数构成隐藏关系。为什么它要对析构函数名作单独处理呢,因为如果这里不构成多态,调用时看的是指针的类型,那么这里 p1 和 p2 调用的都是 Person 的析构函数,此时就不对了。p1 没问题,但是 p2 指向的是一个子类对象,子类对象应该先调用子类的析构函数,再去调用父类的析构函数,万一子类对象中需要 delete,那么 Student 的析构函数没调到就有可能会出现资源泄漏。

      所以这里 delete p1/p2 是想达到多态的场景,Person* 指向父类调父类,指向子类调用子类,上面已经满足多态的条件之一,通过基类的指针或者引用调用虚函数;但是并没有满足多态的条件之二,被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;要完成虚函数的重写有两个条件:它们必须是虚函数以及三同,析构函数本来没有返回值,也就不考虑协变了。这里的两个析构函数没有返回值、参数,函数名不相同,因为在这种场景下需要多态,所以编译器对它们进行了特殊处理,统一成 destructor(),所以这里我们对于这种场景是需要加上 virtual 的,所以 delete p1 指向父类,调用父类的虚函数,delete p2 指向子类,调用子类的虚函数,子类析构函数结束后,再调用父类的虚函数。

✔ 测试用例五:

#include<iostream>
#include<string>
using namespace std;

class Person

public:
	virtual void BuyTicket()
	
		cout << "正常排队-全价买票" << endl;
	
protected:
	int _age;
	string _name;
;
class Student : public Person

public:
	void BuyTicket()//可以不加virtual
	
		cout << "正常排队-半价买票" << endl;
	
protected:
	//...
;
void Func(Person* ptr)

	ptr->BuyTicket();


int main()

	Person ps;
	Student st;

	Func(&ps);
	Func(&st);

	return 0;

  • 其实严格来说这里还有一个例外,子类中的重写函数可以不加 virtual,但是通常不建议这样做。

    为什么子类重写时可以不加 virtual ❓

      因为它的理解是认为你继承下来,我是在重写你,继承后你都有虚函数属性了,我去重写你,加与不加都无所谓。主要的应用场景还是测试用例四中的问题,如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类的析构函数名不同,看起来违背了重写的规则,其实编译器对析构函数名统一处理成了 destructor()。也就是说如果支持子类不加虚函数也构成重写的话,那么只要父类中析构函数是虚函数,析构函数就一定构成重写,之后的问题就不存在了。

      这种例外,无疑是让语法变的更重了,C++ 经常爱搞这种东西,已经见怪不怪了。

💦 静态多态和动态多态

有些书籍会把多态进行细分:

  • 静态多态是函数重载,调用一个函数,传不同的参数,就有不同的行为。
  • 动态的多态是调用一个虚函数,不同的对象去调用,就有不同的行为。

💦 C++11 final和override

✔ 测试用例六:

class A final
;
class B : public A
;

int main()

	return 0;

#include<iostream>
using namespace std;

class Car

public:
	virtual void Drive() final 
	
;
class Benz : public Car

public:
	virtual void Drive() 
	 
		cout << "Benz-舒适" << endl; 
	
;

int main()

	return 0;

  • final 修饰类,表示该类不能再被继承。同样是实现出一个不能被继承的类,C++11 中的 final 相对 C++98 中对构造函数私有化更为彻底。

  • final 修饰虚函数,表示该虚函数不能再被重写,重写则会报错。

✔ 测试用例七:

#include<iostream>
using namespace std;

class Car 

public:
	virtual void Drive() 
;
class Benz : public Car 

public:
	//virtual void Drive() override//ok,重写 
	// 
	//	cout << "Benz-舒适" << endl; 
	//

	//void Drive() override//ok,属于重写的例外
	// 
	//	cout << "Benz-舒适" << endl; 
	//

	virtual void Drive(int) override//err,没有完成重写
	
		cout << "Benz-舒适" << endl;
	
;
int main()

	return 0;

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

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

三、抽象类

💦 概念

✔ 测试用例八:

#include<iostream>
using namespace std;

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

int main()

	Benz bz;

	Car* pBenz = new Benz;
	pBenz->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();	

	return 0;

  • 在虚函数的后面写上 = 0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类 (也叫接口类,你可以模糊的认为一个类的公有成员函数是接口,但更多是因为纯虚函数只有声明,没有实现,所以叫接口类。通常还是叫抽象类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象,也就是说纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

    理解接口类和抽象类 ❓

      接口类相比抽象类的概念更广泛,你可以认为一个类的公有成员函数是接口,换一个角度,如果一个类设计的不规范,也不能说公有的成员函数就是接口。但更重要的是纯虚函数只有声明,没有定义,所以有些地方叫接口类。这个概念比较模糊,但是也要能理解有些地方叫接口类,但是更重要的还是要理解它叫抽象类。

      抽象这个词,我们理解的场景是 “ 你长的好抽象 ” 或 “ 抽象派画家画的画好抽象 ”。本质抽象类指的是在现实世界中没有具体的对应实物,也就没必要实例化对象,比如说车去实例化对象,那么对象是卡车、公交车还是观光车?车是抽象的,它实例化的对象没有具体对应实物,所以我们可以把它实现为抽象类,让它不能实例化对象。

  • 抽象类可以创建指针,但是只能指向子类,因为父类不能创建对象。

💦 接口继承和实现继承

  • 这个概念了解下,有些书上会提,主要是为了避免以后遇到了不知道它讲的啥。
  • 普通函数的继承是一种实现继承,派生类继承了基类,可以使用函数,继承的是函数的实现。虚函数的继承是接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达到多态,继承的是接口。所以不实现多态,不要把函数定义成虚函数。

四、多态的原理

💦 虚函数表

✔ 测试用例九:

#include<iostream>
using namespace std;

class Base

public:
	virtual void Func1()
	
		cout << "Base::Func1()" << endl;
	
	virtual void Func2()
	
		cout << "Base::Func2()" << endl;
	
private:
	int _b = 1;
	char _ch = 'a';
;

int main()

	cout << sizeof(Base) << endl;
	Base b;

	return 0;

  • 这是常考一道笔试题,遇到这种题时打死也不可能是 8,因为是 8 的话那就是考查结构体的内存对齐,为啥还要搞个类呢。

  • 当一个类有虚函数后,这个类会增加 4 个字节在前面,这 4 个字节是一个指针,这个指针叫做虚函数表指针,简称虚表指针 __vfptr (v 是 virtual、f 是 function、ptr 是指针,但是 __vftptr 更准确,就是说这个指针不是指向虚函数,而是指向虚函数表,表里才是虚函数),__vfptr 指向的表是虚函数表,简称虚表,这个表你可以认为它是函数指针数组,表里存储的是虚函数的地址 (注意虚函数存储于虚表中这种说法不完全对,因为虚函数被编译成指令后,跟普通函数一样存储在代码段,只是它的地址放到了虚表中)。注意区分继承中谈的虚基表指针,它所指向的表所存储的是偏移量,用于查找基类。

✔ 测试用例十:

#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;
	char _ch = 'a';
;
class Drive : public Base

public:
	virtual void Func1()//重写Func1
	
		cout << "Drive::Func1()" << endl;	
	
private:
	int _d = 2;
;

int main()

	Base b1;
	Base b2;
	Base b3;

	Drive d1;
	Drive d2;

	return 0;