C++:多态详解
1. 多态的概念
多态是在不同继承关系的类对象,去调同一函数,产生了不同的行为。通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举例子来说:
比如现在你有两只不同的猫,然后你给猫买了很多小鱼干,一只猫完成吃鱼干这个动作只花了很短的时间,而另一只猫吃的比较慢花了很长的时间,两者都完成了吃鱼干这个动作,但是他们对应吃的速度不相同,完成时产生的状态也就不相同。
2. 多态的分类
多态分为静态多态和动态多态
2.1 静态多态
- 静态多态(静态绑定、静态联编、早绑定):在编译期间,就可以确定函数的行为,即:具体调用哪个函数
- 具体体现:函数重载和模板
- 函数重载:在编译期间,编译器会对函数实参类型进行推演,然后根据推演的结果选择合适的函数进行调用,如果有完全匹配的,直接调用;否则进行隐式类型转化,如果可以转化则调用,否则报错
- 模板:在编译期间,编译器会对函数实参类型进行推演,然后根据推演的结果再给模板的参数设定为具体的类型,然后再进行实现
函数重载
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
模板
template <class T>
T Add(const T& left, const T& right)
{
return left + right;
}
Add(1,2);
Add(1.0,2.0);
2.2动态多态
- 动态多态(动态绑定、动态联编、晚绑定):在程序运行时,根据基类指针或者引用指向不同类的对象,调用对应的虚函数(在程序运行时,确定函数具体的行为)
3. 动态多态的实现条件
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
两个条件缺一不可,否则无法实现多态
具体体现:此时如果两个条件已经完全满足了,在程序运行时,根据基类的指针或者引用指向不同派生类的对象选择对应类的虚函数进行调用
4. 虚函数的重写
- 一定是子类对基类的函数进行重写,而且该函数一定要是虚函数
- 子类和基类虚函数的原型必须要一致:返回值类型、函数名字、参数列表必须都一样
特例:
协变
- 基类虚函数返回基类的指针或者引用
- 子类虚函数返回子类的指针或者引用
- 基类/子类和基类/子类可以是不同的继承体系返回类型不同
析构函数
- 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
- 虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
- 为什么要将析构函数设置虚函数?
在继承体系中,最好将基类的析构函数设置为虚函数,如果子类中动态管理内存资源,一般情况下必须将基类的析构函数设置为虚函数,否则很可能会造成内存泄漏
- 基类虚函数可以与子类虚函数的访问权限不同,但是一般情况下都会将基类虚函数的访问权限设置为public
- 重载、覆盖(重写)、隐藏(重定义)的对比
- C++11中新增的解决重写相关问题的 override和final
override:只能修饰子虚函数,而且定义是子类的虚函数
作用:让编译器在编译代码期间,帮助检测子类中某个虚函数是否对基类中虚函数进行重写如果重写成功则编译通过,否则编译失败
形式:virtual void fun()override
final: 修饰类:表明该类不能被继承
修饰成员函数:修饰虚函数,表明该虚函数不需要被子类重写
形式:virtual void fun()final
- 以下代码对通过重写实现多态的各种情况进行了演示(包括不构成重写的部分)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vld.h>
#include<assert.h>
#include<vector>
using namespace std;
class A {};
class B : public A {};
class Base
{
public:
void f1()
{
cout << "Base::f1()" << endl;
}
void f2()
{
cout << "Base::f2()" << endl;
}
virtual void f3()
{
cout << "Base::f3()" << endl;
}
virtual void f4()
{
cout << "Base::f4()" << endl;
}
virtual void f5()
{
cout << "Base::f5()" << endl;
}
virtual Base* f6()
{
cout << "Base::f6()" << endl;
return this;
}
virtual A* f7()
{
cout << "Base::f7()" << endl;
return nullptr;
}
virtual void f8()
{
cout << "Base::f8()" << endl;
}
virtual void I9()
{
cout << "Base::I9()" << endl;
}
};
class Derived : public Base
{
public:
// 与基类f1的关系: 同名隐藏
void f1()
{
cout << "Derived::f1()" << endl;
}
// 与基类f2的关系: 同名隐藏
virtual void f2()
{
cout << "Derived::f2()" << endl;
}
// 与基类f3的关系:重写
// 注意:基类成员函数只要是虚函数,即使子类原型相同的函数没有增加virtual关键字
// 也可以构成重写,但是一般建议子类函数还是加上virtual
void f3()
{
cout << "Derived::f3()" << endl;
}
// 与基类f4的关系: 同名隐藏
virtual void f4(int a = 10)
{
cout << "Derived::f4()" << endl;
}
// 子类与基类f5虚函数返回值类型不同,因此既不是重写也不是重定义
// 因为基类和子类的f5都是虚函数,返回值类型如果不同---除非是协变代码可以通过编译
// 否则:编译报错
//virtual int f5()
//{
// cout << "Derived::f5()" << endl;
// return 0;
//}
// 与基类f6的关系:重写---->协变
virtual Derived* f6()
{
cout << "Derived::f6()" << endl;
return this;
}
// 与基类f7的关系:重写---->协变
virtual B* f7()
{
cout << "Derived::f7()" << endl;
return nullptr;
}
private:
virtual void f8()
{
cout << "Derived::f8()" << endl;
}
public:
// 注意:基类中I9()与子类中l9()没有构成重写
// 因为函数名字不同---导致原型不一致
// 但是:该种情况太难发现了,名字太像了
// C++11中:override
virtual void l9()//override
{
cout << "Derived::l9()" << endl;
}
};
void TestVritual(Base* pb)
{
pb->f1();
pb->f2();
pb->f3();
pb->f4();
pb->f6();
pb->f7();
pb->f8();
pb->I9();
}
int main()
{
Base b;
Derived d;
TestVritual(&b);
TestVritual(&d);
return 0;
}
5. 多态的实现原理
5.1 对象模型
如果类中包含有虚函数,对象模型中会多4个字节,编译器给会对这4个字节进行填充,具体是在构造函数中会给对象前4个字节中填充数据
- 举例:b、b1、b2都是基类B的对象
- 现象:三个对象不同,但是三个对象前4个字节中存储的虚表地址是一样的
- 说明:同一个类的不同对象共享一份虚表
5.2 基类和子类虚表的构建过程
基类:
- 虚表中肯定放置的都是虚函数
- 按照虚函数在类中声明的先后次序,依次添加到虚表中
子类:
- 将基类虚表中内容拷贝一份放置到子类虚表中
- 如果子类重写了基类某个虚函数,则使用子类虚函数替换虚表中相同偏移量位置的基类虚函数
- 将子类新增加的虚函数按照其在子类中声明的先后次序依次添加到虚表的最后(在虚表中隐藏)。
class B
{
public:
virtual void f1()
{
cout << "B::f1()" << endl;
}
virtual void f2()
{
cout << "B::f2()" << endl;
}
virtual void f3()
{
cout << "B::f3()" << endl;
}
int _b;
};
class D : public B
{
public:
virtual void f1()
{
cout << "D::f1()" << endl;
}
virtual void f3()
{
cout << "D::f3()" << endl;
}
virtual void f4()
{
cout << "D::f4()" << endl;
}
virtual void f5()
{
cout << "D::f5()" << endl;
}
protected:
int _d;
};
int main()
{
cout << sizeof(B) << endl;
B b;
b._b = 1;
B b1, b2;
D d;
return 0;
}
5.3 虚函数调用原理
- 从上述汇编可以看出,如果通过指针或引用的方式调用虚函数,编译器没有直接调用该虚函数
调用过程:
- 从对象前4个字节中获取虚表的地址
- 传递this指针+其他参数
- 从虚表中找对应虚函数的地址
- 调用该虚函数
- 注意:虚表的入口地址不能被修改,其表中存放的虚函数也不能被直接修改。
B* pb = (B*)&d;
pb->f1(); // f1调用的是基类的还是子类的f1?
取决于pb到底指向的是基类对象还是子类对象与是否类型强转无关,类型强转只是pb将d对象对应空间中的内容按照基类的方式类进行解析,并不会生成临时对象
class B
{
public:
virtual void f1()
{
cout << "B::f1()" << endl;
}
virtual void f2()
{
cout << "B::f2()" << endl;
}
virtual void f3()
{
cout << "B::f3()" << endl;
}
protected:
int _b;
};
class D : public B
{
public:
virtual void f1()
{
cout << "D::f1()" << endl;
}
virtual void f3()
{
cout << "D::f3()" << endl;
}
int _d;
};
typedef void(*PVFT)();
void TestVirtual(B& b)
{
b.f1();
b.f2();
b.f3();
}
int main()
{
B b;
D d;
TestVirtual(b);
TestVirtual(d);
B* pb = (B*)&d;
pb->f1(); // f1调用的是基类的还是子类的f1?
// 假设:需求修改基类B虚表中第一个虚函数的入口地址
// *(int*)&b; 注意:对象前4个字节中的内容--->int值的方式取到的
// 只不过所拿到了的整形值与虚表地址在数值上是相等的
PVFT* pVFT = (PVFT*)*(int*)&b;
pVFT[0] = nullptr;
pVFT[1] = nullptr;
pVFT[2] = nullptr;
TestVirtual(b);
TestVirtual(d);
return 0;
}
6. 抽象类
- 纯虚函数:在虚函数声明后跟=0,就是纯虚函数
- 将包含有纯虚函数的类,称为抽象类。
抽象类特性:
- 抽象类不能实例化对象
- 子类需要对抽象类中的纯虚函数进行重写,否则子类也是抽象类
7. 多态相关的面试题
- 构造函数可以是虚函数吗?
- 构造函数不能是虚函数。
- 原因:假设构造函数可以是虚函数—>说明构造函数将来需要通过虚表来进行调用—>找到虚表—>从对象前4个字节找虚表地址—>虚表地址是在构造函数创建对象时,将虚表地址放在对象前4个字节中—>如果构造函数没有调用,对象不完整,即虚表指针还没有放在对象前4个字节—>则在调用构造函数之前,无法通过对象获取到虚表的地址—>无法调用构造函数