多态的概念
- 多态是面向对象设计中的一个概念,与继承点此进入、封装并称为面向对象设计的三大特性。
- 多态是不同的对象完成相同的行为会产生不同的结果。
例:
- 游乐园买票时,成人全价,儿童可半价。
- 发工资时,上层领导工资较多,底层员工较少。
多态的实现
- 多态在继承的基础上
- 调用对象必须是指针或引用
- 被调用的函数必须为虚函数且完成了虚函数的重写
虚函数
- 在函数前加virtual关键字的非静态成员函数
- 形式一般为:
virtual 函数返回值类型 虚函数名(形参表){函数体}
特点
- 定义了虚函数后,可在派生类中对虚函数重新定义。派生类重新定义的函数与虚函数有相同的形参个数与形参类型。实现了统一接口,但实现过程不同。
虚函数的重写
- 派生类中有和基类完全相同(函数名,参数,返回值)的虚函数,称派生类的虚函数重写了基类的虚函数。
- 虚函数的重写又称虚函数的覆盖。
class Employee
{
public:
virtual void Salary()
{
cout << "salary——little" << endl;
}
};
class Leader:public Employee
{
public:
virtual void Salary()
{
cout << "salary——many" << endl;
}
};
void GetSalary(Employee& e)
{
e.Salary();
}
int main()
{
Employee em;
Leader le;
GetSalary(em);
GetSalary(le);
return 0;
}
虚函数重写的例外:协变
- 重写的虚函数的返回值可以不同,但必须是基类指针(引用)和派生类指针(引用)
class A
{
};
class B : public A
{
};
class Base
{
public:
virtual A* func() { return new A; }
};
class Derived : public Base
{
public:
virtual B* func() { return new B; }
};
重写不规范写法
- 在派生类重写中可以不加virtual关键字,因为基类虚函数被继承至派生类后,依然具有虚函数属性。但不建议使用。
class Base
{
public:
virtual void Func(){}
};
class Derived : public Base
{
public:
void Func(){}
};
- 注意:析构函数比较特殊。
- 如果基类的析构函数是虚函数,则派生类的析构函数就重写了基类的析构函数。看起来他们函数名不同而违反了重写的规则。
- 其实编译器对析构函数的名称进行了特殊处理,统一成destructor。
- 建议析构函数写成虚函数。
多态的原理
虚函数表
下面看个例子
class Base
{
public:
virtual void Func1()
{}
virtual void Func2()
{}
private:
int _a = 1;
int _b = 2;
};
- 可以看出来除了成员_a,_b,还多出了一个_vfptr的指针。这个指针称为虚函数表指针。
- 一个虚函数类中至少有一个虚函数表指针,用虚函数表来存放虚函数的地址。
- 那么这个虚函数表(又称虚表)中存放着什么呢?
class Base
{
public:
virtual void Func1()
{}
virtual void Func2()
{}
void Func3()
{}
private:
int _a = 1;
};
class Derived : public Base
{
public:
virtual void Func2()
{}
private:
int _b = 2;
};
经过对以上代码的测试得到了下面内容:
可以得出以下结论:
- 基类对象b中有一个虚表指针,存放着自己的虚函数Func1,Func2,派生类对象d中也有一个虚表指针,存放着自己继承来的Func1和自己重写的Func2
- Fun3同样被继承了下来,但由于不是虚函数因此没有在虚表中
- 虚函数表本质是一个从存放虚函数指针的指针数组,最后存了nullptr
- 派生类虚函数表的生成:
- 先拷贝一份基类的虚表内容至自己的虚表中
- 用自己重写过的虚函数覆盖基类的虚函数
- 将自己增加的虚函数按声明次序增加至虚表后
多态实现过程
- 可以看出e指向em对象时,e->GetSalary在em的虚函数表中找到虚函数Employee::Salary
- 可以看出e指向le对象时,e->GetSalary在em的虚函数表中找到虚函数Leader::Salary
- 这就证明了不同对象完成同一行为,产生了不同结果。
查看反汇编可以看出
- mov eax,dword ptr [e]//e中存的是em对象的地址,将e赋给eax
- mov edx,dword ptr [eax]//将eax中的值(即为em对象)的前四个字节(虚标指针)赋给edx
- mov eax,dword ptr [edx]//将edx的值(即虚表中前四个字节)赋给eax
- call eax//eax存着虚函数的指针
可以看出多态的调用实在运行后通过对象来查找的。