ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多态的认识和使用(cpp)

多态的认识和使用(cpp)

一、多态的概念

多态就是多种状态,比如打印机可以打印黑白 也可以打印彩的

买票买学生票或者成人票或者儿童票,也可以逃票~~~

二、多态的定义和实现

实现多态的两个必要条件!!

必须是基类的指针或者引⽤调⽤虚函数
被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
如下是代码!!
#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* ptr) { // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。 ptr->BuyTicket(); } int main() { Person ps; Student st; Func(&ps); Func(&st); return 0; }

那啥是重写和覆盖呢???

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值 类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

class Student : public Person { public: private: virtual void BuyTicket() { cout << "买票-打折" << endl; } };

甚至这样也是可以调用的

是因为,多态发生,派生类使用的是基类的声明 所以private还是public就无所谓了~

重写的一些其他问题 协变

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们了解⼀下即可。
class A { }; class B : public A { }; class Person { public: virtual A *BuyTicket() { cout << "买票-全价" << endl; return nullptr; } }; class Student : public Person { public: virtual B *BuyTicket() { cout << "买票-打折" << endl; return nullptr; } }; void Func(Person *ptr) { ptr->BuyTicket(); } int main() { Person ps; Student st; Func(&ps); Func(&st); return 0; }

析构函数的重写

因为多态的原因,析构函数的名字变成了destructor() 这样两个类的继承的析构就变成了隐藏

class A { public: virtual ~A() { cout << "~A()" << endl; } }; class B : public A { public: ~B() { cout << "~B()->delete:" << _p << endl; delete _p; } protected: int *_p = new int[10]; }; // 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能 // 构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。 int main() { A *p1 = new A; A *p2 = new B; delete p1; delete p2; return 0; }

这样才不会造成内存泄漏!!! 多态调用,指向谁,调用谁的析构函数!!

基类的析构函数一定要是虚函数!!!

重载 重写 隐藏的对比

纯虚函数和抽象类

在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。
纯虚函数
1. 不能实例化对象
2. 强制子类去重写虚函数
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() { // 编译报错:error C2259: “Car”: ⽆法实例化抽象类 Car car; Car* pBenz = new Benz; pBenz->Drive(); Car* pBMW = new BMW; pBMW->Drive(); return 0; }

三、多态的原理

看下列代码,运行结果是什么?64位

class Base { public: virtual void Func1() { cout << "Func1()" << endl; } // virtual void Func2() // { // cout << "Func2()" << endl; // } protected: int _b = 1; char _ch = 'x'; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }

因为会存一个虚函数表指针 64位 是8个字节

虚函数表,简称虚表,本质是函数指针数组!!!

class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } private: string _name; }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-打折" << endl; } private: string _id; }; class Soldier : public Person { public: virtual void BuyTicket() { cout << "买票-优先" << endl; } private: string _codename; }; void Func(Person* ptr) { // 这里可以看到虽然都是Person指针ptr在调用BuyTicket // 但是跟ptr没关系,而是由ptr指向的对象决定的。 ptr->BuyTicket(); } int main() { // 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后 // 多态也会发生在多个派生类之间。 Person ps; Student st; Soldier sr; Func(&ps); Func(&st); Func(&sr); return 0; }

满足多态,运行时到指向对象虚函数表中找到对应虚函数调用!!

3.1 动态绑定与静态绑定

对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数
的地址,也就做动态绑定。

3.2 虚函数表

同一个类的所有对象共享同一份虚函数表

同时派生类会继承基类的虚函数表,然后根据已有的进行覆盖和添加!!

派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类
⾃⼰的虚函数地址三个部分。
虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函
数的地址⼜存到了虚表中。
虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以
对⽐验证⼀下。vs下是存在代码段(常量区)
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进