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;
-
可以看到普通函数并不会放到虚函数表中。
-
可以看到 Base 和 Drive 所创建的对象的虚表指针不同。
如果虚函数 Func2 没有被重写,子类中的虚表中放的依旧是 Func2 的虚函数的地址;如果虚函数 Func1 重写,我们说重写也叫做覆盖,你可以理解为子类的虚表是把父类的虚表拷贝过来 (当然这里没必要做写时拷贝),谁完成了重写,就把重写的位置覆盖成重写的虚函数,所以你可以认为重写是语法层的概念,覆盖是原理层的概念;如果都不完成重写,虽然父子类中虚表的内容是一样的,但是并不代表着它们要共用一张虚表,也没必要,因为空间用的不多。
所以一个类的所有对象共享一张虚表;父子类无论是否完成虚函数重写,都有各自独立的虚表;
C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表