用dw做的网页怎么连到网站上,淘宝网站是语言用什么做的,百度推广多少钱一天,宿州信息网送给大家一句话#xff1a; 一个犹豫不决的灵魂#xff0c;奋起抗击无穷的忧患#xff0c;而内心又矛盾重重#xff0c;真实生活就是如此。 – 詹姆斯・乔伊斯 《尤利西斯》
_φ(*#xffe3;ω#xffe3;)#xff89;_φ(*#xffe3;ω#xffe3;)… 送给大家一句话 一个犹豫不决的灵魂奋起抗击无穷的忧患而内心又矛盾重重真实生活就是如此。 – 詹姆斯・乔伊斯 《尤利西斯》
_φ(*ω)_φ(*ω)_φ(*ω) _φ(*ω)_φ(*ω)_φ(*ω) _φ(*ω)_φ(*ω)_φ(*ω) 从零开始认识多态 1 前言2 什么是多态3 多态的构成3.1 协变3.2 析构函数的重写3.3 语法细节3.4 C11 override 和 final3.5 重写覆盖 - 重载 - 重定义隐藏 4 多态的底层实现4.1 底层实现4.2 验证虚表 5 抽象类6 多继承中的多态一般的多继承菱形继承和菱形虚拟继承 Thanks♪(ω)谢谢阅读下一篇文章见 1 前言
面向对象技术oop的核心思想就是封装继承和多态。通过之前的学习我们了解了什么是封装什么是继承。
封装就是对将一些属性装载到一个类对象中不受外界的影响比如洗衣机就是对洗衣服功能甩干功能漂洗功能等的封装其功能不会受到外界的微波炉影响。
继承就是可以将类对象进行继承派生类会继承基类的功能与属性类似父与子的关系。比如水果和苹果苹果就有水果的特性。
接下来我们就来了解学习多态
2 什么是多态
多态是面向对象技术OOP的核心思想我们把具有继承关系的多个类型称为多态类型通俗来讲就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态。
举个例子就拿刚刚结束的五一假期买票热为例买票这个行为当普通人买票时是全价买票学生买票时是半价买票军人买票时是优先买票。同样一个行为在不同的对象上就有不同的显现。
多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。 #includeiostreamusing namespace std;class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout 买票-半价 endl; }
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person p;Student s;//同一个函数对不同对象有不同效果Func(p);Func(s);return 0;
}比如Student继承了Person。Person对象买票全价Student对象买票半价。我们运行看看
多态调用运行时到指定对象的虚表中找虚函数来调用指向基类调用基类的虚函数指向子类调用子类的虚函数普通调用编译时调用对象是哪个类型就调用它的函数。
乍一看还挺复杂接下来我们就来了解多态的构成。
3 多态的构成
继承的情况下才有虚函数才有多态 多态构成的条件
必须通过基类的指针或者引用调用虚函数virtual修饰的类成员函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写父子虚函数要求三同 虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数
看起来很是简单当时其实有很多的坑一不小心就会掉进去。
3.1 协变
上面我们说了多态的条件父子虚函数要求三同。但是却有这样一个特殊情况协变 协变派生类重写基类虚函数时与基类虚函数返回值类型不同
基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用
这样的情况称为协变。
#includeiostreamusing namespace std;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; }
};很明显派生类与基类的返回值不同注意一定是基类返回“基类”派生类返回“派生类” 但是结果确实正常的依然构成多态这样的情况就称为协变
3.2 析构函数的重写
析构函数在编译阶段都会转换成destructor所以表面析构函数名字不同但是实质上是一致的。这样就会构成多态。
来看正常情况下的析构 #includeiostream
using namespace std;class Person
{
public:~Person() { cout ~Person() endl; }
};class Student : public Person
{
public:~Student() { cout ~Student() endl; }
};
int main()
{Person p;Student s;return 0;
}
这样会正常的调用析构函数子类析构会自动调用父类析构-先子后父 再来看
int main()
{//Person p;//Student s;//基类可以指向基类 也可以指向派生类的基类部分Person* p1 new Person ;//通过切片来指向对应内容Person* p2 new Student;delete p1;delete p2;return 0;
}如果是这样呢 这样调用的析构不对啊Student对象没有调用自身的析构函数而是调用Person的为什么会出现这样的现象呢
这样就可能会引起一个十分严重的问题内存泄漏
#includeiostreamusing namespace std;class Person
{
public:~Person() { cout ~Person() endl; }
};
class Student : public Person
{
public:Student() { int* a new int[100000000]; }~Student() { cout ~Student() endl; }
};
int main()
{for(int i 0; i 100000 ; i){Person* p2 new Student;delete p2;}return 0;
}
如果我们在Student中申请一个空间而析构的时候却不能调用其析构函数俩把申请的空间free这样就导致了内存泄漏 这就十分危险了 而我们希望的是指向谁就调用谁的析构指向基类调用基类析构指向派生类调用派生类析构。 那我们怎么做到呢 ---- 当然就是多态了 那我们来看看现在满不满足多态的条件
必须通过基类的指针或者引用调用虚函数virtual修饰的类成员函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写父子虚函数要求三同
在编译的时候析构函数都会变成destructor这样满足三同构成重写 那么我们就只需要将析构函数变为虚函数就可以了
class Person
{
public:virtual ~Person() { cout ~Person() endl; }
};
class Student : public Person
{
public:virtual ~Student() { cout ~Student() endl; }
};来运行看看 老铁 OK了应该释放的空间全都释放了
所以建议析构函数设置为虚函数避免出现上述的情况。
3.3 语法细节
派生类(基类必须写)的对应函数可以不写virtual这个语法点非常奇怪建议写上virtual“重写”的本质是重新写函数的实现函数声明包括缺省参数的值与基类一致
来看一道面试题 以下程序输出结果是什么
#includeiostreamusing namespace std;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(int argc, char* argv[])
{B* p new B;p-test();return 0;
}A: A-0 B: B-1 C: A-1 D: B-0 E: 编译出错 F: 以上都不正确答案是B 为什么呢
首先 A类与B类构成继承关系func函数是虚函数B类是派生类可以不写virtual并且AB 中满足三同。构成多态。test函数的参数是基类指针A* this 成员函数的默认参数满足多态条件。 然后 主函数中调用test函数因为B是子类没有test函数所以会在父类A中寻找。test函数调用 func函数参数this指向的是B类(指向谁调用谁)所以就会调用B类的func函数B-重写的本质是对函数的实现进行重写函数的结构部分包括参数缺省值函数名返回值等与基类一致。所以是 1
所以就可以判断是B选项。 当然实际中不能这么写代码奥会有生命危险Doge
3.4 C11 override 和 final
从上面可以看出C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名字母次序写反而无法构成重载而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来debug会得不偿失因此C11提供了override和final两个关键字可以帮助用户检测是否重写。
final 修饰类(最终类)表示该类不能被继承。C98直接粗暴使用private来做到不能继承 class car final { };修饰虚函数表示该虚函数不能再被继承。 virtual void func() final { } override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。
class Car {
public:virtual void Drive() {}
};class Benz :public Car {
public:virtual void Drive() override { cout Benz-舒适 endl; }
};3.5 重写覆盖 - 重载 - 重定义隐藏
我们来区分一下这三个类似的概念
重载 两个函数作用在同一作用域函数名相同参数不同 重写覆盖 两个函数分别在基类作用域好派生类作用域函数名、参数、返回值都一样协变例外两个函数必须是虚函数 重定义 两个函数分别在基类作用域好派生类作用域仅仅函数名相同两个基类和派生类的同名函数不是重写就是重定义 重定义包含重写
4 多态的底层实现
4.1 底层实现
首先我们来看一下具有多态属性的类的大小 #includeiostream
using namespace std;class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;char _ch x;
};int main(int argc, char* argv[])
{cout sizeof(Base) endl;return 0;
}Base的大小在x86环境下是12字节。这十二字节是怎么组成的呢 首先类里面有一个虚函数表指针_vfptr 只要有虚函数就会有虚表指针这个是实现多态的关键 我们来探索一下 通过VS的调试我们可以发现 那么如何实现传基类调用基类的虚函数传派生类调用派生类的虚函数 当然是使用切片了
1. 首先每个实例化的类如果有虚函数会有一个虚函数表。 2. 传基类调用基类的虚函数就正常在基类虚表中寻找其对应函数 3. 传派生类因为多态函数时基类的指针那么就会切片出来一个基类虚函数表是派生类的那么就会在派生类虚表调用对应虚函数。
这样就实现了执行谁就调用谁 运行过程中去虚表中找对应的虚函数调用。具体的汇编语言实现还是比较直白的。
注意同类型的虚表是一样的
满足多态那么运行时汇编指令会去指向对象的虚表中找对应虚函数进行调用不满足多态编译链接时直接根据对象类型确定调用的函数确定地址
这里需要分辨一下两个概念虚表与虚基表
虚表虚函数表存的是虚函数用来形成多态虚基表存的是距离虚基类的位置的偏移量用来解决菱形继承的数据冗余和二义性
注意虚函数不是存在虚表中的 , 虚表中存的是虚函数的指针。那虚函数存在哪里呢 来验证一下
class Person
{
public:virtual ~Person() { cout ~Person() endl; }
};
class Student : public Person
{
public:virtual ~Student() { cout ~Student() endl; }
};int main()
{int i 0;static int j 1;int* p1 new int;const char* p2 xxxxxxxx;printf(栈:%p\n, i);printf(静态区:%p\n, j);printf(堆:%p\n, p1);printf(常量区:%p\n, p2);Person p;Student s;Person* p3 p;Student* p4 s;printf(Person虚表地址:%p\n, *(int*)p3);printf(Student虚表地址:%p\n, *(int*)p4);return 0;
}运行可以看到 虚表地址与常量区最接近那可以推断出虚表储存在常量区
4.2 验证虚表
我们来看
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:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::func4 endl; }
private:int b;
};梳理一下结构
Base作为基类 Derive作为派生类派生类Derive重写了func1函数构成多态其余函数均不构成多态。
然后我们来探索一下
int main()
{Base b;Derive d;return 0;
}通过监视窗口可以查看一下虚表的内容
这是VS调试的一点BUG导致监视中派生类的虚表不能显示。在内存窗口里存在4个函数指针接下来我们来验证一下他们是不是对应的虚函数。
虚函数表本质是一个函数指针数组 那么如何定义一个函数指针和函数指针数组呢
//这样定义
//返回值是void 所以写void
void(*p)( //函数里面的参数 );
void(*p[10])( //函数里面的参数 )当然可以使用typedef来简化这个typedef也很特别
typedef void(*VFPTR)();
VFPTR p1;
VFPTR p2[10];那么如果我们想要打印出虚表我们可以设置一个函数
//因为是函数指针数组所以传参是函数指针的指针int arr[10] 传入 int*。
void PrintVFT(VFPTR* vft )
{for(size_t i 0 ; i 4 ; i){printf(%p\n , vft[i]);}}这样就可以打印了那么现在就需要解决如何获取虚表的首地址。虚表首地址是类的头4个字节x86环境我们如何取出来了呢? 直接把类强转为int类型不就4个字节了吗但是没有联系的类型是不能强转的。那怎么办呢 C/C中指针可以直接互相强转BUG级别的操作,整型与指针也可以互相转换。
VFPTR* p (VFPTR*) *( (int*)d );//这样就变成4个字节了d 是取类的指针int*)d将类指针强转为int*指针*( (int*)d ) 将 int * 解引用为int(VFPTR*) *( (int*)d ) 将int转换为VFPTR*,取到虚表首地址
那么我们来验证一下
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:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::func4 endl; }
private:int b;
};
typedef void(*VFPTR)();void PrintVFT(VFPTR* vft)
{for (size_t i 0; i 4; i){printf(%p -, vft[i]);(*(vft[i]))();}}int main()
{Base b;Derive d;VFPTR* p (VFPTR*)*((int*)d);//这样就变成4个字节了PrintVFT(p);return 0;
}
来看 这样就成功获取到了虚标的内容验证了虚表的内容中存在4个虚函数
5 抽象类
在虚函数的后面写上 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;}
};
void Test()
{//各种调用自身的虚函数Car* pBenz new Benz;pBenz-Drive();Car* pBMW new BMW;pBMW-Drive();
}
int main()
{Test();return 0;
}抽象类与override关键字的区别
抽象类间接强制了派生类必须进行虚函数重写override是在已经重写的情况下帮助进行重写的语法检查
6 多继承中的多态
多继承我们讲过是一种很危险的继承很容易导致菱形继承引起数据冗余和二义性。那么我们再来看看多态在多继承中是然如何实现的 。
一般的多继承
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:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }
private:int d1;
};分析一下继承关系
有两个基类Base1类与Base2类Derive继承两个基类对func1函数进行了重写构成多态
来看看Derive类的大小是多大 我们分析一下:Base1类应该有一个虚表指针和一个int类型数据所以应该为8字节。Base2同理8字节。 那么Derive由于多继承的缘故会包含两个基类所以应该为16 4 20字节 运行一下看来我们的分析没有问题也就是有两张虚表func1重写会改变两个虚表因为两个基类都有func1函数func3是放在Base1的虚表中的通过虚表验证
typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[])
{cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
}
int main()
{Derive d;VFPTR* vTableb1 (VFPTR*)(*(int*)d);PrintVTable(vTableb1);//通过切片获取Base2 b2 d;VFPTR* vTableb2 (VFPTR*)(*(int*)b2);PrintVTable(vTableb2);return 0;
}运行看看 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
菱形继承和菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承一方面太复杂容易出问题另一方面这样的模型访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了一般我们也不需要研究清楚因为实际中很少用。这里简单叙述一下 先看菱形继承
class A
{
public:virtual void func1() { cout A::func1 endl; }int _a;
};class B : public A
{
public:virtual void func2() { cout B::func2 endl; }int _b;
};class C : 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;return 0;
}先来看一下这个类有多大
28 字节这个是怎么得到的来分析一下
A类有一个虚函数表指针和一个整型 应该是8字节B类继承于A类 包含A类的内容B的虚函数储存在A的虚表中所以B类一个为8 4 12 C类同理D类继承于B类和C类那么就包含B类与C类D类的虚函数储存在B类的虚表中A的虚表 通过内存来验证一下 可以看到只有两个虚表指针。所以菱形继承和多继承类似
再来看菱形虚拟继承 这个36字节是怎么得到的
首先菱形虚拟继承会把共同的基类提取出来也就是A被提出来了那么B类就会有一个虚基表指针来指向这个提前出来的A类。所以B类大小为4 (虚表指针) 4虚基表指针 4int数据 12C类同理那么现在就有12 (B类) 12C类 4A类的int 4(D类的int) 32 啊这才32字节剩下的4字节是什么难不成还有一个虚表指针是的A 里面还有一个虚表指针
来看内存 很明显在A类中还有一个虚表指针真滴复杂 所以应该是 12 (B类) 12C类 8A类的int 4(D类的int) 36 那为什么A会有一个虚表指针而不是D类有 首先派生类的成员是不会有虚表指针的虚表指针都在基类的部分中 我们这四个类都有自身的虚函数 菱形继承中B类与C类都继承于A类所以BC是派生类就不需要有独立的虚表指针而是与A类共用。父类有了就与父类共用父类没有才会独立创建。菱形虚拟继承中B类与C类都虚拟继承于A类A类被单独出去了那么B类与C类的虚函数就不能放在A类里因为A类是共享的放进去就会产出问题所以BC会独立创建一个虚表指针。 总结 子类有虚函数继承的父类有虚函数就有虚表子类不需要单独建立虚表如果父类是共享的无论如何都有创建独立的虚表
注意虚基表中储存两个值第一个是距离虚表位置的偏移量第二个是距离基类位置的偏移量
Thanks♪(ω)谢谢阅读
下一篇文章见