马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
目录
什么是多态?
多态的条件
虚函数:
虚函数的重写:
协变
析构函数的重写
C++11 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();如许就形成了相同的函数。构成虚函数重写
C++11 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; //抽象类不能实例化出对象,error
- return 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:
- //重写虚函数Func1
- virtual 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对象。
留意:
正常来说不要计划出菱形虚拟继续和菱形继续,语法太复杂容易堕落,同时在虚继续下由于多态在调用函数时是有多余的损耗的。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |