Lesson 13 -- 多态
1. 多态的概念
就是去完成某个行为,不同的对象去完成时会产生不同的状态
2. 多态的定义和实现
2.1 多态的构成条件
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须时虚函数,且派生类必须对基类的虚函数进行重写
如此,调用哪一个函数,就和指针或者引用指向的对象有关
2.2 虚函数
class A
{
public:
virtual void func()
{}
};
2.3 虚函数的重写
派生类中有一个跟基类虚函数完全相同(函数名、参数、返回值)的虚函数,不符合重写就是隐藏。
class A
{
public:
virtual void func()
{}
}
class B: public A
{
public:
virtual void func()
{}
}
重写的例外
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写时,与基类返回值类型不同,就是返回各自的指针或者引用 - 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只需要定义,无论是否加virtual都构成重写。因为析构函数编译后同一被处理为destructor - 子类不加virtual依旧构成重写
2.4 C++11 override和final
- final:修饰虚函数,表示该虚函数不能再被重写,继承中修饰一个类不能被继承
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
2.5 重载、覆盖(重写)、隐藏(重定义)的对比
3. 抽象类
3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}// test 的参数是A的this指针,也就是多态调用。并且该指针指向子类
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}// 运行结果:B->1,因为重写是一个接口继承,重写实现,普通函数继承是实现继承。
4. 多态的原理
4.1 虚函数表
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
除了_b成员,还多了一个_vfptr,这个指针叫做虚函数表指针。一个含有虚函数的类中至少都有一个虚函数表指针。虚函数的地址放到虚函数表中。虚函数表是一个函数指针数组,虚函数调用时,到指向对象的虚表中找到对应的虚函数地址进行调用,普通函数的调用,是在编译链接时就确定函数的地址
派生类虚函数表
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对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以在原理层重写叫做覆盖。
- Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以虚函数表中没有
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放一个nullptr
- 派生类虚表生成:先将基类中的虚表内容拷贝一份到派生类需表中;如果派生类完成了某个基类虚函数的重写,就用派生类自己的虚函数覆盖基类的虚函数;派生类自己新增的虚函数按在派生类中的声明次序增加到虚表后面
- **虚函数存在哪?虚表存在哪?**虚表中放的是虚函数的地址,而不是虚函数,虚函数和普通函数一样存放在代码段。虚表在vs下存放在代码段。
4.2 多态的原理
构成多态,到指向对象的虚表中找到对应的虚函数地址进行调用,普通函数的调用,是在编译链接时就确定函数的地址
4.3 动态绑定和静态绑定
- 静态绑定又叫前期绑定,在程序编译期间确定了程序的行为,也称静态多态。如函数重载
- 动态绑定又叫后期绑定,在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也成为动态多态
5. 多继承关系的虚函数表
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;
}
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);
// 切片打印
// Base2* ptr=&d; // 会自动偏移,找到Base2的那一部分
// PrintVTable((VFPTR*)(*(int*)&ptr);
return 0;
}
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
6. 面试题
- inline可以是虚函数吗?可以,inline只是一个建议,编译器就忽略了inline,这个函数不再是inline,因为inline没有地址。所以在多态调用中失效了
- static可以是虚函数吗?不能。静态成员函数没有this指针,也就可以使用类域直接调用,是在编译是决议,多态都是运行时去虚表中运行时决议查找。
- 构造函数可以是虚函数吗?不能。对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。而虚函数是要去虚函数表中找到再调用
- 析构函数可以是虚函数吗?可以,建议基类的析构函数定义为虚函数
- 拷贝构造和赋值可以是虚函数吗?拷贝构造也是构造函数,不能;赋值虽然可以,但是没有意义
- 对象访问普通函数快还是虚函数快?虚函数不构成多态是一样快;构成多态,到虚表中找到地址再调用就会慢
- 虚函数表再什么阶段生成的,存在哪的?虚函数表是在编译阶段就生成了,一般放在常量区。而初始化列表阶段初始化的是虚表指针,对象中存的也是虚表指针。