1.什么是多态?
多态是指函数调用的多种形态,使我们调用函数更加灵活。
多态分为静态多态与动态多态
1)静态多态:静态多态指的是编译时的多态,通过函数重载实现。根据函数命名规则找到函数地址,从而实现调用不同的方法。
2)动态多态(运行时):父类指针或引用调用重写虚函数
a.父类指针或引用指向父类,调用的就是父类的虚函数,指向哪个子类就去调用哪个子类里相应的虚函数。
动态的多态构成的条件:
1)必须通过基类的指针或者引用调用虚函数
2)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
满足多态的条件时:
跟对象有关,父类的指针或引用指向哪个对象就调用哪个对象的虚函数
不满足多态的条件:
跟类型有关,调用的类型是谁,就调用谁。
虚函数的定义:只有类的非静态成员函数才可以称作是虚函数。
虚函数的virtual与虚继承的virtual没有关系。
虚函数是为了实现多态而虚继承是为了解决菱形继承带来的数据冗余和二义性。
虚函数的重写:
派生类中有一个跟基类完全相同的虚函数(即返回值类型,函数名,参数列表完全相同)则称子类的虚函数重写了基类的虚函数。
虚函数的重写的例外:
1)协变(函数名和参数相同,返回值是指针或者引用,俩个返回值须有父子关系)
基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类的指针或者引用。称为协变
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
2)
析构函数的重写(基类与派生类析构函数的名字不同)但底层都是destructor
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
关键字final
1)修饰虚函数,此虚函数不能被重写。
2)修饰一个类,则这个类就成为叶子类,不能被继承。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
关键字override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则会出现编译错误。
重载,覆盖(重写),隐藏(重定义)的对比
抽象类
包含纯虚函数的类叫做抽象类
纯虚函数:在虚函数后面写上 =0,则这个函数为纯虚函数。
抽象类的特性
1.不能直接实例化出对象,纯虚函数规范了派生类必须进行重写,(如果不重写,子类依然是抽象类,依然不能实例化出对象)更好地体现了接口继承。
接口继承和实现继承:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用基类的函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。继承的是接口。
如果不实现多态,不要把函数定义成虚函数。
多态的原理:
1.虚函数表
// 这里常考一道笔试题:sizeof(Base)是多少?sizeof(Base) = 8
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
原因是类中开头有一个虚表指针。
除了_b成员,还多一个_vfptr放在对象的前面(有些平台可能会放到对象的最后面,vs下是放在最前面),对象中的这个指针叫做虚函数表指针。
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;
}
虚表是在编译阶段生成的,虚函数跟普通函数一样,都是被放在代码段(常量区)的 ,虚表里面存放的是虚函数的地址。
而虚表指针初始化是在初始化列表阶段生成的。
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
验证该结论:
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;
}
反思一下达到多态的两个条件
1)虚函数覆盖->为了使不同的对象去完成同一行为时,展现出不同的形态。
2)必须是基类对象的指针或者引用调用虚函数->父类指针或者引用,切片时指向或者引用父类和子类对象中的切出来的那一部分;如果函数参数为父类对象,此时切片只会将成员变量拷贝过去,而不会拷贝虚函数表指针vfptr,因此拷贝过去的话不合理(父类对象不应该存在子类的vfptr)
同类型的对象,虚表是一样的,所以他们共享同一张虚表。