|
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据布局详解】【C++详解】
目次
1. 多态的概念
1.1 概念
2. 多态的界说及实现
2.1 多态的构成条件
2.2 虚函数
2.3 虚函数的重写
2.4 C++11 override 和 final
2.5 重载、覆盖(重写)、潜伏(重界说)的对比
3. 抽象类
3.1 概念
3.2 接口继承和实现继承
4.多态的原理
4.1 虚函数表
4.2 多态的原理
5.单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
5.2 多继承中的虚函数表
5.3. 菱形继承、菱形捏造继承
1. 多态的概念
1.1 概念
多态的概念:平凡来说,就是多种形态,详细点就是去完成某个举动,当差异的对象去完成时会
产生出差异的状态。
举个栗子:比如买票这个举动,当平凡人买票时,是全价买票;弟子买票时,是半价买票;武士
买票时是优先买票。
2. 多态的界说及实现
2.1 多态的构成条件
多态是在差异继承关系的类对象,去调用同一函数,产生了差异的举动。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态另有两个条件:
- 1. 必须通过基类的指针大概引用调用虚函数
- 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数举行重写
2.2 虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
注意:
全局虚函数没故意义,由于虚函数是为多态而用的。
- virtual void BuyTicket()
- {}
复制代码 报错信息
父类虚函数实现
- class Person
- {
- public:
- virtual void BuyTicket() { cout << "买票-全价" << endl;}
- };
复制代码 2.3 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全类似的虚函数(即派生类虚函数与基类虚函数的
返回值范例、函数名字、参数列表完全类似(范例类似即可)),称子类的虚函数重写了基类的虚函数。
基类
- 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();
- //}
复制代码 主函数
- int main()
- {
- Person p;
- Student s;
- Func(p);
- Func(s);
- //Func(&p);
- //Func(&s);
- return 0;
- }
复制代码 测试结果
虚函数重写的三个例外:
1、协变(基类与派生类虚函数返回值范例差异)
派生类重写基类虚函数时,与基类虚函数返回值范例差异。即基类虚函数返回基类对象的指针大概引用,派生类虚函数返回派生类对象的指针大概引用时,称为协变。
- 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;
- }
- };
复制代码 2、析构函数的重写(基类与派生类析构函数的名字差异)
假如基类的析构函数为虚函数,此时派生类析构函数只要界说,无论是否加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;
- }
复制代码 测试结果
3、派生类重写虚函数virtual关键字可以省略
- class Person
- {
- public:
- virtual ~Person()
- {
- cout << "~Person()" << endl;
- }
- };
- class Student : public Person
- {
- public:
- // 派生类virtual关键字省略
- ~Student()
- {
- cout << "~Student()" << endl;
- }
- };
复制代码 测试结果
2.4 C++11 override 和 final
从上面可以看出,C++对函数重写的要求比力严酷,但是有些环境下由于疏忽,大概会导致函数名字母序次写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在步伐运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以资助用户检测是否重写。
1. final:修饰虚函数,体现该虚函数不能再被重写
- // 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;
- }
复制代码
一个类不能被继承,应该怎么实现呢?
方式一:C++98方法
将父类的构造函数界说为私有,派生类则不能访问父类的构造函数。
- class Car
- {
- public:
- private:
- // C++98方法:父类的构造函数私有
- // 子类的构造函数不能生成和实现,导致无法实例化
- // 缺陷:不实例化派生类对象不会报错
- Car();
- };
- class Benz : public Car
- {
- public:
- };
- int main()
- {
- Benz b;
- return 0;
- }
复制代码
方式二:C++11方法
将父类界说为终极类,终极类不能被继承。
- // final修饰的类为最终类,不能被继承
- // 相比于C++98,C++11直接不允许继承
- class Car final
- {
- public:
- };
- class Benz : public Car
- {
- public:
- };
- int main()
- {
- //Benz b;
- return 0;
- }
复制代码
2. override: 查抄派生类虚函数是否重写了基类某个虚函数,假如没有重写编译报错。
- // override 判断是否进行了重写
- class Car {
- public:
- virtual void Drive() {}
- };
- class Benz :public Car {
- public:
- // 判断虚函数是否重写,重写则编译正常,没有重写则报错
- virtual void Drive() override { cout << "Benz-舒适" << endl; }
- };
- int main()
- {
- Benz b;
- return 0;
- }
复制代码
2.5 重载、覆盖(重写)、潜伏(重界说)的对比
3. 抽象类
3.1 概念
在虚函数的反面写上 =0 ,则这个函数为纯虚函数。包罗纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才华实例化出对象。纯虚函数规范了派生类必须重写,别的纯虚函数更体现出了接口继承。
- class Car
- {
- public:
- // 纯虚函数 强制派生类重写虚函数
- virtual void Drive() = 0;
- };
- int main()
- {
- Car c;
- return 0;
- }
复制代码
- // 纯虚函数 不能实例化出对象 C++98
- // 间接强制派生类重写虚函数
- // override 已经重写了,帮助检查语法是否有问题 C++11
- 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;
- }
复制代码
3.2 接口继承和实现继承
平凡函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,告竣多态,继承的是接口。以是假如不实现多态,不要把函数界说成虚函数。
4.多态的原理
4.1 虚函数表
- // 计算Base类的大小
- class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Func1()" << endl;
- }
- private:
- // 有虚函数就有虚函数表指针,虚表指针
- int _b = 1;
- char _c = 'x';
- };
- int main()
- {
- cout << sizeof(Base) << endl;
- Base b;
- return 0;
- }
复制代码 通过观察测试我们发现b对象是12bytes,除了_b,_c成员,还多一个__vfptr放在对象的前面(注意有些平台大概会放到对象的末了面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,由于虚函数
的地点要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们
接着往下分析
针对上面的代码我们做出以下改造
- 1.我们增长一个派生类Derive去继承Base
- 2.Derive中重写Func1
- 3.Base再增长一个虚函数Func2和一个平凡函数Func3
- class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Base::Func1()" << endl;
- }
- virtual void Func2()
- {
- cout << "Base::Func2()" << endl;
- }
- void Func3()
- {
- cout << "Base::Func3()" << endl;
- }
- private:
- int _b = 1;
- };
- class Derive : public Base
- {
- public:
- virtual void Func1()
- {
- cout << "Derive::Func1()" << endl;
- }
- private:
- int _d = 2;
- };
- int main()
- {
- Base b;
- return 0;
- }
复制代码
通过观察和测试,我们发现了以下几点题目:
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是本身的成员。
2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,以是d的虚表中存的是重写的Derive::Func1,以是虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 别的Func2继承下来后是虚函数,以是放进了虚表,Func3也继承下来了,但是不是虚函数,以是不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一样平常环境这个数组末了面放了一个nullptr。
5. 总结一下派生类的虚表天生:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.假如派生类重写了基类中某个虚函数,用派生类本身的虚函数覆盖虚表中基类的虚函数 c.派生类本身新增长的虚函数按其在派生类中的声明序次增长到派生类虚表的末了。
6. 这里另有一个童鞋们很容易肴杂的题目:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的复兴的错的。但是许多童鞋都是如许深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和平凡函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。别的对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段(常量区)的。
- // 设计一个程序,判断虚表存放在哪个内存区域
- int main()
- {
- int i = 0;
- static int j = 1;
- int* p1 = new int;
- const char* p2 = "xxxx";
- printf("栈:%p\n", &i);
- printf("堆:%p\n", p1);
- printf("静态区:%p\n", &j);
- printf("常量区:%p\n", p2);
- Person p;
- Student s;
- Person* p3 = &p;// Person对象的地址
- Student* p4 = &s;
- // 与常量区相隔最近
- printf("Person虚表地址:%p\n", *(int*)p3);
- printf("Student虚表地址:%p\n", *(int*)p4);
- return 0;
- }
复制代码
4.2 多态的原理
上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket
- class Person
- {
- public:
- virtual void BuyTicket()
- {
- cout << "Person-> 买票-全价" << endl;
- }
- };
- class Student : public Person
- {
- public:
- virtual void BuyTicket()
- {
- cout << "Student-> 买票-半价" << endl;
- }
- };
- // 多态调用,运行时,到指向对象的虚表中找函数调用,指向父类调用父类
- // 普通调用,编译时,调用对象是哪个类型就调用哪个
- // 虚表(虚函数表) 存的虚函数,目的实现多态
- // 虚基表 存的当前位置距离虚基类部分的偏移量,解决菱形继承的数据冗余和二义性
- void Func(Person& p)
- {
- p.BuyTicket();
- }
- int main()
- {
- Person Mike;
- Student Johnson;
- Func(Mike);
- Func(Johnson);
- return 0;
- }
复制代码 满足多态:
运行时,到指向对象的虚表中找函数调用,指向父类调用父类,指向子类调用子类。
不满足多态(去掉父类virtual关键字则不满足多态)
编译时,调用对象是哪个范例就调用哪个范例的函数。
5.单继承和多继承关系的虚函数表
必要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模子,由于基类的虚表模子前面我们已经看过了,没什么必要特殊研究的。
5.1 单继承中的虚函数表
- class Base {
- public:
- virtual void func1() { cout << "Base::func1" << endl; }
- virtual void func2() { cout << "Base::func2" << endl; }
- private:
- int a;
- };
- class Derive :public Base {
- public:
- Derive()
- {}
- // 重写func1 继承func2 自己类的func3 4
- virtual void func1() { cout << "Derive::func1" << endl; }
- virtual void func3() { cout << "Derive::func3" << endl; }
- virtual void func4() { cout << "Derive::func4" << endl; }
- private:
- int b = 0;
- };
复制代码 观察下图中的监督窗口中我们发现看不见func3和func4。这里是编译器的监督窗口故意潜伏了这两个函数,也可以以为是他的一个小bug。那么我们怎样查察d的虚表呢?下面我们使用代码打印出虚表中的函数。
前面的解说中我们知道虚表实质是一个函数指针数组,用来存放函数指针的,一样平常环境这个数组末了面放了一个nullptr,因此我们通过函数指针数组的首地点(派生类第一个成员)则可打印出虚函数表中全部函数的地点。
我们先回首一下函数指针与函数指针数组
- // 函数指针
- void (*p)();
- // 函数指针数组
- void (*pa[10])();
复制代码 为了更好的使用函数指针,一样平常我们会将使用typedef将函数指针重定名,由于什么的虚函数返回值为void,参数为空,因此我们可以将函数指针重定名为:
- // 重命名函数指针
- typedef void(*VFTPTR)();
- // 错误示范
- typedef void(*)() VFTPTR;
复制代码 注意:
函数指针使用typedef重定名的名字必要放在指针右边。
打印虚函数表函数
- // 重命名函数指针
- typedef void(*VFTPTR)();
- // 打印虚表(函数指针数组)
- //void PrintVFT(VFTPTR ptr[])
- void PrintVFT(VFTPTR* ptr)
- {
- //for (size_t i = 0; i < 4; i++)// 仅适用于知道有几个虚函数情况,有局限性
- for (size_t i = 0; ptr[i] != nullptr; i++)// 任意情况都适用
- {
- printf("%p->", ptr[i]);
- VFTPTR pf = ptr[i];
- (*pf)();// 调用函数
- // pf();// 也可以这样调用函数
- }
- }
复制代码 怎样获取虚函数表首元素地点呢?
思绪:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组末了面放了一个nullptr
// 1.先取b的地点,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFTPTR*,由于虚表就是一个存VFTPTR范例(虚函数指针范例)的数组。
// 4.虚表指针通报给PrintVFT举行打印虚表
// 5.必要分析的是这个打印虚表的代码常常会瓦解,由于编译器偶然对虚表的处理处罚不干净,虚表末了面没有放nullptr,导致越界,这是编译器的题目。我们只必要点目次栏的-天生-整理办理方案,再编译就好了。
- int main()
- {
- Base b;// Base 基类
- Derive d;// Derive 派生类
- // 监视窗口查看Derive虚基表函数只能看到两个
- // 内存可以看到4个函数地址,但是不能证明是否为虚表的地址
- // 函数指针
- void (*p)();
- // 函数指针数组
- void (*pa[10])();
- // void func()函数 没参数 返回值为void
- // 派生类的第一个成员存放的是函数指针,x86为4字节的指针
- VFTPTR* ptr = (VFTPTR*)(*(int*)&d);
- PrintVFT(ptr);
- ptr = (VFTPTR*)(*(int*)&b);
- PrintVFT(ptr);
- return 0;
- }
复制代码
5.2 多继承中的虚函数表
- // 多继承
- class Base1 {
- public:
- // 生成一个虚表
- virtual void func1() { cout << "Base1::func1" << endl; }
- virtual void func2() { cout << "Base1::func2" << endl; }
- private:
- int b1;
- };
- class Base2 {
- public:
- // 生成一个虚表
- virtual void func1() { cout << "Base2::func1" << endl; }
- virtual void func2() { cout << "Base2::func2" << endl; }
- private:
- int b2;
- };
- class Derive : public Base1, public Base2 {
- public:
- // func1重写两个类 func3地址存放在第一个虚表中
- virtual void func1() { cout << "Derive::func1" << endl; }
- virtual void func3() { cout << "Derive::func3" << endl; }
- private:
- int d1;
- };
复制代码 打印函数
- typedef void(*VFTPTR)();
- void PrintVFT(VFTPTR* ptr)
- {
- cout << "虚表地址>" << ptr << endl;
- // 满足任意类打印需要改变判断条件
- for (size_t i = 0; ptr[i] != nullptr; i++)
- {
- printf("第%d个虚表的地址:0x%p", i, ptr[i]);
- VFTPTR pf = ptr[i];
- (*pf)();
- }
- }
复制代码 主函数
- int main()
- {
- // 计算多继承之后派生类的大小
- cout << sizeof(Derive) << endl;
- Base1 b1;
- Base2 b2;
- Derive d;
- // 查看三个类的虚表
- // 第一个虚表
- VFTPTR* ptr1 = (VFTPTR*)(*(int*)&d);
- PrintVFT(ptr1);
- //VFTPTR* ptr2 = (VFTPTR*)(*(int*)((char*)&d + sizeof(Base1)));
- Base2* ptr = &d;
- VFTPTR* ptr2 = (VFTPTR*)(*(int*)ptr);
- PrintVFT(ptr2);
- return 0;
- }
复制代码 观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
5.3. 菱形继承、菱形捏造继承
- class A
- {
- public:
- virtual void func1() { cout << "A::func1" << endl; }
- int _a;
- };
- //class B : public A // 菱形继承
- class B : virtual public A // 菱形虚拟继承
- {
- public:
- virtual void func2() { cout << "B::func2" << endl; }
- int _b;
- };
- //class C : public A // 菱形继承
- class C : virtual public A // 菱形虚拟继承
- {
- public:
- virtual void func3() { cout << "C::func3" << endl; }
- int _c;
- };
- class D : public B, public C
- {
- public:
- virtual void func4() { cout << "D::func4" << endl; }
- int _d;
- };
复制代码- // 菱形继承
- int main()
- {
- D d;
- cout << sizeof(d) << endl;
- // 结论菱形继承的对象模型跟多继承类似
- d.B::_a = 1;
- d.C::_a = 2;
- d._b = 3;
- d._c = 4;
- d._d = 5;
- return 0;
- }
复制代码 菱形继承
菱形捏造继承
实际中我们不发起操持出菱形继承及菱形捏造继承,一方面太复杂容易出题目,另一方面如许的模子,访问基类成员有肯定得性能斲丧。以是菱形继承、菱形捏造继承我们的虚表我们就不看了,一样平常我们也不必要研究清晰,由于实际中很少用。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金 |