彩票网站开发系统,群晖搭建wordpress不加端口,建站运营新闻,光明做网站目录
前言#xff1a;
1.多态的概念
2.多态的定义及实现 2.1多态的构成条件 2.2析构函数的重写#xff08;基类与派生类析构函数名字不同#xff09;
2.3虚函数重写
2.4C override 和final
2.5 重载、覆盖#xff08;重写#xff09;隐藏#xff08;重定义#…
目录
前言
1.多态的概念
2.多态的定义及实现 2.1多态的构成条件 2.2析构函数的重写基类与派生类析构函数名字不同
2.3虚函数重写
2.4C override 和final
2.5 重载、覆盖重写隐藏重定义的对比
3.多态的原理
3.1虚表与续表指针
3.2动态绑定与静态绑定
4单继承与多继承
4.1单继承中虚表
4.2多继承中虚表
4.2.1子类新增虚表归属问题 4.2.2多继承虚函数调用问题
4.3菱形继承多态与菱形虚拟继承多态 前言
上一章节对面向对象三大特性的继承做了知识复盘本章节对最后一个特性多态做一个知识梳理和总结。
1.多态的概念 通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会 产生出不同的状态。可以举个现实中车站买票的例子同一个窗口不同年龄多不同职业的对象去买票价格是不同的这就是多种形态也就是多态 2.多态的定义及实现 2.1多态的构成条件 实现多态需要借助虚表这里的虚表指的是虚函数表虽然也是借助关键字virtual 这里需要和继承里面的虚拟继承区分开两个概念不能搞混。有了虚函数表就可以使用父类指针进行不同对象调用实现不同形态接下来会仔细介绍 继承中构成多态的两个条件 1-必须通过基类的指针或者引用调用虚函数 2- 被调用的函数必须是虚函数 且派生类必须对基类的虚函数进行重写 我们通过一个买票的demo 理清楚多态的流程
class Person
{
public:virtual void BuyTicket() { cout 全价票 endl; }};
class Student :public Person
{
public:virtual void BuyTicket() { cout 半价票 endl; }};void Func(Person person)
{person.BuyTicket();
}
int main()
{Person p;Student s;Func(p);Func(s);return 0;} 注意除了上面提到的构成多态的两个必要条件有两个例外是需要注意的
除父类外其他子类中的函数不必使用 virtual 修饰此时仍然能构成多态注意三同需要构成重写父子类中的虚函数返回值可以不相同但此时需要返回对应的父类指针或子类指针确保构成多态这一现象称为 协变了解
class A{};
class B : public A {};
class Person {
public:virtual A* f() {return new A;}
};
class Student : public Person {
public:virtual B* f() {return new B;}
}; 2.2析构函数的重写基类与派生类析构函数名字不同
1-析构函数可以是虚函数吗为什么需要是虚函数 2-析构函数加virtual是不是虚函数重写 答案是肯定的如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor。 为什么要这么处理呢是要要让他们构成重写吗 那为什么要让他们构成重写呢我们可以用一个demo 来解释为什么要这么做
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }~Person() { cout ~Person() endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }~Student() {cout ~Student() endl;delete[] ptr;}protected:int* ptr new int[10];
};int main()
{//Person p;//Student s;Person* p new Person;p-BuyTicket();delete p;p new Student;p-BuyTicket();delete p; // p-destructor() operator delete(p)// 这里我们期望p-destructor()是一个多态调用而不是普通调用return 0;
} 此时 只有BuyTicket函数和父类构成了虚函数重写且都是由父类指针进行的调用所以我们会看到买票的多态但是析构函数并未构成虚函数重写既不是虚函数也不是重写再调用delete p的时候他只是一个普通对象当前类型为Person* 所以只会去调用父类的析构从而造成内存泄漏。 虽然编译器对析构函数名称做了特殊处理编译后嘻哈猴函数的名称统一处理成 destructor
我们还是希望p-destrctor能够是一个多态调用而不是普通调用那就是构成虚函数重写所以我们就能理解为什么父类成员必须加上virtual。修改完成后就不会造成内存泄漏了代码如下 总结
如何快速判断是否构成多态
首先观察父类的函数中是否出现了 virtual 关键字其次观察是否出现虚函数重写现象三同返回值、函数名、参数协变例外最后再看调用虚函数时是否为【父类指针】或【父类引用】
父类指针或引用调用函数时如何判断函数调用关系
若满足多态看其指向对象的类型调用这个类型的成员函数不满足多态看具体调用者的类型进行对应的成员函数调用
2.3虚函数重写 通过上面的介绍我们知道虚函数是构成多态的必要条件我们也知道想要构成多态还需要实现重写可是重写具体怎么实现我们好像一笔带过下面我将用代码虚函数表的具体演示派生类如何实现覆盖重写以及重写了什么
#include iostreamusing namespace std;class A
{
public:virtual void func(int val 1) { cout A: val endl; }
};class B : public A
{
public:virtual void func(int val 2) { cout B: val endl; }
};int main()
{A* p new B();p-func();return 0;
} 结果解析初始化两个对象的时候子类继承父类且都是是虚函数会创建两张虚函数表我们发现虚表指针的地址不一样所以第一步是两张虚表当使用父类指针调用的时候就会去完成重写将父类的虚表复制将自己的虚函数函数进行覆盖但是重写的是实现方法也就是外壳内容是不会改变的所以最后的结果就是 B:1
补充 我们已知多态的条件之一就是父类的指针或者引用去调用那为什么不能子类的指针或者引用去调用呢为啥不能是父类对象呢 答1因为是复制父类的虚表进行重写如果是父类调用父类就不用重写父类调用子类就重写子类属于自己的那部分如果用子类指针 永远无法调用到父类 2 子类赋值给父类对象切片不会拷贝虚表如果拷贝虚表那么父类对象虚表中是父类虚函数还是子列就不确定会乱套。
2.4C override 和final C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名字母次序写反而无法构成重载而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来debug会得不偿失因此C11提供了override和final两个关键字可以帮助用户检测是否重写。
final修饰虚函数表示该虚函数不能再被重写对于父类的虚函数如果加上final就不能被重写也就无法实现多态 override 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错 2.5 重载、覆盖重写隐藏重定义的对比 截至目前为止我们已经学习了三个 “重” 相关函数知识重载、重写、重定义
这三兄弟不止名字很像而是功能也都差不多很多面试题中也喜欢考这三者的区别
重载即函数重载函数参数 不同而触发不同的 函数参数 最终修饰结果不同确保链接时不会出错构成重载
重写覆盖发生在类中当出现虚函数且符合重写的三同原则时则会发生重写覆盖行为具体表现为 父类虚函数接口 子类虚函数体是实现多态的基础
重定义隐藏发生在类中当子类中的函数名与父类中的函数名起冲突时会隐藏父类同名函数默认调用子类的函数可以通过 :: 指定调用
重写和重定义比较容易记混简言之 先看看是否为虚函数如果是虚函数且三同则为重写若不是虚函数且函数名相同则为重定义 3.多态的原理 之前提到过多态需要虚函数表以及指向虚函数标的指针我们可以写一个空类通过测试大小验证一下
class Parent
{virtual void func() {};
};int main()
{Parent p; cout Parent : sizeof(p) endl;return 0;
}通过验证我们发现一个带有虚函数的类的大小在64位平台下是8因此也就验证了我们猜想虚函数的类中包含一个虚表指针 。虚表指针-虚表 实现多态。 3.1虚表与续表指针 虚函数表虚表即 virtual function table - vft指向虚表的指针称为 虚表指针 virtual function pointer - vfptr在 vs 的监视窗口中可以看到涉及虚函数类的对象中都有属性 __vfptr虚表指针可以通过虚表指针所指向的地址找到对应的虚表。虚函数表中存储的是虚函数地址可以在调用函数时根据不同的地址调用不同的方法。 在下面这段代码中父类 Person 有两个虚函数func3 不是虚函数子类 Student 重写了 func1 这个虚函数同时新增了一个 func4 虚函数
#include iostreamusing 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;
}如何通过程序验证虚表的真实性
虚表指针指向虚表虚表中存储的是虚函数地址而 64 位平台中指针大小为 8字节因此可以先将虚表指针强转为 指向首个虚函数 的指针然后遍历虚表打印各个虚函数地址验证即可。vs 中对虚表做了特殊处理在虚表的结尾处放了一个 nullptr因此下面这段代码可能在其他平台中跑不了。 typedef void (*VF_T)();//函数指针 为下面函数指针数组做铺垫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; };
};
void test(VF_T table[])
{int i 0;while(table[i]){printf( [%d]:%p-, i, table[i]);//虚函数表里面存的是虚函数地址 直接解引用就是该虚函数VF_T f table[i];f();i;}cout endl;
}
int main()
{Person p;Student s;test((VF_T*)(*(int*)p));test((VF_T*)(*(int*)s));return 0;
} 因为平台不同指针大小不同因此上述传递参数的方式(VF_T*)(*(int*)p 具有一定的局限性 假设在 64 位平台下需要更改为 (VF_T*)(*(long long*)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;
}显然虚表地址与常量区的地址十分接近因此可以推测 虚表位于常量区中因为它需要被同一类中的不同对象共享同时不能被修改如同代码一样
函数代码也是位于 常量区代码段可以在监视窗口中观察两者的差异 3.2动态绑定与静态绑定 静态绑定前期绑定/早绑定
在编译时确定程序的行为也称为静态多态
动态绑定后期绑定/晚绑定
在程序运行期间调用具体的函数也称为动态多态 p1-func1();
p2-func1();add(1, 2);
add(1.1, 2.2);简单来说静态绑定就像函数重载在编译阶段就确定了不同函数的调用而动态绑定是虚函数的调用过程需要 虚表指针虚表在程序运行时根据不同的对象调用不同的函数 4单继承与多继承 需要注意的是在单继承和多继承关系中下面我们去关注的是派生类对象的虚表模型因为基类 的虚表模型前面我们已经看过了没什么需要特别研究的。
4.1单继承中虚表
我们上面研究的基本都是子类继承父类对父类中的虚函数进行覆盖重写。 向父类中新增虚函数父类的虚表中会新增同时子类会继承并纳入自己的虚表之中
向子类中新增虚函数只有子类能看到因此只会纳入子类的虚表中父类是看不到并且无法调用的
向父类/子类中添加非虚函数时不属于虚函数不进入虚表仅当作普通的类成员函数处理
4.2多继承中虚表 C 中支持多继承这也就意味着可能出现 多个虚函数重写 的情况当父类指针面临 不同虚表中的相同虚函数重写 时该如何处理呢
#include iostream
using namespace std;//父类1
class Base1
{
public:virtual void func1() { cout Base1::func1() endl; }virtual void func2() { cout Base1::func2() endl; }
};//父类2
class Base2
{
public:virtual void func1() { cout Base2::func1() endl; }virtual void func2() { cout Base2::func2() endl; }
};//多继承子类
class Derive : public Base1, public Base2
{
public:virtual void func1() { cout Derive::func1() endl; }virtual void func3() { cout Derive::func3() endl; } //子类新增虚函数
};int main()
{Derive d;return 0;
}此时derive继承了base1和base2所以derive有两张虚表分别为 Base1 Derive::func1 构成的虚表、Base2 Derive::func1 构成的虚表 此时出现了两个问题
子类 Derive 中新增的虚函数 func3 位于哪张虚表中为什么重写的同一个 func1 函数在两张虚表中的地址不相同
下面我们对这两个问题做一个深度解析。
4.2.1子类新增虚表归属问题 在单继承中子类中新增的虚函数会放到子类的虚表中因为只有一张表我们没有疑问多继承中子类中新增的虚函数默认添加至第一张虚表中我们可以通过test打印进行验证因为此时有两张虚表所以需要分别打印第一张虚表的地址和子类的首地址重合只需要取地址类型强转第二张虚表就比较麻烦需要在第一张虚表的起始地址处跳过第一张虚表的大小然后才能获取第二张虚表的起始地址。 //打印虚表
typedef void(*VF_T)();void test(VF_T table[])
{//vs中在虚表的结尾处添加了 nullptrint i 0;while (table[i]){printf([%d]:%p-, i, table[i]);VF_T f table[i];f(); //调用函数相当于 func()i;}cout endl;
}int main()
{Derive d;test(*(VF_T**)d); //第一张虚表test(*(VF_T**)((char*)d sizeof(Base1))); //第二张虚表return 0;
}可以看出新增的 func3 函数确实在第一张虚表中;可能有的人觉得取第二张虚表的起始地址很麻烦那么可以试试利用 切片 机制天然的取出第二张虚表的地址切片行为是天然的可以完美取到目标地址.
Base2* table2 d; //切片
PrintVFTable(*(VF_T**)table2); //第二张虚表4.2.2多继承虚函数调用问题 在上面的多继承多态代码中子类分别重写了两个父类中的 func1 函数但最终通过监视窗口发现同一个函数在两张虚表中的地址不相同因此可以推测编译器在调用时根据不同的地址寻找到同一函数解决冗余虚函数的调用问题至于实际调用链路还得通过汇编代码展现 ptr2 在调用时的关键语句 sub ecx 4sub 表示减法ecx 通常存储 this 指针4 表示 Base1 的大小这条语句表示将当前的 this 指针向前偏移 sizeof(Base1)后续再 jmp 时调用的就是同一个 func1这一过程称为 this 指针修正用于解决冗余虚函数的调用问题 为什么是 Base2 修正因为先继承了 Base1后继承了 Base2假设先继承的是 Base2那么修正的就是 Base1这种设计很大胆也很巧妙完美解决了多继承多态带来的问题因此回答问题二两张虚表中同一个函数的地址不同是因为调用方式不同后继承类中的虚表需要通过 this 指针修正的方式调用虚函数。
4.3菱形继承多态与菱形虚拟继承多态 菱形继承问题是 C 多继承中的大坑为了解决菱形继承问题提出了 虚继承 虚基表 的相关概念那么在多态的加持之下菱形继承多态变得更加复杂需要函数调用链路设计的更加复杂菱形虚拟继承多态就更不得了需要同时考虑两张表虚表、虚基表
虚基表中空余出来的那一行是用来存储偏移量的表示当前虚基表距离虚表有多远