纯静态网站的用什么cms,沈阳互联网公司,网站备案在线注销,ps做网站如何目录 什么是多态#xff1f;
多态的条件
虚函数#xff1a;
虚函数的重写#xff1a;
协变 析构函数的重写
C11 final 和 override
final#xff1a;
override#xff1a;
总结#xff1a;
三重对比#xff1a;重载重写重定义对比
抽象类
多态的原理
虚函数…目录 什么是多态
多态的条件
虚函数
虚函数的重写
协变 析构函数的重写
C11 final 和 override
final
override
总结
三重对比重载重写重定义对比
抽象类
多态的原理
虚函数表
为什么只能是父类的指针或者引用来调用才能形成多态 动态绑定和静态绑定
单继承的虚函数表
多继承的虚函数表
菱形继承和菱形虚拟函数继承 什么是多态
现实中买票的测策略学生 社会人 军人都是要买票的但是不同的人买票买到的价格却可能是不一样的学生票普通票军人票。虽然我们都执行了买票的操作但是我们的操作行为是不一样的。
多态其实是一种语法特性只有你使用时他才会出现不使用时这个特性不会出现如下是多态形成的条件
多态的条件
1.必须是基类的指针或者引用来调用虚函数。
当然如果使用派生类对象直接访问那打印出来的就是派生类函数的内容如果是用基类的指针或者引用进行访问会根据这个基类的赋值对象的类进行访问括号内是继承的内容多态特性要形成的条件是1.用基类的指针或者引用来调用虚函数
#include iostream
using namespace std; class Base {
public: void show() { // 非虚函数 cout Base class show function endl; } virtual void virtualShow() { // 虚函数 cout Base class virtual show function endl; }
}; class Derived : public Base {
public: void show() { // 隐藏了Base::show而不是重写 cout Derived class show function (hiding) endl; } void virtualShow() override { // 重写了Base::virtualShow cout Derived class virtual show function endl; }
}; int main() { Derived derivedObj; derivedObj.show(); // 输出 Derived class show function (hiding)因为调用的是Derived::show derivedObj.virtualShow(); // 输出 Derived class virtual show function因为调用的是重写的虚函数 Base* basePtr derivedObj; basePtr-show(); // 输出 Base class show function因为调用的是Base::show隐藏不影响基类指针调用 basePtr-virtualShow(); // 输出 Derived class virtual show function因为调用的是重写的虚函数 return 0;
}
如上代码我们直接用派生类对象调用虚函数打印出来的还是派生类的内容但是我们用基类的指针指向派生类此时调用虚函数打印出来的依然是派生类的内容。
2.形成条件2被调用的函数必须是虚函数并且派生类要对虚函数进行重写。
综上形成多态的条件有两个
1.必须是基类的指针或者引用来调用虚函数。
2被调用的函数必须是虚函数并且派生类要对虚函数进行重写。
虚函数
它的格式如下
class Person
{
public://被virtual修饰的类成员函数virtual void BuyTicket(){cout 买票-全价 endl;}
};只要在函数前面加上一个virtual就可以了。
注意
1.只要在你觉得是虚函数的函数前面加virtual其他只作用于本类的函数不要加因为虚函数是强制要求重写的。
2.静态函数不能是虚函数。因为静态函数全部类中只能有一份就算继承下去的也是那一份。如果给其加上虚函数那就要求重写一份此时这个重写的静态函数和原本的静态函数就冲突了。
3.虚函数的virtual和虚继承的virtual都是虚的二者的用法是不同的虚函数用于多态虚继承则是为了解决菱形继承时的数据的二义性和冗余的。
虚函数的重写
虚函数的重写也叫虚函数的覆盖虚函数的重写就是把继承下来的虚函数里的内容定义重新写一遍将原来替换下来的函数给替换。
//父类
class Person
{
public://父类的虚函数virtual void BuyTicket(){cout 买票-全价 endl;}
};
//子类
class Student : public Person
{
public://子类的虚函数重写了父类的虚函数virtual void BuyTicket(){cout 买票-半价 endl;}
};
//子类
class Soldier : public Person
{
public://子类的虚函数重写了父类的虚函数virtual void BuyTicket(){cout 优先-买票 endl;}
};如上代码三者的虚函数除了内容定义不同其他都是相同的。
此时我们在使用基类的指针或者引用去调用三者就形成了多态
void Func(Person p)
{//通过父类的引用调用虚函数p.BuyTicket();
}
void Func(Person* p)
{//通过父类的指针调用虚函数p-BuyTicket();
}
int main()
{Person p; //普通人Student st; //学生Soldier sd; //军人Func(p); //买票-全价Func(st); //买票-半价Func(sd); //优先买票Func(p); //买票-全价Func(st); //买票-半价Func(sd); //优先买票return 0;
}
注意在派生类中重写虚函数时是可以不用加virtual的因为它已经把基类的虚的特性继承下来了但是建议还是加上virtual便于其他程序员观看。
协变
协变就是在虚函数的层次下基类和派生类的返回值不同因为可能会有这样的需求要求基类的这个虚函数返回的是基类的对象指针或者引用或者派生类返回派生类的对象的指针或者引用。
//基类
class A
{};
//子类
class B : public A
{};
//基类
class Person
{
public://返回基类A的指针virtual A* fun(){cout A* Person::f() endl;return new A;}
};
//子类
class Student : public Person
{
public://返回子类B的指针virtual B* fun(){cout B* Student::f() endl;return new B;}
};在我们重写协变函数发现不同指向的指针调用函数调用的依然是对应的虚函数说明虚函数重写是成功的。
int main()
{Person p;Student st;//父类指针指向父类对象Person* ptr1 p;//父类指针指向子类对象Person* ptr2 st;//父类指针ptr1指向的p是父类对象调用父类的虚函数ptr1-fun(); //A* Person::f()//父类指针ptr2指向的st是子类对象调用子类的虚函数ptr2-fun(); //B* Student::f()return 0;
} 析构函数的重写
析构函数如果你自己定义了那不管有没有加virtual都是虚函数尽管名字不同为什么呢 //父类
class Person
{
public:virtual ~Person(){cout ~Person() endl;}
};
//子类
class Student : public Person
{
public:virtual ~Student(){cout ~Student() endl;}
};
上面代码写了析构函数试想这样的场景我们用基类的指针指向new出来的基类对象和派生类对象此时如果没有虚函数的定义delete时这个基类的指针应该只会调用基类的析构函数而对于派生类而言我们继承的定义是先调用基类的析构函数后调用派生类的析构函数。所以析构函数默认就是虚函数
int main()
{//分别new一个父类对象和子类对象并均用父类指针指向它们Person* p1 new Person;Person* p2 new Student;//使用delete调用析构函数并释放对象空间delete p1;delete p2;return 0;
}
其实因为子类的析构函数是先调用父类的析构后调用自己的析构已经是多态行为了父类的指针按理来说调用的就是父类的析构函数这里的多态就是把子类的析构函数重写成了先调用父类的析构函数后再去调用子类的析构函数。
注意为什么子类和父类的析构函数构成重写虽然表面写的时候析构函数的名字是不同的但是在编译后析构函数都会被统一命名为destructor();这样就形成了相同的函数。构成虚函数重写
C11 final 和 override
final final修饰虚函数表示这个虚函数不能再被重写。 //父类
class Person
{
public://被final修饰该虚函数不能再被重写virtual void BuyTicket() final{cout 买票-全价 endl;}
};
//子类
class Student : public Person
{
public://重写编译报错virtual void BuyTicket(){cout 买票-半价 endl;}
};
//子类
class Soldier : public Person
{
public://重写编译报错virtual void BuyTicket(){cout 优先-买票 endl;}
};上述代码用final修饰person的函数此时如果后面依然有虚函数重写就会导致编译报错。
override override检查派生类是否重写了虚函数如果没有重写则报错 //父类
class Person
{
public:virtual void BuyTicket(){cout 买票-全价 endl;}
};
//子类
class Student : public Person
{
public://子类完成了父类虚函数的重写编译通过virtual void BuyTicket() override{cout 买票-半价 endl;}
};
//子类
class Soldier : public Person
{
public://子类没有完成了父类虚函数的重写编译报错virtual void BuyTicket(int i) override{cout 优先-买票 endl;}
};
如上代码 我们在子类的函数上加上override这时override就会去基类判断子类是否重写对了。如果没有发现基类中有相同函数则编译报错必须是虚函数
总结
override和final的优点就是写虚函数时要求很高要求函数名称参数等等全都相同这就导致了程序员在写虚函数时如果犯错这时就发现不了因为可能被认作是一个新的函数所以这两个关键词可以用来检查虚函数final可以用来判断派生类有没有同名函数override可以用来解决虚函数写错的问题
三重对比重载重写重定义对比 重载很好理解就是同一作用域的同名函数构成
重写和重定义的区别是
重写要求是虚函数函数名称参数返回值等等都是一样的只是定义不一样。
重定义要求只要函数名相同且不是虚函数重定义类似于基类派生类间的重载。
抽象类
先说说抽象我们现实生活中泛指一类东西时比如我是人你是人他是人人就是抽象的
抽象类要求在虚函数后面加上0表示这个函数为纯虚函数然后只有一个类中出现了一个纯虚函数就可以叫做抽象类。抽象类不能实例化出对象就像人是一个泛指不能准确的指出是谁。
#include iostream
using namespace std;
//抽象类接口类
class Car
{
public://纯虚函数virtual void Drive() 0;
};
int main()
{Car c; //抽象类不能实例化出对象errorreturn 0;
}
子类继承了父类这时父类是抽象类那子类继承下来原本按理来说也是抽象类但是只要把纯虚函数重写成普通函数这时就子类就不抽象了纯虚函数被改了就可以实例化了。
#include iostream
using namespace std;
//抽象类接口类
class Car
{
public://纯虚函数virtual void Drive() 0;
};
//派生类
class Benz : public Car
{
public://重写纯虚函数virtual void Drive(){cout Benz-舒适 endl;}
};
//派生类
class BMV : public Car
{
public://重写纯虚函数virtual void Drive(){cout BMV-操控 endl;}
};
int main()
{//派生类重写了纯虚函数可以实例化出对象Benz b1;BMV b2;//不同对象用基类指针调用Drive函数完成不同的行为Car* p1 b1;Car* p2 b2;p1-Drive(); //Benz-舒适p2-Drive(); //BMV-操控return 0;
}
所以说抽象类存在的意义是什么它也可以直接用虚函数不用纯虚函数也能完成
1.既然说了人是抽象的那现实生活中抽象的东西也很多这时也希望在编程时能有抽象这一概念虽然没有具体的实例对象但是有存在这个名词。所以可以更好的用来表示现实世界
2.抽象类也可以变相的要求派生类强制子类重写虚函数因为不重写纯虚函数子类也没办法实例化出对象。
多态的原理
虚函数表 Base类实例化出对象的大小是多少笔试题 class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};
我们可以直接实例化出Base对象然后sizeofBase对象发现对象有8个字节
我们的_b成员是4字节那剩下的4字节是哪里来的 这里就能引出我们的主题虚函数表Virtual Function Table也可以叫做虚表
虚函数表和虚基表的区别虚函数表是因为虚函数出现的多态而虚基表则是因为菱形继承为了防止二义性和冗杂的继承
这里先讨论虚函数表只有虚函数出现时这个表指针才会出现。 看下面代码我们Base中有三个函数
#include iostream
using namespace std;
//父类
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://重写虚函数Func1virtual void Func1(){cout Derive::Func1() endl;}
private:int _d 2;
};
int main()
{Base b;Derive d;return 0;
} 在Derive类中重写了Func1。此时观察虚函数表中存储了什么 1.可以发现只有虚函数才能进入 虚函数表中 func3没有进入。虚函数也是在代码区的
2.在d对象中func1被重写为Derive类的func1.这就是重写覆盖原来的func1的地址重写为了派生类的func1
3.对于func3而言他是普通函数它当然也被继承了它的位置是在代码区的这个类的函数都放在一个统一的代码区不同对象调用时用的也是同一份代码就不会冗余
4.如果派生类有自己的虚函数而不是继承下来的这时派生类的虚函数位置应该在继承下来的虚函数的下面。 虚函数表的初始化时间虚函数存在哪里虚表存在哪里 1.虚表在构造函数初始化阶段进行初始化的虚表存的是虚函数的地址而不是虚函数
2.虚函数和普通函数一样都是存在代码区中的只是他的地址也存到了虚函数表中为了重写调用时就知道调用的是哪一个函数
3.虚表也是存在于代码区的
现在回想我们构成多态的两个条件1.必须有虚函数2.只能调用父类的指针或者引用来调用对象
为什么只能是父类的指针或者引用来调用才能形成多态 看上图我们有两个对象都是person类继承下来的Mike是成人johnson是学生
二者都有继承的或者重写buyticket函数 所以用person指针调用二者的butticket函数结果是不同。
这样就是多态了同一个行为不同的形态。
Person* p1 Mike;
Person* p2 Johnson;
大家还记得切片吗父类的指针或者引用调用子类对象是一种切片行为 可以调用父类中有的成员 而父类中没有的成员 则会被切掉不能调用。
Person p1 Mike;
Person p2 Johnson; 如果直接是用子类构造父类对象 父类对象它依然是父类对象因为在构造时首先会构造一个临时对象这个临时对象因为是person类所以他去调用了person的构造函数所以指向的依然是父类的虚表只是内置类型构造用的是子类的内容。 总结 1.如果构成多态是一个指针或者引用指向的是什么类型的对象调用什么虚函数和对象有关 2.如果不构成多态是一个父类对象这时父类对象依旧是父类所以虚函数表也是父类的但是成员的构造是复制的子类的成员 动态绑定和静态绑定 静态绑定 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也成为静态多态比如函数重载。 动态绑定 动态绑定又称为后期绑定(晚绑定)在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。 观察如下代码
//父类
class Person
{
public:virtual void BuyTicket(){cout 买票-全价 endl;}
};
//子类
class Student : public Person
{
public:virtual void BuyTicket(){cout 买票-半价 endl;}
};
int main()
{Student Johnson;Person p Johnson; //不构成多态p.BuyTicket();return 0;
}
这串代码我们没用引用或者指针构造p所以不构成多态。然后我们查看汇编代码 可以发现此时调用buyticket时是直接找到函数位置并进行调用的。
而如果我们用多态的方式调用buyticket
int main()
{Student Johnson;Person p Johnson; //构成多态p.BuyTicket();return 0;
} 然后查看汇编码 会发现有八条汇编指令大概意思就是我们对象要先去虚函数表中然后通过虚函数表找到对应的虚函数然后在进行调用。在使用时才去寻找。 对比 可以发现1.静态绑定是直接找到函数而动态绑定是先找到虚函数表的位置然后在虚函数表中找到对应函数的位置。 2.在多态的两条件没够成时函数调用是不会根据虚函数表的而是直接调用类中函数静态绑定。 3.而多态构成时才会去找虚函数表因为只有多态构成时才有可能出现重定义所以要根据虚函数表来找到要调用的函数动态绑定 单继承的虚函数表
//基类
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;
}; 根据上图发现只有重写的函数在虚函数表中被覆盖其他都是正常继承的然后自己类中多出来的虚函数普通函数不进入放在继承的下面 。
注意基类的虚函数和派生类的虚函数都是生成了的覆盖的意义就是把原本基类虚函数的地址改为派生类虚函数的地址 过程 1.派生类先把基类的表继承下来所以现在都是基类的虚函数。 2.把重写基类的虚函数在表中进行覆盖此时原本被继承下来的虚函数的地址改为了派生类虚函数的地址。 3.派生类自己有的虚函数放在后面。 注意在部分编译器中派生类自己的新增的虚函数可能不会在监视窗口中显示。所以此时可以使用内存级的监视窗口查看可以找到在虚函数表中是有对应的函数地址的。 当然你也可以使用打印的方式
typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{printf(虚表地址:%p\n, ptr);for (int i 0; ptr[i] ! nullptr; i){printf(ptr[%d]:%p--, i, ptr[i]); //打印虚表当中的虚函数地址ptr[i](); //使用虚函数地址调用虚函数}printf(\n);
}
int main()
{Base b;PrintVFT((VFPTR*)(*(int*)b)); //打印基类对象b的虚表地址及其内容Derive d;PrintVFT((VFPTR*)(*(int*)d)); //打印派生类对象d的虚表地址及其内容return 0;
}
这个方法首先就是定义了重命名了一个虚函数指针4字节然后通过for循环每次打印一个指针大小的内容。 多继承的虚函数表
多继承就是一个类继承了不止一个类。
//基类1
class Base1
{
public:virtual void func1() { cout Base1::func1() endl; }virtual void func2() { cout Base1::func2() endl; }
private:int _b1;
};
//基类2
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;
};
其实很好理解如果继承了一个类会出现一个虚函数表那继承了两个类那就生成两个虚函数表就好了。
注意虚函数表是一个地址这个地址下存着虚函数的指针可以想成存着指针的数组 由上图就可以知道虽然两个base中都有func1和func2但是因为有不同的虚函数表所以覆盖的原理还是一样的。只是在于派生类自己多出来的虚函数此时是存在第一张虚函数表中的而不是两表都存。
当然有些编译器也会出现上述问题监视窗口不显示派生类新增的虚函数此时依然可以用上面的两个方法只是在定位时要注意找第二个表时要sizeof(base1)跳过base1的内容找到base2.
菱形继承和菱形虚拟函数继承
class A
{
public:virtual void funcA(){cout A::funcA() endl;}
private:int _a;
};
class B : virtual public A
{
public:virtual void funcA(){cout B::funcA() endl;}virtual void funcB(){cout B::funcB() endl;}
private:int _b;
};
class C : virtual public A
{
public:virtual void funcA(){cout C::funcA() endl;}virtual void funcC(){cout C::funcC() endl;}
private:int _c;
};
class D : public B, public C
{
public:virtual void funcA(){cout D::funcA() endl;}virtual void funcD(){cout D::funcD() endl;}
private:int _d;
};
菱形继承就是两个父类同时继承了同一个类此时派生类同时继承了这两个父类。 A类对象的成员分布 B类对象的成员分布 C类对象的成员分布 D类对象的成员分布 虚基表菱形继承防止二义性和数据冗杂的。
虚表多态时用于存储虚函数的。
先说菱形继承因为菱形继承两个父类是继承于同一个类BC类中都有A对象此时如果直接继承在D中不就出现了两份A类对象吗为了防止这样的数据冗余和二义性所以干脆就只存一个A类对象所以此时在D类对象中BC类要找到A对象所以就出现了虚基表里面存的是BC类对象找到A类对象的距离。
菱形虚函数继承因为A类对象在D中单独一份了所以D对funcA的重写是单独的。所以BC中不存在A的虚函数了对于BC中的内容实际和多继承相同。最后多了两个虚基表用于让BC找到A对象。
注意
正常来说不要设计出菱形虚拟继承和菱形继承语法太复杂容易出错同时在虚继承下由于多态在调用函数时是有多余的损耗的。