从C++开始的编程生活(17)——多态
前言
本系列文章承接C语言的学习,需要有C语言的基础才能学会哦~
第17篇主要讲的是有关于C++的多态。
C++已经进入进阶,加油!!
目录
多态
多态分为静态多态和动态多态
静态多态:传不同参数,编译出的函数就会不同(如函数重载、函数模板等)。
动态多态:同一个行为(函数),不同的对象完成的方式或者结果不一样(下文重点讲)。
语法
//函数重写Buyticket() class Peraon { public: virtual void BuyTicket() { cout << "买票——全价" << endl;}//① }; class Student : public Person{ public: virtual void BuyTicket() { cout << "买票——75折" << endl;}//② }; void Func(Person* ptr) { ptr->Buyticket(); } int main() { Person ps; Student st; //根据传入对象,调用指定的函数 Func(&ps);//调用① Func(&st);//调用② return 0; }实现多态的重要条件!!
①必须是父类的指针或引用去调用虚函数。
父类指针既可以指向父类也可以指向子类。
②被调用的函数必须是虚函数(关键字virtual在这里和虚继承的virtual没有关系)。
③子类必须对父类的虚函数进行重写。
重写(覆盖)的条件
同名、同参数、同返回值的虚函数,函数体不同,即可构成重写。如果没有virtual修饰,就会被函数隐藏。
在满足多态的情况下,会调用子类重写的虚函数,但是函数声明遵循的是父类,因为继承了父类的整个函数签名(连virtual也继承了)。
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() { B* p = new B; p->test(); return 0; }观察上述代码,我们可以看到B的func满足了重写A的func的条件,函数声明的属性用的是父类的声明,但是调用的是子类的函数体,所以最后输出的结果是B->1,让val使用了父类的缺省值。
这里也告诉我们,在写多态的时候,父子类最好不要有不同的缺省值,不然就会有意外的事情发生。
协变(了解即可)
子类重写父类虚函数的时候,与父类虚函数的返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用,这就叫做协变。(算是特殊情况,了解即可)。
析构函数的重写
子类的析构函数只要定义了,无论是否函数名是否相同,都可以与父类的析构函数构成重写。编译器会把子类的析构看成重写,并且进行特殊处理。
特殊处理:把父类和子类的函数名在编译时都换为destructor()
override和final关键字
override放在函数的括号后面修饰。被修饰的函数必须重写,不然就报错,可用于自检。
final也放在函数的括号后面修饰,被修饰的函数禁止重写,不然就报错,可用于自检。
纯虚函数和抽象类
(如果懂java可以类别为java的抽象接口)
纯虚函数:在虚函数的后面写上=0,那么这个函数就叫做纯虚函数,纯虚函数不需要定义实现,就算实现了也必须被重写,所以只用声明即可。
抽象类
包含纯虚函数的类叫做抽象类。
抽象类不可以实例化出对象,如果其子类不对其纯虚函数进行重写,那么子类也是抽象类,也不能实例化出对象。
虚函数表
简称虚表,存储了抽象类中所有虚函数的指针。
抽象类存储时,除了存储成员变量外,还会存储一个隐藏的虚函数表指针,这个指针指向一个指针数组,存储了虚函数的指针。
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } protected: int _b = 1; char _ch = 'x'; };如上的抽象类,运用内存对齐的知识,在32位下,该类的大小为12字节,而不是8字节,因为需要先存储虚函数表指针。

抽象类存储如上(_vftptr为虚函数表指针)
多态的原理
当抽象类被继承之后,其子类也会继承一个隐藏的成员——即虚函数表指针。但是指向的虚函数表不一样,子类的虚表指针指向的是自己的虚函数表。
因此,在进行多态调用的时候,会在对应的虚表中找到对应的虚函数进行调用。
void Func(Person* ptr) { //因为指针类型是Person*,所以传入的子类指针,只能访问父类成员的切片 ptr->BuyTicket(); } int main() { Person ps; Student st; Func(&ps);//父类对象,调用父类对象的虚函数表中的函数 Func(&st);//子类对象,调用子类对象的虚函数表中的函数 }如上,因为Func的参数为父类指针,
故:
①子类调用的时候,只能访问到父类成员的切片,具有安全性;
②同时因为父子类存储的虚表不同,传入父类就调用父类的虚函数,传入子类就调用子类的虚函数,从而实现多态(调用了同一个函数Func却有不同的效果)。
总结:指针指向谁,调用时就实现谁。
动态绑定和静态绑定
静态绑定:对不满足多态条件的函数调用,在编译的时候就绑定好,编译时就已经确定了要调用的地址。
动态绑定:对满足多态条件的函数调用,在运行的时候才绑定,也就是运行时到指定对象的虚函数表中查询要调用的地址来调用。
虚函数表原理
①父类对象的虚函数表中存放着父类所有虚函数的地址。
②子类由继承的父类和自己的成员组成,子类的虚表在继承的父类成员里,但是和父类的虚表不是相同的。
③子类重写了父类的虚函数后,就会用重写后的虚函数覆盖虚函数表里的函数。没重写就不覆盖。
④子类的虚函数表包括父类的虚函数地址、重写父类的虚函数地址、自己的虚函数地址(但是vs监视窗口会隐藏)。
⑤同类型对象使用的虚函数表是一样的。如Person p1和Person p2使用的是同一个虚表,不会再另外开辟空间。
⑥一般虚表这个数组最后会放一个0x00000000进行标记(vs有放,g++没放,具体看编译器定义)。
⑦虚函数存放在代码段(也叫常量区)。在vs中,虚表也存放在代码段(虚表存放C++标准没要求)。
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } virtual void func1() { cout << "func1" << endl; } void func2() { cout << "func2" << endl; } int _a1 = 1; int _a2 = 2; }; class Student : public Person { public: virtual void Buyticket() { cout << "买票-打折" << endl; } virtual void func1() { cout << "func1" << endl; } virtual void func3() { cout << "func2" << endl; } int _a3 = 3; int _a4 = 4; }; int main() { Person p; Student st; return 0; }运行后,启动监视窗口

可见父子类对象的虚函数表地址是不一样的,但是继承下来的_a1和_a2确实一样的。
补充:虽然函数名可以代表函数指针,但是取成员函数的地址要加取地址符号,这是语法规定,nowhy。
❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤