C++学习笔记:多态
- 什么是多态?
- 多态的构成条件?
- C++11中的final和override
- 抽象类是什么?
- 什么是虚表?
- 多继承中的虚表
什么是多态?
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
例如:学校在餐厅的某个档口为老师们提供了教师优惠,同样的一份食物,如果是学生去吃饭的话需要10元,而老师去吃饭的话仅需要8元,这就是多态的一种体现.
多态的构成条件?
多态构成需要有两个条件:
- 必须是虚函数,并且对虚函数的调用必须是指针或者引用
- 对所继承的虚函数必须进行重写
例如:
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }void Func(Person& p)
{ p.BuyTicket(); }int main()
{
Person ps;
Student st;
Func(ps);
Func(st);return 0;
}
同样的函数,调用后的结果却不同,这就是多态的使用
C++11中的final和override
因为在继承和多态中对虚函数是否重写有着比较高的要求,为了防止有时候因为疏忽而出现问题,C++11提出了关键字final和override
- final —— 在虚函数名后加final可以限制该虚函数不能被重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒适" << endl;}//此处报错
};
- override——在虚函数后加上override可以规定该函数必须重写
class Car{
public:virtual void Drive(){}
};
class Benz :public Car {
public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
抽象类是什么?
一般在虚函数后写上 =0 则把这个虚函数视为纯虚函数.
而把包含纯虚函数的类称为抽象类(也叫接口类),纯虚函数不能实例化出对象,只能靠派生类重写虚函数来进行实例化对象.纯虚函数的存在意味着派生类必须重写纯虚函数,由此体现了接口继承
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
void Test()
{
Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}
什么是虚表?
在C++中,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表.
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
通过观察和测试,我们发现了以下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段 - 值得注意的是,不同的类的虚表是不同的,即使派生类没有重写父类的虚函数,那么派生类的虚表和父类的虚表也是不同的
- 所有虚函数一定会被放入虚表
多继承中的虚表
当一个派生类继承了两个父类的时候,此时这个派生类就会有两个虚表,分别是两个父类的虚表
class Base1 {
public:virtual void func1() {cout << "Base1::func1" << endl;}virtual void func2() {cout << "Base1::func2" << endl;}
private:int b1;
};
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));PrintVTable(vTableb2);return 0;
}
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中