目录
1.多态概念
不同对象去完成同一间事情会产生不同的结果
例子:
(1) 支付宝扫红包活动,有些人能扫到八块十块,我却一直徘徊在三毛五角。同样是扫码行为,不同的用户扫得到的不一样的红包,这就是一种多态行为。
(2) 期末考试,不同的对象做相同试卷,不同的得分就是一种多态行为。
2.构成多态的两个条件
多态是在一个继承体系中,不同的类对象去调用同一函数,产生了不同的行为。
要在继承体系中实现这一行为,有两个必要条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数
继承中我们了解到了虚拟继承,用virtual修饰继承关系,虚函数也是用virtual来修饰,被virtual修饰的类成员函数称为虚函数。
重写/覆盖
多态中虚函数需要被重写(覆盖):子类中有一个跟基类完全相同的虚函数(即子类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),即构成重写
class Student {
public:
virtual void test() { cout << "及格" << endl; }
};
class ordinary : public Student {
public:
virtual void test() { cout << "良好" << endl; }
};
注:
子类的虚函数不用virtual声明也可以,但是基类的虚函数必须用virtual来声明。
重写之所以叫重写,是因为子类虚函数继承基类虚函数接口,用子类虚函数的实现来运行,感性理解一下:重写就是,子类虚函数继承了基类虚函数大括号外面的类型、声明、函数名、参数,保留了自己大括号里面套实现的东西。
协变
协变是重写的一种特殊情况:
基类与子类虚函数返回值类型不同,基类虚函数返回基类对象的指针或引用,子类虚函数返回子类对象的指针或引用,除此之外函数名及参数相同的情况叫做协变。
class A
{};
class B : public A
{};
class Student {
public:
virtual A* test()
{
cout << "virtual A* test()" << endl;
return nullptr;
}
};
class ordinary : public Student {
public:
virtual B* test()
{
cout << "virtual B* test()" << endl;
return nullptr;
}
};
void gotest(Student* s)
{
s->test();
}
int main()
{
Student s1;
ordinary o;
gotest(&s1);
gotest(&o);
return 0;
}
析构函数的重写
继承体系中基类的析构函数与子类的析构函数默认构成隐藏,统一会被处理成destructor(),那么当基类的析构函数用virtual修饰后,他们的关系由隐藏变为重写。
如果基类析构函数没有用virtual 进行修饰,当我们释放掉开辟的空间时,是由指针的类型决定的,可能会出现如下情况:
当出现这种情况时,外面就应想到用多态来解决:
将基类用virtual修饰,析构函数变为虚函数,
子类继承基类的析构函数,接口继承(继承大括号外面的类型、函数名(统一处理为destructor() )、参数),
因此子类析构也是虚函数,默认隐藏关系转变为覆盖关系,
通过基类的指针调用虚函数:
delete s1 = s1->destructor() + operator delete(s1)
delete s2 = s2->destructor() + operator delete(s2)
符合多态条件,正确结果如下:
3.override 和 final
在C++11中,针对重写,给出了这两个关键字
final
a.修饰虚函数,表示该虚函数不能再被重写:
b.修饰类,表示该类不能被继承:
在继承中,我们可以通过私有化基类的构造函数,令创建对象时无法调用构造函数来打断继承:
这属于间接打断继承,而 final 则是直接打断继承:
override
检查子类虚函数有没有重写基类的虚函数,如果没有重写编译报错:
4.抽象类
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象
纯虚函数
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
如果子类想要实例化,必须重写纯虚函数:
5.多态的原理
那么多态是怎么样实现基类直至或引用去调用不同对象的函数来实现不同行为的呢?
先来看看这样一个题目:
很多铁子和我一样,想着都应该是4,因为函数不存在类中,只算整型是4个字节,但是答案不然:
为什么会存8字节、除了整型,内存还背着我们藏了谁:
_vfptr 叫做虚函数表指针,存的是虚函数的地址,_vfptr 下存储的叫做虚函数表,这是运行时生成的,在调用虚函数时,根据生成的虚函数表来找到不同对象的虚函数进行调用,实现多态。
为了更清晰一些,请看下图:
编译时决议:普通函数的地址是在编译时生成的。
根据对象类型去找到函数地址进行调用,这是普通调用。
运行时决议:运行时确定调用函数的地址,多态就是如此。
查虚函数表找到调用函数位置进行调用,这是多态调用。
打印虚函数表
那么所有的虚函数都会在虚函数表中存储吗,这是肯定的,但是监视窗口可以看作是修饰过的,不一定真实:
我们打开内存看看虚函数表中有什么:
在监视窗口看不到子类中的虚函数test3,但是内存不会骗人,内存的前两个指针存了从基类继承下来的两个虚函数,一个重写了一个未重写;
除了这两个,还存了一个监视窗口不存在的地址,虚函数表中只会存储虚函数地址,结合我们子类中的虚函数,这个地址一定是test3无疑。
那么如何将虚表中的地址打印出来查看呢?
思路
1.打印的是虚表中的地址,地址不止一个,遇到空结束。可以当作数组循环打印。
2.地址是函数的地址,那么打印的地址类型应该是函数指针。
3.那么这个数组是一个存储函数指针的数组
实现
1.为了方便,可以重命名函数指针的类型
2.函数参数放的是函数指针类型的数组
//下面两种写法等效
void PrintVFTable(V_FUNC* a)
void PrintVFTable(V_FUNC a[])
3.我们知道子类虚函数的个数,设置循环条件
4.调用函数时,传的应该是数组地址,也就是虚函数表的地址
PrintVFTable((V_FUNC*)(*((int*)&o)));
这里比较复杂,首先我们要取子类对象的地址,此时&o的类型是ordinary*;
然后强转为(int*)类型,因为存储虚表的地址占了四个字节(指针类型可相互转换);
之后对整体解引用,得到的是存储虚函数表的地址;
此时再将其强转为我们的函数指针类型即可
5.在打印地址后,有了地址,可以调用虚函数,来明确是哪一个虚函数
class Student
{
public:
virtual void test(){cout << "virtual void test()" << endl;}
virtual void test2(){cout << "virtual void test2()" << endl;}
private:
int _sid;
};
class ordinary : public Student {
public:
virtual void test() override {cout << "良好" << endl; }
virtual void test3(){cout << "良好3" << endl;}
void test4(){cout << "良好4" << endl;}
};
//打印虚函数表
typedef void(*V_FUNC) ();
void PrintVFTable(V_FUNC* a)
{
for (size_t i = 0;a[i]!=nullptr;i++)
{
printf("[%d]:%p->", i, a[i]);
V_FUNC f = a[i];
f();
}
}
int main()
{
Student s;
ordinary o;
PrintVFTable((V_FUNC*)(*((int*)&o)));
}