前言
需要声明的,本节课件中的代码及解释都是在 vs2019 下的x86程序中,涉及的指针都是4bytes 。如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes 问题等等
1. 多态的概念
1.1 概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
再举个栗子:
最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。
2. 多态的定义及实现
2.1多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
比如 Student 继承了 Person 。Person 对象买票全价,Student 对象买票半价。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2 虚函数
虚函数:即被 virtual 修饰的类成员函数称为虚函数。
virtual 只能用来修饰非静态的成员函数(即 全局函数和静态函数都不行)
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } };
2.3虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
如果没有加 virtual 标识,则子类和父类相同的成员 叫做隐藏
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 ps; Student st; Func(ps); Func(st); return 0; }
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用这样使用
这个意思是:子类不写 virtual 可以,父类不写 virtual 不行。后面讲的派生类的析构可以不加virtual,但是建议加上,别乱搞doge
2.3.1 虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函 数返回派生类对象的指针或者引用时,称为协变。(了解)
返回值类型可以写什么?:只要是构成父子类的都行,是 Person 和 Student 都行,是 A 和 B 都行,但是如果 A 和 B 不构成 父子关系,则报错这个怎么用:没什么用,记住了解就行(这个设计可以说一个C++的垃圾点)一些题可能会拿这个出 判断是否是多态的题(用 协变来混淆你)
// A 和 B 构成父子 class A {}; class B : public A {}; // 就可以被 Person 类 和 Student 类拿来做 返回值类型 class Person { public: virtual A* f() { return new A; } }; class Student : public Person { public: virtual B* f() { return new B; } };
2. 析构函数的重写(基类与派生类析构函数的名字不同)
⭐先看一个例子
下面的 父子类的析构函数都没有加 virtual ,则不构成多态
此时定义一个 父类指针 p1 指向 子类Student
delete p1 时,你会发现 程序只调用了 父类的 析构函数,而没有调用子类的析构,会导致一个严重的问题:内存泄漏(没有释放 _ptr)(C++最怕内存泄漏了)
class Person { public: ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() { delete _ptr; cout << "~Student():"<< _ptr<< endl; } protected: int* _ptr = new int[10]; }; int main() { //Student st; Person* p1 = new Student; delete p1; return 0; }
⭐解决办法
可以使父子的析构构成多态,因为多态的规则是 :父类指针或引用指向哪个对象类型,就调用哪个对象的函数
此时 p1 指向 Student 类,则调用 Student 类的 析构函数
class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() //override { //delete _ptr; cout << "~Student():"<< _ptr<< endl; } protected: int* _ptr = new int[10]; }; int main() { //Student st; // 指向子类:调用子类的 析构 Person* p1 = new Student; 实现多态,就正常了 p1->destructor() + operator delete(p1) delete p1; cout << '\n'; // 指向父类:自然只调用父类的析构 Person* p2 = new Person; delete p2; return 0; }
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
析构函数名称统一处理成 destructor 的来源:因为老本刚开始设计C++时仅仅是在C语言的基础上添加类,想到构造和析构的功能相反,就直接在构造函数的前面加上 ~ 取反符号,就表示 析构了,可没想到 后续设计 继承和多态时,会有隐藏和重写(函数名需要相同)的规则,因此 析构函数名称统一处理成destructor
小疑问:既然虚函数一定程度上可以帮助避坑,可不可以所有成员函数都写成虚函数?
不可以,语法规定,写了虚函数,就一定要 构成重写,内部会生成虚表
这个东西是有空间代价的
2.4 C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,
因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
1. final:修饰虚函数,表示该虚函数不能再被重写
class Car { public: virtual void Drive(){} // 不能被重写,下面子类重写会报错 }; class Xiaomi_Su7 :public Car { public: virtual void Drive() override { cout << "Xiaomi_Su7-遥遥领先!" << endl; } };
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
这个就是强制检查 下面子类函数是否有重写
class Car { public: virtual void Drive(){} // 不能被重写,下面子类重写会报错 }; class Xiaomi_Su7 :public Car { public: virtual void Drive() override { cout << "Xiaomi_Su7-遥遥领先!" << endl; } };
因为析构函数不写成虚函数构成重写,可能会导致内存泄漏,因此C++做出的补救措施:override,可以帮助检查是否有构成重写
如果要写一个不能被继承的类:
方法一:可以用这个 final 阻止别人继承
class A final {}; class B:public A //一继承就报错 {};
方法二:将类的构造函数设置成 private
因为继承一个父类,子类会先调用父类的构造函数,如果父类的构造函数无法访问,则无法被继承
2.5 重载、覆盖(重写)、隐藏(重定义)的对比
3.抽象类
3.1 概念
在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出 了接口继承。
只要包含纯虚函数的类叫做抽象类:继承抽象类的 子类也含有纯虚函数,也变成抽象类。既然是因为 含有 纯虚函数,才是抽象类,如果我们想要恢复实例化的功能,就要改变纯虚函数这个纯虚的特性:即重写纯虚函数
什么时候方便使用 纯虚函数创建抽象类?
现实中或实际开发需求中,无需定义实体对象的类,这样的类型适合定义成纯虚函数。
比如 Person 类,不好定义成什么实体的,只要履行好一个父类的义务就好,不用你实例化成一个实体对象,具体的实体应该是外卖员、商家、用户 这三种 子类,子类直接继承 父类 Person 就行
class Car { public: virtual void Drive() = 0 {}; protected: string colour = "白色";// 颜色 string num = "粤AHE100";//车牌号 }; class Xiaomi_Su7 : public Car { public: // 这里必须强制重写纯虚函数 Drive,子类的 virtual 也可以不写的 virtual void Drive() { cout << "米时捷,豪帅!" << endl; } //virtual void func()=0;// 如果这里有一个纯虚函数,就不能被实例化 }; class AlTO : public Car { public: void Drive() { cout << "问界,遥遥领先!doge" << endl; } }; int main() { Xiaomi_Su7 xm; return 0; }
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4.多态的原理
由于篇幅限制,这里跳转博客学习:
【C++ 第十一章】多态的原理(详细图文 + 代码演示)-CSDN博客
5. 继承和多态常见的面试问题(大厂面试题)
1、多继承中指针偏移问题?
下面说法正确的是( )
// 多继承中指针偏移问题?下面说法正确的是( ) 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; }
这个设计 继承父类 ,父类在子类中的存储位置:先进来的,在前面,后进来的往后排
2、sizeof(Base)是多少?
// 这里常考一道笔试题:sizeof(Base)是多少? class Base { public: virtual void Func1(){ cout << "Func1()" << endl; } private: int _b = 1; char _c; }; int main() { cout << sizeof(Base) << '\n'; return 0; }
本题是考查 虚函数表的存储,有了虚函数,会在类中存储一个指向虚函数表的指针
因此计算类的大小时,需要算上这个指针的大小
根据内存对齐且在32位下指针是 4 字节,Base 的大小为 4 + 4 + 1 = 9 ,再和最大对齐数对齐,最终结果为 12 字节
3、【大厂超坑题】以下程序输出结果是什么()
比如这一道题:
Test 函数使用父类指针接收,符合多态
在 main 函数中 ,给 Test 函数传过去的 是 B 对象,则 Test 函数中 引用 p 指向 对象B,就会调用对象 B 的 func 函数,此时运行结果是什么?
class A { public: virtual void func(int val = 11) { cout << "A->" << val << '\n'; }; }; class B : public A { public: virtual void func(int val = 22) { cout << "B->" << val << '\n'; }; }; void Test(A& p) { p.func(); } int main() { B p; Test(p); return 0; }
A. A->11 B. A->22 C. B->11 D. B->22 E.编译错误 F.以上结果都不对
按照正常流程走,是不是答案应该选 D ?错了!!
解释:
继承的本质:
不会真的将父类的成员拷贝一份下来,而是在 生成子类对象时,在子类中放一个父类,然后再放子类的成员
父类虽然继承到了子类,但是不代表父类的成员变成了子类的成员
调用某个成员时,会优先到子类的成员中查找,然后到父类的成员中查找
当在子类中查找成员时,this 指针是 子类的;当在 父类中查找成员时,this 指针是 父类类的
重写的本质:(注意:下面的解释是 符合多态的 情况下的)
构成多态时,是重写虚函数的实现
仔细理解这句话:函数的实现是指 函数体,而不包括接口
则重写的就是 函数体,而接口不变!!
因此,父类的虚函数 A 和子类 虚函数 B 构成重写后,实际上 子类的虚函数 B 的结构变成:函数接口是父类的 虚函数 A 的接口,函数体 是 子类的虚函数 B
因此,这里其实有坑,上面讲解过 在多态下 重写函数的 本质:子类中的虚函数实际上,函数接口是父类的虚函数接口!
也就是 类 B 中 func 函数里面的 val = 11,即 父类的 func 函数
因此结果选 C
原题
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; }
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
选 B