云南网站建设公司,注册安全工程师查询官网,wordpress get page,开源网站系统1. 多态的概念1.1 概念多态的概念#xff1a;通俗来说#xff0c;就是多种形态#xff0c;具体点就是去完成某个行为#xff0c;当不同的对象去完成时会产生出不同的状态举个例子#xff1a;比如买票这个行为#xff0c;当普通人买票时#xff0c;是全价买票#xff1b…1. 多态的概念1.1 概念多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态举个例子比如买票这个行为当普通人买票时是全价买票学生买票时是半价买票军人买票时是优先买票。另外一个例子最近为了争夺在线支付市场支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...而有人扫的红包都是1毛5毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据比如你是新用户、比如你没有经常支付宝支付等等那么你需要被鼓励使用支付宝那么就你扫码金额 random()%99比如你经常使用支付宝支付那么就不需要太鼓励你去使用支付宝那么就你扫码金额 random()%1总结一下同样是扫码动作不同的用户扫得到的不一样的红包这也是一种多态行为。2. 多态的定义及实现2.1多态的构成条件多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。比如Student继承了Person。Person对象买票全价Student对象买票半价。在继承中要构成多态还有两个条件1. 必须通过基类的指针或者引用调用虚函数2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写2.2 虚函数虚函数即被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();
}int main()
{Person p;Student s;Func(p);Func(s);return 0;
}通过结果我们不难发现这就是多态的影响下面深入学习多态2.3虚函数的重写虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。虚函数重写的两个例外1. 协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。了解可以像下面这样使用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. 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor。对于这个程序的结果有人可以有疑问为什么这里有3个打印结果因为子类继承了父类有父类全部的东西而这个程序也正好证明了需要先析构了子类然后在析构父类。在继承的学习中我们就为什么析构函数不要显示写就是这个原因。2.4 C11 override 和 fifinal1. final修饰虚函数表示该虚函数不能再被重写class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout Benz-舒适 endl;}
};这段程序就没有办法正常的运行。2. override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。使用方式class Car{
public:virtual void Drive(){}
};
class Benz :public Car {
public:virtual void Drive() override {cout Benz-舒适 endl;}
};2.5 重载、覆盖(重写)、隐藏(重定义)的对比3. 抽象类3.1 概念在虚函数的后面写上 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();
}3.2 接口继承和实现继承普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。4.多态的原理4.1虚函数表class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};上面这段程序的sizeof(Base)是多少如果是32为那么就是8字节如果是64位就是16字节。为什么呢除了_b成员还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数表也简称虚表。我们在以上内容中再增加一些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;Derive d;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 a 0;cout 栈: a endl;int* p1 new int;cout 堆: p1 endl;const char* str hello world;cout 代码段/常量区: (void*)str endl;static int b 0;cout 静态区/数据段: b endl;Base be;cout 虚表: (void*)*((void**)be) endl;Derive de;cout 虚表: (void*)*((void**)de) endl;return 0;
}声明这里使用的是64位的计算机我们可以通过打印他们的地址就能大致得到虚表存储的位置虚表的地址和代码段的地址十分接近因此可以认为虚表存储在代码段。4.2多态的原理根据这段代码来讲解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;Func(p);Student s;Func(s);return 0;
}看出满足多态以后的函数调用不是在编译时确定的是运行起来以后到对象的中取找的。(动态不满足多态的函数调用时编译时确认好的静态我们可以通过汇编代码确定这一点这里的s是普通调用而这个p是多态调用看看其中的反汇编的区别多态调用很明显就是得到了虚表指针之后通过解引用得到函数的地址再去调用。从上面的反汇编我们就可以看出来普通调用是编译时就已经确定了函数的地址而多态调用就是在运行时才能得到地址并调用。4.3 动态绑定与静态绑定1. 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态比如函数重载2. 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。5.单继承和多继承关系的虚函数表class Base {
public :virtual void func1() { coutBase::func1 endl;}virtual void func2() {coutBase::func2 endl;}
private :int a;
};
class Derive :public Base {
public :virtual void func1() {coutDerive::func1 endl;}virtual void func3() {coutDerive::func3 endl;}virtual void func4() {coutDerive::func4 endl;}
private :int b;
};
int main()
{Base b;Derive d;return 0;
}我们在监视窗口不能得到func3以及func4的虚表这时我们可以自己将func3以及func4在虚表中的地址打印出来打印思路本计算机是64位指针大小为8字节取出b、d对象的头8bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr1.先取b的地址强转成一个void**的指针解引用就是指针指针的大小会根据平台决定大小。 2.再解引用取值就取到了b对象头8bytes的值这个值就是指向虚表的指针3.再强转成VFptr*因为虚表就是一个存VFptr类型(虚函数指针类型)的数组。 4.虚表指针传递给PrintVTable进行打印虚表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:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }
private:int d1;
};
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]);vTable[i]();}cout endl;
}
int main()
{Derive d;VFPTR* vTableb1 (VFPTR*)(*(void**)d);PrintVTable(vTableb1);//因为这里要跳过一个base的大小才能得到虚表的指针VFPTR* vTableb2 (VFPTR*)(*(void**)((char*)d sizeof(Base1)));PrintVTable(vTableb2);return 0;
}看看结果5.3. 菱形继承、菱形虚拟继承实际中不建议设计出菱形继承及菱形虚拟继承一方面太复杂容易出问题另一方面这样的模型访问基类成员有一定得性能损耗实际中很少使用。看一道恶心的题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这里体现了接口继承就是B继承了A的时候继承了A的func的接口所以最后的答案是B