目录
1.何为多态?
多态即为多种状态,具体就是不同对象去执行同种行为的状态是不同的。就好似狮子和猎豹都会跑,但它们跑的具体姿势等是不同的。在C++中的多态主要是通过基类指针或引用指向不同派生类,从而调用同一虚函数产生不同行为实现的。
多态条件:
1、基类指针或引用调用虚函数。2、派生类需对调用的虚函数进行重写。
虚函数:用virtual修饰的类的成员函数。
重写:或称覆盖,派生类的虚函数与基类的虚函数三同(函数名相同,参数列表相同,返回值类型相同)
注:基类的virtual关键字不可省略,派生类重写基类的虚函数的virtual关键字可以省略。
例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class animal {
public:
virtual bool eatFood()
{
cout << "animal eat food." << endl;
return true;
}
};
class cat :public animal{
public:
virtual bool eatFood()
{
cout << "cat eat food." << endl;
return true;
}
};
int main()
{
animal ani;
cat c;
ani.eatFood();//不构成多态
c.eatFood();//不构成多态
cout << endl;
animal* p = &c;
p->eatFood();//满足多态两个条件,构成多态
return 0;
}
输出:
虚函数重写的两个例外条件:
(1)协变:派生类重写基类的虚函数时,与基类函数的返回值类型不同,基类虚函数返回基类对象的指针或引用,派生类重写的虚函数返回派生类对象的指针或引用,称为协变。
例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class animal {
public:
virtual animal& eatFood()
{
cout << "animal eat food." << endl;
return *this;
}
};
class cat :public animal{
public:
virtual cat& eatFood()
{
cout << "cat eat food." << endl;
return *this;
}
};
int main()
{
animal ani;
cat c;
animal* p1 = &c;
animal* p2 = &ani;
p1->eatFood();//协变也构成多态
p2->eatFood();//协变也构成多态
return 0;
}
输出:
(2)析构函数的重写
前面《继承》中提到,基类和派生类析构函数名都被同一处理成了destructor,这样析构函数看似函数名不同,只要基类析构函数用virtual修饰成虚函数,派生类的析构函数就构成重写。这样在delete时调用析构函数时就能构成多态。
例如:保证p1和p2调用正确的析构函数
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class animal {
public:
virtual ~animal()
{
cout << "~animal()" << endl;
}
};
class cat :public animal{
public:
virtual ~cat()
{
cout << "~cat()" << endl;
}
};
int main()
{
animal* p1 = new animal;
animal* p2 = new cat;
delete p1;
cout << endl;
delete p2;
return 0;
}
输出:
2.override和final
final:用final修饰成员函数,表示该函数不能被重写
用法:virtual void Drive() final {}override:用override修饰成员函数,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。用法:virtual void Drive() override{}
C++对函数重写的要求比较严格,有些情况下由于疏忽,可能会导致函数没有构成重写,这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才知晓,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写,在编译期就报出错误,便于检查。
函数的 重载,重写(覆盖),隐藏(重定义)的比较图:
注:两个基类和派生类的同名函数不构成重写就是隐藏关系。
3.纯虚函数和接口继承,实现继承
在虚函数后加上=0,表示这个虚函数是纯虚函数,拥有纯虚函数的类是抽象类,抽象类不可以实例化。
例如:virtual void Drive() = 0;
接口继承:指继承基类的函数名和函数的参数列表,不继承函数的具体实现(函数体)
实现继承:指继承函数的实现(函数体)
非虚函数的继承是接口继承和实现继承。
纯虚函数的继承是接口继承。
(非纯虚函数)虚函数的继承是接口继承和其缺省实现,意味着派生类不重写时就用基类的实现。
4.多态的原理
为探究多态的原理,先探究有虚函数的基类的对象模型是怎样的。
下面这段代码在x86平台下将输出8而不是4,因为Base类型的对象模型中不仅存储了4字节大小的m_base,还存储了一个指向虚函数表的虚表指针_vfptr。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class Base {
public:
virtual void fun1(){}
virtual void fun2() {}
void fun3() {}
int m_base;
};
class derive :public Base{
public:
};
int main()
{
Base base1;
cout << sizeof(base1) << endl;
return 0;
}
在调试窗口可见:
这个虚表指针指向了一张虚表,虚表中存储了虚函数fun1和虚函数fun2的地址。
那么虚函数的重写是如何实现的?
见如下代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class Base {
public:
virtual void fun1(){}
virtual void fun2(){}
void fun3() {}
int m_base;
};
class Derive :public Base{
public:
virtual void fun1() {}
};
int main()
{
Base base1;
Derive derive1;
return 0;
}
查看重写虚函数的派生类的对象模型,可见: 派生类对象中虚函数表指针指向的表中的Base::fun1被Derive::fun1覆盖了。
在运行时,派生类对象通过虚函数表指针去调用对应的虚函数,从而调用了派生类中重写的虚函数,从而实现了多态,在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态,具体是指指针或引用指向的对象的类型,在程序执行过程中可以改变,根据类型来调用不同的函数代码。而在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
5.单继承和多继承关系的虚函数表
探究派生类的虚函数表与基类的虚函数表有何不同,例如:
class Base {
public :
virtual void func1() { cout<<"Base::func1" <<endl;}
virtual void func2() {cout<<"Base::func2" <<endl;}
private :
int a;
};
class Derive :public Base {
public :
virtual void func1() {cout<<"Derive::func1" <<endl;}
virtual void func3() {cout<<"Derive::func3" <<endl;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
void func5() {cout<<"Derive::func5" <<endl;}
private :
int b;
};
int main()
{
Base base1;
Derive derive1;
return 0;
}
基类有两个虚函数,派生类重写其中一个,继承了其中一个,派生类还有两个虚函数。
在调试窗口查看其虚函数表:
可见VS2022少显示了派生的虚函数fun3,fun4,通过虚函数表地址查看派生类的虚函数表:
可见确实有四个函数地址,下面两个地址应该是fun3和fun4的。
可以通过打印虚表来验证:
例如:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
void func5() { cout << "Derive::func5" << endl; }
private:
int b;
};
typedef void(*vfptr)();//函数指针
void printVftable(vfptr* array)//传入虚函数表(指针数组)
{
int i=0;
cout << "虚函数表 : "<<array<<endl;
//vs下的虚函数表最后一个函数地址为0,可作为判断条件
while (array[i])
{
array[i]();//直接用函数地址去调用函数,直接验证了
cout << array[i++] << endl;
}
}
int main()
{
Base base1;
Derive derive1;
printVftable((vfptr*)(*(int*)&base1));//取对象的前4个字节(虚函数表地址)
printVftable((vfptr*)(*(int*) & derive1));//取对象的前4个字节(虚函数表地址)
return 0;
}
输出:可见上述猜测为true
在多继承下,虚函数表也是类似的,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。