1. 🏷多态的概念
多态的概念: 通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态。
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票。
再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的
活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5
毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐
2. 🏷多态的定义及实现
📌虚函数
虚函数: 即被 virtual
修饰的类成员函数称为虚函数
class Person
{public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
📌虚函数的重写
✒概念:虚函数的重写(覆盖):
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、*参数列表完全相同),称子类的虚函数重写了基类的虚函数。
✒如何重写?
- 首先得是一个虚函数:使用关键字
virtual
来修饰成员函数 让其成为一个虚函数 - 然后安照:返回值类型、函数名字、*参数列表完全相同来写子类的成员函数
class Person { //父类public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};class Student : public Person { //子类public:virtual void BuyTicket() { cout << "买票-半价" << endl; }/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*//*void BuyTicket() { cout << "买票-半价" << endl; }*/};void Func(Person& p) //必须是父类的指针或者引用
{ p.BuyTicket(); }int main(){Person ps;Student st;Func(ps);Func(st);return 0;}
✒虚函数重写的两个例外:
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
class A{}; //父类class B : public A {}; //子类class Person
{public:virtual A* f() {return new A;} //返回父类的指针
};class Student : public Person
{ public:virtual B* f() {return new B;}[ //返回子类的指针]
};
- 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual
关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
。
class Person
{public:virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person
{public:virtual ~Student() { cout << "~Student()" << endl; }
};// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
📌多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
📌多态的一些理解
✒多态的举例:
#include<iostream>
using namespace std;class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person //子类
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}
✒ 不继承
上面的代码如果我们 不继承 的话,效果会怎样?
#include<iostream>
using namespace std;class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student //: public Person //子类 我们把这里的公有继承删掉
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}
改了之后上面的代码会报错,因为,你Func函数的参数是person
类型的,但是你的Johoson
是student类型
的,所以类型不匹配,报错。
✒不引用
如果我们把Func函数改变一下:
它原来是这样的:
void Func(Person& p)
{p.BuyTicket();
}
如果它的参数,不是指针
也不是引用
的话,那将怎样呢?
如下:
void Func(Person p)
{p.BuyTicket();
}
修改后的完整代码如下:
#include<iostream>
using namespace std;class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person //子类
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}
由于它不是指针也不是引用,说明他不满足多态的定义,所以这里就是普通的函数调用。
这里提一下 普通调用和多态调用的区别
✒把引用换成指针
#include<iostream>
using namespace std;class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person //子类
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person* p) //注意这里变成指针了
{p->BuyTicket();
}int main()
{Person Mike;Func(&Mike); // 所以传参也会反生改变Student Johnson;Func(&Johnson); //注意这里也会发生变化return 0;
}
✒判断下面的情况是否构成多态
class Person { //父类
public:void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person //子类
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
这里把父类的virtual
删除掉了
如果你毫无思绪我们这里可以提一下多态的条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
答:这样不构成多态,因为父类的不是virtual
没有完成对虚函数的重写
那下面这种情况构成多态吗?
class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person //子类
{
public:void BuyTicket() { cout << "买票-半价" << endl; }
};```这里把子类里的``virtual`` 删掉了答: 这里构成多态,这里就是上面我们写的虚函数重写的两个例外;### ✒在什么场景下析构函数必须是 虚函数?在下面的场景是不需要析构函数是虚函数的
```c++
#include<iostream>
using namespace std;class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }~Person(){cout << "~Person" << endl;}};
class Student : public Person //子类
{public:virtual void BuyTicket() { cout << "买票-半价" << endl; }~Student(){cout << "~Student()" << endl;}};void Func(Person* p)
{p->BuyTicket();
}int main()
{Person Mike;//Func(&Mike);Student Johnson;//Func(&Johnson);return 0;
}
运行结果:
而在:new
的场景下 要求析构函数必须是 虚函数
请看下面的场景:
Person* ptr = new Person;delete ptr;ptr = new Student;delete ptr;
完整代码如下:
#include<iostream>
using namespace std;class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }~Person(){cout << "~Person" << endl;}};
class Student : public Person //子类
{public:virtual void BuyTicket() { cout << "买票-半价" << endl; }~Student(){cout << "~Student()" << endl;}};void Func(Person* p)
{p->BuyTicket();
}int main()
{/*Person Mike;Func(&Mike);Student Johnson;Func(&Johnson);*/Person* ptr = new Person;delete ptr;ptr = new Student;delete ptr;return 0;
}
运行上面的代码,我们可以得到下面的结果:
我们来分析结果出现的原因,
第一个释放:delete ptr 是父类的指针指向的父类的对象 所以调用父类的析构函数,所以在屏幕上打印~Person
第二个释放:delete ptr 是父类的指针指向的子类的对象,但是父类的析构不是虚函数,所以不会去调用子类的析构函数,只会调用父类的析构函数,所以在屏幕上打印:~Person
。
如何改进:
我们回想一下多态的条件:
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
上面已经满足条件1了,因为都是父类的指针
就只差条件2没有满足了,差个虚函数。
虚函数的条件也只差一个 virtual 关键字了,有的兄弟会疑惑不是函数名也不相同吗,那怎么构成虚函数,这里就是构成虚函数的那两个特例中的其中一个,可以回过去看看。
修改的代码:
修改部分,在析构函数上面加上virtual
关键字。
#include<iostream>
using namespace std;class Person { //父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual ~Person(){cout << "~Person" << endl;}};
class Student : public Person //子类
{public:virtual void BuyTicket() { cout << "买票-半价" << endl; }virtual ~Student(){cout << "~Student()" << endl;}};void Func(Person* p)
{p->BuyTicket();
}int main()
{/*Person Mike;Func(&Mike);Student Johnson;Func(&Johnson);*/Person* ptr = new Person;delete ptr;ptr = new Student;delete ptr;return 0;
}
这回就对了。
第一个delete ptr 由于ptr是 Person* 类型的并且指向的是 Person 类型的对象,所以会调用~Person ,在屏幕上打印~Person
第二个 delete ptr, 由于ptr是Person* 类型的 但是指向的却是:Student 类型的变量,满足了多态的条件(1. 父类的指针,2. 虚函数) 所以会先调用子类的析构函数 ~Student ,在屏幕上打印:~Student
, 然后再调用父类的析构 在屏幕上打印 ~Person
。 什么? 你问为什么会去调用父类的析构函数,因为子类继承的父类,所以释放子类的空间的时候,要先去调用子类的析构,如果先调用父类的析构会造成问题。
✒下面看一道题来加深我们的理解
答案是 B
![[Pasted image 20240117215245.png]]
首先,我们看一下这个 p->test() , 我们分析一下这个是什么调用。
首先,p
是一个子类的指针,由于子类B是继承了它的父类A的,所以它可以去调用父类的这个函数test()
,但是这里的函数test()
并没有重写,所以不是多态,就是一个普通的调用。
然后,p
会传参,传给test()
函数的this
指针。
❓ 那这个时候这个this
指针的类型是A*
还是 B*
答案:这里的this
指针还是 A*
首先,B 是 A 的子类, B 是继承的 A ,但是 继承 只是一种比较形象的说法,它并不是直接去在B类中重新开一个test()
函数,而是 让 B 中调用的地方 可以去用A 类中的test()
函数,所以它并不会改变test()
函数的参数,所以test()函数中的this
指针的类型还是A*
如果你是A
对象去调用这个test()
函数,那就把A对象传给他,如果是 B 对象去调用,那就是切片
把B 对象中 A 那一部分切出来,然后指向 B 对象中的 A 的那一部分
好 然后我们就来到了这里:
接下来就是 func()
函数 , 这个就是一个多态调用了,因为她满足了多态调用的两个条件:
有些兄弟可能会感到疑惑,A 类中的 func() 函数, 和 B 类中的func()函数,他们的参数不同呀(A 类是 int val = 1, B 类是 int val = 0),那怎么构成虚函数的重写?
其实我们认为它们的参数是相同的。 因为我们看的是形参的类型,因为这里形参的类型都是 int
类型的 ,所以我们认为他们是相同的。
举个 例子,如果形参是这样的:A类中(int a = 1), B类中(int b = 2) 只要它的形参都是int
我们认为他们相应的函数都是相同的。
说回来,因为func()
函数构成了多态,所以这里会去调用子类的func()
函数。所以会在屏幕上打印B->
❓ 那到底是打印 B->1
还是 B->0
呢?
有些兄弟会认为会打印B->0
因为 B 类的那个func()
函数的 缺省值给的就是 0
,这么想就又踩坑了。
首先我们要明白一点,虚函数的重写
继承的是什么?
继承的是父类的接口声明也就是函数的声明。重写的是函数的实现,所以 子类的函数可以不加virtual
,因为只是重写函数的实现,所以函数的参数不会受到影响即:调用时还是调用的父类中虚函数的参数,子类虽然重新写了缺省值但是并没有什么作用,因为它根本不去调用你子类的那个参数,而是直接用父类的,所以这里的val
是等于父类中的值的 即: val = 1
,
所以屏幕上打印的是:B -> 1
📌 函数重载,覆盖(重写) 隐藏(重定义) 的对比
🏷C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
📌1. final:修饰虚函数,表示该虚函数不能再被重写
class Car{public:virtual void Drive() final {}};class Benz :public Car{public:virtual void Drive() {cout << "Benz-舒适" << endl;}};
final也可以修饰一个类,这个类不能被继承
📌2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{public:virtual void Drive(){}};class Benz :public Car {public:virtual void Drive() override {cout << "Benz-舒适" << endl;}};
修饰子类的虚函数 检查它是否完成了重写:如果它没有完成重写就会报错 完成重写了就是正常的
3. 🏷抽象类
📌概念
一个新概念:纯虚函数 , 什么是纯虚函数, 就是在虚函数的后面加上一个 =0
包含纯虚函数的类 就叫做抽象类(也叫接口类) 抽象类的特点就是:不能实例出对象
#include<iostream>
using namespace std;class Car {public:virtual void Drive() = 0; //在虚函数后面加上 =0 就是纯虚函数 }int main() {Car c1; // 这样写就错了 因为抽象类无法实例化对象,就是它定义不出对象Car* c2; //但是它可以定义出指针; return 0;
}
如果父类包含了纯虚函数 则子类也无法实例化出对象:
#include<iostream>
using namespace std;class Car {public:virtual void Drive() = 0; //在虚函数后面加上 =0 就是纯虚函数 };class Benz :public Car
{
public:};int main() {Car c1; // 这样写就错了 因为抽象类无法实例化对象,就是它定义不出对象Car* c2; //但是它可以定义出指针; Benz B1; // 因为父类包含了纯虚函数,所以它这个子类继承了父类 也无法实例化出对象return 0;
}
除非子类对纯虚函数进行了重写
class Benz :public Car
{
public: virtual void Drive() { // 这里重写了这个纯虚函数之后就可以实例化出对象了cout << "Benz-舒适" << endl;}
};
📌接口继承和实现继承
普通函数的继承就是接口继承,虚函数的继承就是实现继承。
4. 🏷多态的原理
// 这里常考一道笔试题:sizeof(Base)是多少?class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;};
这道题:答案是 8 (此结果为32位下的)如果是64位 结果是16
为甚是 8 ?
int 类型的 对象 _b
4个字节 + 虚函数表的指针
4 个字节 = 8
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些
平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代
表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表。
不管你继承没有继承 只要你有虚函数 就会有虚函数表。
虚函数的地址存放在虚函数表中的。
📌多态调用和普通调用是什么时候确定地址的?
📌 多态的其中一个条件是指针和引用,那为什么对象不行?
指针和引用都是指向了子类对象中切片出来的属于父类的那一部分, 但是对象的话是将子类对象中父类的那一部分成员拷贝给父类,但并不会拷贝虚函数表指针,所以使用对象不行。
📌 如果虚函数不重写,父类和子类的虚函数表指针一样不一样?
它们虚标里面的内容是一样的但是地址不一样
📌 同类对象的虚表一样不一样?
这个时候是一样的了:
本章图集