前言
多态也是三大面向对象语言的特性之一,同时我也觉得他也蛮有意思的
与封装“一个方法,多个接口”差别的是,多态可以实现 “一个接口,多种方法”
调用同名函数时,可以根据差别的对象(父类对象或子类对象)调用属于自己的函数,实现差别的方法,因此 多态 的实现依赖于 继承
一、多态的概念
在使用多态的代码中,差别对象完成同一件事会产生差别的效果
比如在购买高铁票时,普通人原价,门生半价,而军人可以优先购票,对于 购票 这一雷同的动作,需要 根据差别的对象提供差别的方法
二、多态的定义与实现
两个必要条件
- virtual 修饰后形成的虚函数,与其他类中的虚函数形成 重写(三同:返回值、函数名、参数均雷同)
- 必须通过【父类指针】或【父类引用】进行虚函数调用
虚函数
被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();
- }
-
- // 指针
- //void Func(Person* p)
- //{
- // p->BuyTicket();
- //}
-
- // 非引用指针,调用父类
- //void Func(Person p)
- //{
- // p.BuyTicket();
- //}
复制代码 测试效果:
重写的三个例外
派生类重写基类虚函数时,与基类虚函数返回值范例差别。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
这个相识一下就行,现实我感觉挺没啥用处的
假如你也有这种感觉,鼓励你致电老本,去好好批斗他!
- class A {};
- class B : public A {};
-
- class Person
- {
- public:
- // 协变 返回值可以是父子类对象指针或引用
- //virtual A* BuyTicket() // 返回值是父类指针
- virtual Person* BuyTicket()
- {
- cout << "Person-> 买票-全价" << endl;
- return nullptr;
- }
- };
- class Student : public Person
- {
- public:
- //virtual B* BuyTicket()// 返回值是子类指针
- virtual Student* BuyTicket()
- {
- cout << "Student-> 买票-半价" << endl;
- return nullptr;
- }
- };
复制代码 假如基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,固然基类与派生类析构函数名字差别。固然函数名不雷同,看起来违背了重写的规则,实在否则,这里可以理解为编译器对析构函数的名称做了特别处理惩罚,编译后析构函数的名称统一处理惩罚成destructor
- class Person
- {
- public:
- // 析构函数名不同,构成重写,编译器将析构函数名字统一处理成destructor
- virtual ~Person()
- {
- cout << "~Person()" << endl;
- }
- };
- class Student : public Person
- {
- public:
- virtual ~Student()
- {
- cout << "delete[]" << _ptr << endl;
-
- delete[] _ptr;
- cout << "~Student()" << endl;
- }
- private:
- int* _ptr = new int[10];
- };
- void Func(Person& p)
- {
- p.BuyTicket();
- }
- int main()
- {
- // 正常情况调用析构没有问题
- //Person p;
- //Student s;
- //Func(p);
- //Func(s);
-
- // 派生类有动态开辟的内存,需要调用多态
- // 指向谁调用谁
- Person* p1 = new Person;
- Person* p2 = new Student;
-
- delete p1;
- delete p2;
-
- return 0;
- }
复制代码
- class Person
- {
- public:
- virtual ~Person()
- {
- cout << "~Person()" << endl;
- }
- };
-
- class Student : public Person
- {
- public:
- // 派生类virtual关键字省略
- ~Student()
- {
- cout << "~Student()" << endl;
- }
- };
复制代码
override 和 final
C++对函数重写的要求比较严酷,但是有些情况下由于疏忽,大概会导致函数名字母次序写反而无法构成重载
- final:修饰虚函数,表示该虚函数不能再被重写
- override: 检查派生类虚函数是否重写了基类某个虚函数,假如没有重写编译报错
- // final 修饰虚函数,不能重写
- class Car
- {
- public:
- // 加了final关键字,虚函数不能被重写
- virtual void Drive() final {}
- };
- class Benz :public Car
- {
- public:
- virtual void Drive() { cout << "Benz-舒适" << endl; }
- };
- int main()
- {
- Benz b;
- return 0;
- }
复制代码
重载、重写(覆盖)、重定义(隐藏)
三、抽象类
概念
在虚函数的背面写上 = 0 ,则这个函数为纯虚函数。包罗纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才气实例化出对象。纯虚函数规范了派生类必须重写,别的纯虚函数更表现出了接口继承
- class Car
- {
- public:
- // 纯虚函数 强制派生类重写虚函数
- virtual void Drive() = 0;
- };
- int main()
- {
- Car c;
- return 0;
- }
复制代码
- 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()
- {
- // Car c;
- Benz b1;
- BMW b2;
-
- // 基类可以定义指针 指向谁调用谁
- Car* ptr1 = &b1;
- Car* ptr2 = &b2;
-
- ptr1->Drive();
- ptr2->Drive();
-
- return 0;
- }
复制代码
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目标是为了重写,达成多态,继承的是接口。以是假如不实现多态,不要把函数定义成虚函数
四、多态的原理
在讲解原理之前,不如我们先来看这么一段神奇代码
- #include <iostream>
- using namespace std;
- class Test
- {
- virtual void func() {};
- };
- int main()
- {
- Test t; //创建一个对象
- cout << "Test sizeof(): " << sizeof(t) << endl;
-
- return 0;
- }
复制代码 大概你会觉得没有对象,会觉得是0,但是你忽然想起了之前讲过的空类也占内存空间,你大概会想是不是1
但是实在都错了,真相是4/8(取决于你的体系是32位照旧64位),大概我这么一说,你也猜到了实在有一个隐藏变量,且范例是指针范例
实在,就是靠着这个虚表指针和虚表实现了多态
虚表和虚表指针
在 vs 的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表
虚函数表中存储的是虚函数指针,可以在调用函数时根据差别的地址调用差别的方法
大概有点混,有三个“虚”,大家别被整虚了!
虚表指针指向虚表,虚表里面存放着虚函数指针,以是虚表的本质实在是个函数指针数组
接下来我会给出一段代码,在该代码中父类 Person 有两个虚函数(func3 不是虚函数),子类 Student 重写了 func1 这个虚函数,同时新增了一个 func4 虚函数
- #include <iostream>
- using namespace std;
- class Person
- {
- public:
- virtual void func1() { cout << "Person::fun1()" << endl; };
- virtual void func2() { cout << "Person::fun2()" << endl; };
- void func3() { cout << "Person::fun3()" << endl; }; //fun3 不是虚函数
- };
- class Student : public Person
- {
- public:
- virtual void func1() { cout << "Student::fun1()" << endl; };
- virtual void func4() { cout << "Student::fun4()" << endl; };
- };
- int main()
- {
- Person p;
- Student s;
-
- return 0;
- }
复制代码
- //打印虚表
- typedef void(*VF_T)();
- void PrintVFTable(VF_T table[]) //也可以将参数类型设为 VF_T*
- {
- //vs中在虚表的结尾处添加了 nullptr
- //如果运行失败,可以尝试清理解决方案重新编译
- int i = 0;
- while (table[i])
- {
- printf("[%d]:%p->", i, table[i]);
- VF_T f = table[i];
- f(); //调用函数,相当于 func()
- i++;
- }
- cout << endl;
- }
- int main()
- {
- //提取出虚表指针,传递给打印函数
- Person p;
- Student s;
- //第一种方式:强转为虚函数地址(4字节)
- PrintVFTable((VF_T*)(*(int*)&p));
- PrintVFTable((VF_T*)(*(int*)&s));
- return 0;
- }
复制代码 子类重写后的虚函数地址与父类差别
由于平台差别指针巨细差别,因此上述传递参数的方式(VF_T * )( * (int * )&p 具有肯定的范围性
假设在 64 位平台下,需要更改为 (VF_T * )( * (long long * )&p
- //64 位平台下指针大小为 8字节
- PrintVFTable((VF_T*)(*(long long*)&p));
- PrintVFTable((VF_T*)(*(long long*)&s));
复制代码 除此之外还可以间接将虚表指针转为 VF_T* 范例进行参数传递
- //同时适用于 32位 和 64位 平台
- PrintVFTable(*(VF_T**)&p);
- PrintVFTable(*(VF_T**)&s);
复制代码 传递参数时的范例转换路径
不能直接写成 PrintVFTable((VF_T*)&p);,由于此时取的是整个虚表区域的首地址地址,无法定位我们所需要虚表的首地址,打印时会堕落
综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系
虚表是在 编译 阶段生成的
虚表指针是在构造函数的 初始化列表 中初始化的
虚表一样平常存储在 常量区(代码段),有的平台中大概存储在 静态区(数据段)
- int main()
- {
- //验证虚表的存储位置
- Person p;
- Student s;
- int a = 10; //栈
- int* b = new int; //堆
- static int c = 0; //静态区(数据段)
- const char* d = "xxx"; //常量区(代码段)
- printf("a-栈地址:%p\n", &a);
- printf("b-堆地址:%p\n", b);
- printf("c-静态区地址:%p\n", &c);
- printf("d-常量区地址:%p\n", d);
- printf("p 对象虚表地址:%p\n", *(VF_T**)&p);
- printf("s 对象虚表地址:%p\n", *(VF_T**)&s);
- return 0;
- }
复制代码
显然,虚表地址与常量区的地址非常接近,因此可以推测 虚表 位于常量区中,由于它需要被同一类中的差别对象共享,同时不能被修改(如同代码一样)
虚函数调用过程
综上,我们可以大概想象出多态的原理了:
- 起首确保存在虚函数且构成重写
- 其次使用【父类指针】或【父类引用】指向对象,其中包罗切片行为
- 切片后,将子类中不属于父类的切掉,只保存父类指针可调用到的部门函数
- 现实调用时,父类指针的调用逻辑是一致的:比如虚表第一个位置调用第一个函数,虚表第二个位置调用第二个函数,但是由于此时的虚表是切片得到的,以是 同一位置 可以调用到差别的函数,这就是多态
也就是说,父类和子类的虚表实在是不一样的,在构成重写的条件下!
这就是多态!
- int main()
- {
- Person* p1 = new Person();
- Person* p2 = new Student();
- p1->func1();
- p2->func1();
- delete p1;
- delete p2;
- return 0;
- }
复制代码 通过汇编代码来看的话:
动态绑定与静态绑定
实在我们想一想,函数重载某种程度上也是一种多态,也是一个函数面临差别对象的时候有差别的效果,但是差别的是,重载在编译的时候就确定了待调用函数的地址,而动态绑定的代码,待调用地址存放在 eax 中,不确定
五、那…那单继承甚至多继承呢?
坦率说,这很麻烦,我也不敢说我很懂,于是我在这里贴两篇文章,大家自行参阅吧!
《C++虚函数表解析》
《C++对象的内存布局》
总结
我们终于学完三大面向对象特性了,坦率说,多态照旧蛮困难的,但是,我们难度的最高峰再过几篇就要来了,怕不怕!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |