注意 virtual关键字:
1、可以修饰原函数,为了完成虚函数的重写,满足多态的条件之一
2、可以菱形继承中,去完成虚继承,解决数据冗余和二义性
两个地方使用了同一个关键字,但是它们互相一点关系都没有
虚函数重写:
多态的条件:
1、虚函数的重写
2、父类对象的指针或者引用去调用虚函数
必须是父类指针或者引用
不可以是子类因为父类不可以传给子类
class Person
{
public:virtual void BuyTicket() { cout << "Person全票" << endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "Student半票" << endl; }
};
void func(Person& p1)
{p1.BuyTicket();
}
int main()
{Person p1;Student s1;func(p1);func(s1);return 0;
}
协变(是多态的一种特殊情况):
多态:
1、虚函数的重写(必须要函数名、返回值、参数要相同)
2、父类对象的指针或者引用去调用虚函数
但是协变可以返回值可以不同
但是返回值必须是基类的指针或引用和子类的指针或引用
//class A
//{
//};
//class B :public A
//{
//}
//其他类的基类和派生类也可以
//class Person
//{
//public:
// virtual A* BuyTicket() { cout << "Person全票" << endl; return nullptr; }
//};
//class Student : public Person
//{
//public:
// virtual B* BuyTicket() { cout << "Student半票" << endl; return nullptr; }
//};
//void func(Person& p1)
//{
// p1.BuyTicket();
//}
//class Person
{
public:virtual Person* BuyTicket() { cout << "Person全票" << endl; return nullptr; }
};
class Student : public Person
{
public:virtual Student* BuyTicket() { cout << "Student半票" << endl; return nullptr;}
};
void func(Person& p1)
{p1.BuyTicket();
}
int main()
{Person p1;Student s1;func(p1);func(s1);return 0;
}
析构函数:
面试题:析构函数需不需要加vitrual?
class Person
{
public:~Person() { cout << "~Person()" << endl; }};
class Student : public Person
{
public:~Student() { cout << "~Student()" << endl;}
};int main()
{Person* p1= new Student;delete p1;return 0;
}
这种情况下父类的指针指向了new Student 但是使用完会造成内存泄漏,父类的指针只会调用父类的析构函数去清理该指向部分的空间,但是我们需要清理子类的空间就要调用子类的析构函数,所以需要加virtual 构成虚函数的重写,让父类的指针调用构成多态,就可以调用子类的析构函数。
看下一道面试题:
在做面试题之前先看下面代码
在继承关系中,
如何理解上述话呢?
看下面代码
在满足多态的条件下,虚函数的继承是继承了接口,所以缺省值继承了,但是子类要自己重写实现
所以当父类中的有虚函数,子类的就可以不用加virtual,但是不规范
答案:是B
为什么多态就要继承父类的接口?突然感悟
比喻:子类中的函数 drive(Banz* const this),父类也有(Car* const this)
//子类这个this是接收不了父类的指针,只有父类的指针或引用才可以指向子类
//所以这个继承接口才需要继承父类的接口----突然感悟
============下面代码===============
//作者:蚂蚁捉虫虫
//链接:https ://www.zhihu.com/question/517444641/answer/2390138862
//来源:知乎
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#include <iostream> // std::cout
class Base {public:Base() {};virtual void func_a(int a = 0) {}; //这个是虚函数,子类只继承接口,具体的实现,由子类去实现void func_b(int b) { std::cout << b + 10 << "\n"; }; //这个是实函数,其接口和实现,都会被子类继承
};class Base_A : public Base {
public:void func_a(int a=15) { std::cout << a << "\n"; };
};class Base_B : public Base {
public:void func_a(int a) { std::cout << a + 15 << "\n"; };
};int main()
{Base_A a;Base_B b;a.func_a(); //仅仅继承了基类的接口,但没有继承实现a.func_b(10); //继承了基类的接口及实现std::cout << std::endl;b.func_a(10); //仅仅继承了基类的接口,但没有继承实现b.func_b(10); //继承了基类的接口及实现return 0;
}
只有在满足多态的情况下,虚函数的继承才是父类的虚函数继承对于子类来说继承的是父类的接口(包括缺省值),子类函数的实现需要子类来写
上述代码只是完成了重写,并没有满足多态,所以并没有继承接口
关键字final和override
1、final修饰虚函数,表示该虚函数不能再被继承
也可以修饰class叫最终类不能被继承
override关键字:检查子类的虚函数是否完成重写
构成虚函数重写吗?
没有,认真看,但是不会报错,所以,加上override就可以自动检测检查子类的虚函数是否完成重写
重载、重写、重定义
抽象类
可以看下列代码:
//作者:蚂蚁捉虫虫
//链接:https://www.zhihu.com/question/517444641/answer/2390138862
//来源:知乎
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。class Base {public:Base(){};virtual void func_a(int a) = 0; //这个是纯虚函数,子类只继承接口,具体的实现,由子类去实现void func_b(int b) {std::cout << b+10 << "\n";}; //这个是实函数,其接口和实现,都会被子类继承
};class Base_A: public Base{
public:void func_a(int a){std::cout << a << "\n";};
};class Base_B: public Base{
public:void func_a(int a){std::cout << a + 15 << "\n";};
};int main ()
{Base_A a;Base_B b;a.func_a(10); //仅仅继承了基类的接口,但没有继承实现a.func_b(10); //继承了基类的接口及实现std::cout << std::endl;b.func_a(10); //仅仅继承了基类的接口,但没有继承实现b.func_b(10); //继承了基类的接口及实现return 0;
}
上述代码里,定一个基类,里面有两个成员函数,一个是虚函数,一个是实际函数;然后又定义了两个子类,Base_A和Base_B,两个子类对基类中的func_b函数有不一样的实现
纯虚函数的作用强制子类完成重写
表示抽象的类型。抽象就是在现实中没有对应的实体的
接口继承和实现继承
多态的原理:
测试我们发现b对象是8个字节,除了_b成员,还多了一个指针_vfptr放在对象对面,我们叫做虚函数指针我们叫做虚函数表指针。一个含有虚函数表的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表称虚表
注意:虚函数存放在哪里? 虚表存在哪里
虚表存的是虚函数指针,不是虚函数,虚函数也是函数所以也是存在代码区,只是它的地址被存进虚函数指针中,这个指针被虚表记录着
重写:接口继承,实现重写,在原理上是覆盖将父类继承下来的vfptr的父类虚函数的地址覆盖成子类的虚函数地址
从反汇编看原理:
普通类函数:
在编译的过程中就已经确定了调用函数的地址
现在我们加上virtual虚函数
进入汇编,当形成多态时是如何调用的
00B021E1 8B 45 08 mov eax,dword ptr [A] //将A指向空间地址给eax
00B021E4 8B 10 mov edx,dword ptr [eax] //将eax空间中的前四个字节地址给edx就是虚函数表指针
00B021E6 8B F4 mov esi,esp//这个是维护函数栈帧的寄存器,不用管
00B021E8 8B 4D 08 mov ecx,dword ptr [A] //将A指向空间地址给ecx
00B021EB 8B 42 04 mov eax,dword ptr [edx+4] //因为edx保存的是前四个字节空间的地址就是虚函数表指针+4就是run()的地址,将run()地址给eax,前4个是speak()的地址
00B021EE FF D0 call eax //调用run()
00B021F0 3B F4 cmp esi,esp
00B021F2 E8 1A F1 FF FF call __RTC_CheckEsp (0B01311h)
多态就是有virtual函数是用虚函数表指针去存放虚函数的地址,在由虚函数表指针调用对应的函数
面试题:
虚函数存在哪里?代码段,虚函数和普通函数一样都是函数所以都是编译成指令存进代码段中
虚函数表存在哪里?
存在代码段中,不是存在栈区,因为栈区是由一个个栈帧堆建的所以每调用创建一个对象就要建立一个虚表是很消耗内存的
证明一下:
虚表存放在代码区中的代码段最合适,堆区是动态开辟的,数据区分为bss区(存放未初始化的static和未初始化的全局变量)和数据区存放(存放初始化的static和初始化的全局变量),所以代码段是最合适的
反向验证:
发现很接近代码区
总结:
多态的本质原理,符合多态的两个条件。那么在父类的指针或引用调用时,会到指向对象的虚表找到对应的虚函数地址,进行调用
多态(程序运行时去指向对象的虚表中找到函数地址,进行调用,所以p指向谁就调用谁的虚函数)
普通函数的调用,编译链接时确定函数的地址,运行时直接调用。类型时谁就是谁调用
动态绑定和静态绑定:
编译:就是代码和语法检查其实就是预处理、编译、汇编、链接
运行:就是将可执行文件加载到内存中进行对数据区的数据替换
静态绑定:更具调的类型就确定了调用的函数
动态绑定:运行时具体拿到类型确定程序的具体行为,就是在编译时无法确定函数的行为
运行时根据寄存器去拿到函数的地址
单继承和多继承的虚表(不是虚基表)
单继承:
void(*p)(); //函数指针
补充:
函数名就是函数的地址
那我们手动打印虚函数表
class base
{
public:virtual void func1() { cout << "base::func1()" << endl; }virtual void func2() { cout << "base::func2()" << endl; }};
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; }};
//void(*)()
typedef void(*VF_PTR)();//重命名函数指针void PrintVFTable(VF_PTR* pTable)//VF_PTR pTable[] 函数指针数组==虚函数表指针
{for (size_t i = 0; pTable[i] != 0; i++){printf("pTable[%d]=%p->", i, pTable[i]);VF_PTR f = pTable[i];//得到函数的地址==函数名f();}cout << endl;
}int main()
{base b1;derive d2;PrintVFTable((VF_PTR*)(*(int*)&b1));//取b1的地址因为要取到虚函数表指针,它在对象的前四个字节//所以转换成int*在解引用就是取空间b1的前四个字节,因为此时是int*//所以要转成VF_PTR*PrintVFTable((VF_PTR*)(*(int*)&d2));return 0;
}
多继承的虚表:
计算一下test 对象等于多少?
class base
{
public:virtual void func1() { cout << "base::func1()" << endl; }virtual void func2() { cout << "base::func2()" << endl; }int i = 0;
};
class derive
{
public:virtual void func1() { cout << "derive::func1()" << endl; }virtual void func3() { cout << "derive::func3()" << endl; }virtual void func4() { cout << "derive::func4()" << endl; }int i = 0;
};
class test:public base,public derive
{
public:virtual void func3() { cout << "test::func1()" << endl; }virtual void func2() { cout << "test::func3()" << endl; }virtual void func7() { cout << "test::func4()" << endl; }
public:int i = 0;
};//void(*)()
typedef void(*VF_PTR)();//重命名函数指针void PrintVFTable(VF_PTR* pTable)//VF_PTR pTable[] 函数指针数组==虚函数表指针
{for (size_t i = 0; pTable[i] != 0; i++){printf("pTable[%d]=%p->", i, pTable[i]);VF_PTR f = pTable[i];//得到函数的地址==函数名f();}cout << endl;
}int main()
{test i;cout << sizeof(i) << endl;return 0;
}
等于20
编译器又没显示!!!那我们手动去看看
继承的子类和其父类的表不是同一张表,只有同一类才是用一张表哦