目录
前言
一、是什么?
二、怎么样?
Ⅰ、构成条件
Ⅱ、虚函数
Ⅲ、虚函数的重写
1.常规情况下
2.虚函数重写的三个例外
①返回值的类型可以不同
②析构函数的重写
③子类虚函数可以不加virtual关键字(不建议)
3.override和final关键字
Ⅳ、重载/重写/重定义(隐藏)区别
三、虚函数表(细致)
场景引入
定义
单继承的虚表
如何打印虚表(指针)?
多继承的虚表
菱形虚拟继承的虚表
总结:
四、底层原理
原理
多态调用与普通调用
五、抽象类
定义
特点
前言
都说面向对象的三大特性为封装,继承,多态,前面已经介绍了封装和继承,下面来看看C++多态是怎么个事。。
一、是什么?
多态,多态,通俗点来说就是一个事物有多种形态,具体点就是对于同一件事情,不同对象去完成时会产生不同的形态,结果不一样!
比如:买车票这件事,有普通票,有学生票半价,军人就优先买票等等
二、怎么样?
多态是在不同继承关系的类对象,去调用同一函数,从而产生了不同的行为!!
多态的特点:指向谁就调用谁!
Ⅰ、构成条件
- 子类完成对父类虚函数的重写
- 通过父类的指针或者引用去调用虚函数
因此实现多态,指向谁,就调用谁!!
Ⅱ、虚函数
所谓虚函数,就是在成员函数前面加上virtual关键字 。
注意:
①是成员函数,不是函数!!!不能在类外声明和定义
②静态成员函数不能设置为虚函数,因为没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
③inline成员函数可以是虚函数。编译器会忽略inline属性。
如:
virtual void Buytick()
Ⅲ、虚函数的重写
1.常规情况下
重写也称为覆盖,即子类中有一个和父类完全一样(函数名,参数列表,返回值三者)的成员函数,称为子类重写了父类的虚函数。。
可以看到,没有完成虚函数重写,不构成多态,结果就不是多态的结果!
以上常规情况下必须完成三同的条件,但是你知道的,这是C++,必然会有例外,也叫坑!
2.虚函数重写的三个例外
①返回值的类型可以不同
这种情况也叫做协变,类型不同,但是仅限父类虚函数返回父类的指针/引用,子类虚函数返回子类的指针/引用。
②析构函数的重写
有以下场景:
可以看到上面的结果,如果子类中申请了空间,但是没有释放,必然会引起内存的泄漏 ,所以为了防止上述场景的发生,我们希望指向谁就调用谁的析构,那就要构成多态。所以建议在继承体系中,将析构函数定义为虚函数。。构成多态。。
③子类虚函数可以不加virtual关键字(不建议)
对于这种例外,不建议这样干,建议最好全加上。
注意:构成多态,父类不能不加!!
为什么可以不加呢?
重写更深一层的含义:子类继承父类,实际上将虚函数头继承了,也就是说虚函数头实际还是和父类完全一样,子类只是完成功能上的重写,也就是把这个虚函数头的函数体换了。所以可以不加。
3.override和final关键字
- final,修饰虚函数,表示该虚函数不能继承;修饰类时,表示该类不能被继承。加父类
- override,检查子类虚函数是否重写父类的某个虚函数,没有重写就报错。加子类
Ⅳ、重载/重写/重定义(隐藏)区别
- 重载
①两个函数在同一作用域
②函数名相同,参数不同
- 重写
①两个函数必须是虚函数
②两个函数分别在父类和子类
③两个函数满足三同(函数名,参数,返回值),协变例外
- 重定义(隐藏)
①两个函数分别在父类和子类中
②两函数名相同即可
三、虚函数表(细致)
场景引入
问题:以下代码类对象的大小是多大?
class Base
{
public:
virtual void Func()
{
cout << "Is Base::Func()" << endl;
}
private:
int _a=1;
};
Base b;
sizeof(b)=?
按照正常理解,类对象中存的是类成员变量,所以大小应该是4,但是事实并非如此!
可以看到结果是8,为啥呢?调试看看
除了有成员变量以外,还有一个_vfptr,这什么玩意啊?实际上就是一个指针,也叫做虚函数表指针。在32位环境下,一个指针的大小就是4字节,所以上面的结果就是 int + 指针大小=8!
定义
虚函数表就是存放虚函数地址的一个表。实际上,对于一个有虚函数的类至少都有一个虚函数表指针,该指针指向一个虚函数表,虚函数表简称虚表,虚函数表指针简称虚表指针!
注意区分:
①虚表里面存的不是虚函数,而是虚函数的地址(指针)!!!只有虚函数才能进入这个表!
②虚表的本质就是一个存虚函数指针的指针数组,一般这个数组最后一个放nullptr!
③虚表是在编译阶段就生成的,一般在常量区!
④虚表指针存在于实例化的对象上!
⑤虚函数跟普通函数一样存在代码段,不在虚表!
⑥继承部分提到的虚基表,存的当前位置距离虚基类部分的偏移量,解决菱形继承数据冗余和二义性
单继承的虚表
上图我们说过,只有一个函数是虚函数时,才能将其地址放入虚表中,那来看看下面的例子
class Base
{
public:
virtual void Func1()
{
cout << "Is Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
};
class Devire:public Base
{
public:
virtual void Func1()
{
cout << "Is Devire::Func1()" << endl;
}
virtual void Func3()
{
cout << "Is Devire::Func3()" << endl;
}
};
Base b;
Devire d;
实际上这个可以理解为编译器的一个BUG。。将Func3给隐藏了,不正常现象!那Fun3究竟在哪?只能打印虚函数表来看看了。
如何打印虚表(指针)?
虚表实际上是一个函数指针数组,元素是函数指针,也就是每一个元素都为void(*)()类型。所以为了方便我们可以重命名。
typedef void(*VFR)();将函数指针类型重命名为VFR
VFR p[10];函数指针数组。元素VFR
虚表的访问需要使用对象中虚表指针才能进行访问,所以得取出对象的虚表指针(数组指针),指向虚表。32位环境下,一个指针的大小为4字节,所以就要取出对象的头4字节。怎么取?直接强制类型转化不可以!
①取出d对象地址,强制类型转化成int*,在解引用,此时就拿到一个整型的大小,4个字节。这个值就是指向虚表的指针。。
②在强转成VFR*,因为虚表就是一个存VFR类型(函数指针类型)的数组。
VFR* ptr = (VFR*)(*(int*)&d);//取出虚表指针。
打印函数:
//void func(VFR ptr[])
void func(VFR* ptr)
{
cout << "虚表地址->>" << ptr << endl;
for (size_t i = 0; i<3; i++)
{
printf("第%d虚函数的地址:%p->",i, ptr[i]);
VFR f = ptr[i];//取出数组的每一个元素,类型为函数指针。
(*f)();//指针解引用,函数调用
}
}
验证一下结果:
可以看到Func3也在对象d中的虚表中。。。只是被编译器隐藏了!!!BUG
所以实际上上图,单继承子类的对象模型是:
多继承的虚表
实际上,多继承体系中,子类没有重写的虚函数(自己的虚函数),会被放在第一个继承基类部分的虚函数表中!
分析如下:
class A
{
public:
virtual void func1() { cout << "A::func1" << endl; }
virtual void func2() { cout << "A::func2" << endl; }
private:
int _a;
};
class B
{
public:
virtual void func1() { cout << "B::func1" << endl; }
virtual void func2() { cout << "B::func2" << endl; }
private:
int _b;
};
class C : public B, public A
{
public:
virtual void func1() { cout << "C::func1" << endl; }
virtual void func3() { cout << "C::func2" << endl; }
private:
int _c;
};
C c
上图的监视窗口可以看到,子类C重写了B和C的func1,那么C自己的虚函数Func3在哪?是否可能会在两个父类的虚表中的一个?打印虚表看看。
从上图可以很清楚的看出,C自己的虚函数放在了父类B的虚表中!!
注意:找到父类A的虚表指针,更加保险的写法:
A* ptr = &c; VFR* vTableb2 = (VFR*)(*(int*)ptr); func(vTableb2);
所以实际上,C的对象模型是这样的:
菱形虚拟继承的虚表
有如下继承关系:
class A
{
public:
virtual void func1() { cout << "A::func1" << endl; }
private:
int _a=1;
};
class B:virtual public A
{
public:
virtual void func2() { cout << "B::func2" << endl; }
private:
int _b=2;
};
class C :virtual public A
{
public:
virtual void func3() { cout << "C::func3" << endl; }
private:
int _c=3;
};
class D :public B, public C
{
public:
virtual void func4() { cout << "D::func4" << endl; }
private:
int _d=4;
};
D d
sizeof(d)=?
对于菱形虚拟继承,实际上存在三张虚表,A、B、C都有虚表。因为虚基类A是共享的,B和C的虚函数不可能同时放入A的虚表中,所以B和C存在各自的虚表,另外又是虚拟继承,所以还存在着一个虚基表指针,虚基表中存的偏移值,为了去寻找父类A,方便切片赋值!
总结:
- 父类没有虚表或者是共享类的时候,子类都需要单独创建虚表存自己的虚函数。如上A是B、C的共享类,B、C就要创建虚表
- 父类存在虚表,子类对象就不需要单独创建了,直接放继承下来的
不要写菱形虚拟继承,太复杂了!!
四、底层原理
原理
结论:若程序满足多态条件,运行时,就会去指向对象的虚函数表中找到对应的虚函数地址,再根据虚函数地址调用对应的虚函数。指向父类调用父类的虚函数,指向子类调用子类的虚函数!
分析如下:
买票代码:
class Person
{
public:
virtual void Buytick()
{
cout << "普通-全价" << endl;
}
};
class Stu :public Person
{
public:
virtual void Buytick()
{
cout << "学生-半价" << endl;
}
};
void Func(Person& p)
{
p.Buytick();
}
int main()
{
Person p;
Stu s;
return 0;
}
来看看子类Stu的虚表是怎样的。
①子类会将父类的虚表内容拷贝一份放到自己的虚表中,如果子类重写了父类中的某个虚函数,那就会用自己重写后的虚函数去覆盖父类的虚函数(如上Buytick)。子类自己新增的虚函数依次按声明顺序放入虚表。
②当你将子类对象赋值给父类指针或者引用时,切出去的父类虚函数已经被子类的覆盖了。
综上可得:父类和子类的虚函数表是完全不一样的两张表,即同类型的对象共用一张虚表,不同类型的对象有各自的虚表。正因如此,才能实现指向谁就调用谁。
多态调用与普通调用
①多态调用:满足多态条件以后的函数调用,不是在编译时确定的,而是程序运行起来以后到对象中找的。因为对象是在运行时才有的,而虚表指针又存在对象中。。
运行起来才确定所调用的函数行为,也称为动态绑定(晚期绑定)
②普通调用:不满足多态的函数调用,在编译时就确定了。根据调用对象的类型,确定调用函数。
在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
五、抽象类
定义
在C++中的抽象类和Java不一样,C++的抽象类:在虚函数的后面写上 =0,该函数为纯虚函数!包含这种纯虚函数的类叫做抽象类
//Car为抽象类
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};
特点
①抽象类不能实例化出对象
②若子类没有重写纯虚函数,该子类也不能实例化出对象,重写后才可以实例化!但父类依旧不能实例化出对象
③间接强制派生类重写虚函数
和override的区别就是:override是检查语法是否有问题,能实例化对象!抽象类不能实例化出对象!
看需求,若不希望父类实例化出对象,那就搞成抽象类!
①普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。②虚函数的继承是一种接口继承 ,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。③如果不实现多态,不要把函数定义成虚函数。
好了,今天的分享就到这里,如果对你有帮助,欢迎三连,你的支持和认可就是我前进的动力!