前言:此篇博客中的代码及解释都是在vs2017下的x86程序中,涉及的指针都是4字节。如果要其他平台下,部分代码需要改动。例如:如果是x64程序,则需要考虑指针是8字节的问题
多态的概念
- 概念:同一事物在不同场景下表现出的不同状态
- 举个例子:我们作为一个人在不同的场合可能拥有不同的身份:在学校我们是学生,我们跟老师说话的时候可能比较拘谨;在家里我们可能是儿子,弟弟等等,我们跟自己的父母,兄弟姐妹之间说话可能比较随意。我们在不同的场合下表现出来的不同的状态就可以理解为多态。
多态的定义及实现
-
构成多态的两个条件
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
class Person { public: virtual void BuyTicket() { cout << "全价票" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "半价票" << endl; } protected: int _stuid; }; class Solider : public Person { public: virtual void BuyTicket() { cout << "免费票" << endl; } }; int main() { Person p; Student stu; Solider sol; p.BuyTicket(); stu.BuyTicket(); sol.BuyTicket(); system("pause"); return 0; }
-
虚函数
- 虚函数:被virtual修饰的函数就是虚函数
class Person { public: virtual void BuyTicket() { cout << "全价票" << endl; } };
-
析构函数的重写
- 基类中的析构函数只要是虚函数,派生类的析构函数定义之后就重写了基类的析构函数(基类与派生类析构函数的名字不同)
- 基类和派生类虚函数的访问权限可以不同:一般将基类的虚函数设置成为public
C++11 override和final
- final:修饰虚函数,表示该虚函数不能再被继承
- override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写就会编译报错
重载、重写、隐藏的对比
三个概念的对比
抽象类
- 概念:在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。
多态的原理
- 虚函数表
按照我们之前计算的类的大小来算的话,此时运行结果不应该是4吗?但是又为什么是8呢?我们首先先来看一下那四个字节存放的是啥?
从上面的运行结果来看发现多了一个叫做_vfptr的指针(在x86下是四个字节),对象中的这个指针我们叫做虚函数表的指针(v代表virtual,f代表function)。一个含有虚函数的类中 都至少都有一个虚函数指针,因为虚函数的地址要被放到虚函数表中,虚函数表中也称虚表。
class Base {
public:
void Func1() {
cout << "Base::Func1()" << endl;
}
virtual void Func2() {
cout << "Base::Func2()" << endl;
}
virtual void Func3() {
cout << "Base::Func3()" << endl;
}
};
class Derived : public Base {
public:
virtual void Func1() {
cout << "Derived::Func1()" << endl;
}
};
int main() {
Base b;
Derived d;
b.Func1();
b.Func2();
b.Func3();
d.Func1();
d.Func2();
d.Func3();
system("pause");
return 0;
}
从程序的运行结果来看,我们发现了几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derived::Func1,所以虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2和Func3继承下来后是虚函数,所以放进了虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的 虚函数
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
- 虚函数存在虚表,虚表存在对象中这种说法是错误的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存放在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。