文章目录
虚函数的作用
虚函数是为了实现动态多态。多态是指为不同的数据类型提供统一的接口,分为静态多态和动态多态。静态多态包括函数重载和模板函数,动态多态是:指针或引用类型可以根据运行中实际指向的派生类型的不同,来执行不同派生类的方法。
举个例子,有一个基类 Character 表示职业统称,玩家会选择职业战士 Warrior 或者魔法师 Magician,然后所有的逻辑例如攻击,防守都会根据玩家选择的不同职业有不同的效果。
class Character{
public:
virtual void attack(){} //虚函数实现多态
};
class Warrior : public Character{
public:
void attack(){
cout << "Slash the Monster"<< endl; //斩击怪物
}
};
class Magician : public Character{
void attack(){
cout << "Fire the Ball"<< endl; //释放火球
}
};
int main(){
Character* cter = new Character();
//玩家选择战士
cter = new Warrior();
//所有的业务逻辑都可以用cter->method()的方式执行,相当于统一接口
cter->attack();
cter->defense();
}
如果没有使用动态多态的话,根据玩家的选择,要重新声明对象,并且写相应的逻辑代码,很麻烦
int main(){
//选择战士
Warrior* wior = new Warrior();
wior->attack();
wior->defense();
...
//选择魔法师又要重写另一套代码
}
注意:虚函数的继承是永久性的,也就是说父类声明某个函数为虚函数,在孙子类,孙子孙子类中也还是虚函数,仍然可以实现动态多态:根据实际指向的类型来选择执行的方法。不过为了可读性和意义明确,派生类也建议加上virtual关键字。
虚函数的原理:虚函数表指针和虚函数表
如果不存在虚函数的话,对象执行方法的过程:根据对象声明的类型以及方法的名称,去代码区找到对应的方法,然后调用。
Character* cter = new Warrior();
cter->attack(); //没有虚函数的话,调用的还是Character类的attack()方法
为此,引入了虚函数表指针vptr以及虚函数表。每个对象有一个虚函数表指针,每个类有一个虚函数表。虚函数表指针大小为4字节,存储在对象的起始地址。虚函数表中存储的是对应类的所有虚函数。
有了虚函数之后的对象执行方法过程:
1、先根据对象本身的虚函数表指针找到虚函数表
2、在虚函数表中查找有没有该方法,有的话就执行,没有找到的话跳转到3
3、根据声明的类型和方法名称去代码区找该方法(原本的过程)。
虚函数表是如何存储虚函数的
单重继承
1、子类继承了父类,那么虚函数表一开始是和父类一样的。
2、如果子类重写了父类的某个虚函数,那么在虚函数表中,子类重写的虚函数地址会覆盖父类相应的虚函数地址。
3、如果子类新增加了某个虚函数,这个虚函数的地址会添加到虚函数表的尾部。
用代码举例说明以及验证:
class Base{
public:
virtual void f(){
cout << "Base: f()" << endl;
}
virtual void g(){
cout << "Base: g()" << endl;
}
virtual void h(){
cout << "Base: h()" << endl;
}
};
//重写了Base的f()方法,新增了i()方法
class Drive: public Base{
public:
virtual void f(){
cout << "Drive: f()" << endl;
}
virtual void i(){
cout << "Drive: f1()" << endl;
}
};
//重写了Drive继承的Base的g()方法,新增了j()方法
class End: public Drive{
public:
virtual void g(){
cout << "End: g()" << endl;
}
virtual void j(){
cout << "End: g1()" << endl;
}
};
验证代码,有点长,可跳过直接看结果
int main(){
Base b;
Drive d;
End e;
typedef void (*func)(void);
func f;
int* pBase = (int*)*(int*)(&b); //此时pBase 存储的是虚函数表的起始地址
cout << "顺序执行Base的虚函数表中的函数:" << endl;
f = (func)*(pBase + 0); //*pBase 得到虚函数表中第一个函数地址
f();
f = (func)*(pBase + 1);
f();
f = (func)*(pBase + 2);
f();
cout << endl << "顺序执行Drive的虚函数表中的函数:" << endl;
int* pDrive = (int*)*(int*)(&d);
f = (func)*(pDrive + 0);
f();
f = (func)*(pDrive + 1);
f();
f = (func)*(pDrive + 2);
f();
f = (func)*(pDrive + 3);
f();
cout << endl << "顺序执行End的虚函数表中的函数:" << endl;
int* pEnd = (int*)*(int*)(&e);
f = (func)*(pEnd + 0);
f();
f = (func)*(pEnd + 1);
f();
f = (func)*(pEnd + 2);
f();
f = (func)*(pEnd + 3);
f();
f = (func)*(pEnd + 4);
f();
return 0;
}
输出结果
顺序执行Base的虚函数表中的函数:
Base: f()
Base: g()
Base: h()
顺序执行Drive的虚函数表中的函数:
Drive: f()
Base: g()
Base: h()
Drive: i()
顺序执行End的虚函数表中的函数:
Drive: f()
End: g()
Base: h()
Drive: i()
End: j()
Drive重写了父类Base的Base:f()方法,所以Drive:f()覆盖了旧函数;Drive新增的i()方法添加至Drive虚函数表末尾。
End重写了父类Drive的Base:g()方法,所以End:g()覆盖了旧函数;End新增的j()方法添加至End虚函数表末尾。
多重继承
1、子类继承了多少个父类,子类就拥有多少个对应的虚函数表。
2、子类的对象中从起始地址开始顺序存储着虚函数指针,存储顺序和继承父类的顺序相同,每个虚函数指针指向对应虚函数表。虚函数指针大小为四个字节。
3、对于第一个虚函数表,如果子类中有第一个虚函数表中不存在的虚函数(即使是重写的其他虚函数表的函数),将会被视为新函数添加到第一个虚函数表的末尾。例如我重写了第二个虚函数表中的某个函数,而这个函数在第一个虚函数表中不存在,那么这个函数不止会覆盖第二个虚函数表中的旧函数,还会被视为新增的函数添加到第一个虚函数表的末尾。
4、如果子类重写了某个父类的虚函数,将会在对应的虚函数表中进行覆盖。
看代码比较好懂:
class Base1{
public:
virtual void f(){
cout << "Base1: f()" << endl;
}
virtual void g(){
cout << "Base1: g()" << endl;
}
virtual void h(){
cout << "Base1: h()" << endl;
}
};
class Base2{
public:
virtual void i(){
cout << "Base2: i()" << endl;
}
virtual void j(){
cout << "Base2: j()" << endl;
}
virtual void k(){
cout << "Base2: k()" << endl;
}
};
//分别重写了Base1的f()和Base2的i()
//新增加了d()
class Drive: public Base1, public Base2{
public:
virtual void f(){
cout << "Drive: f()" << endl;
}
virtual void i(){
cout << "Drive: i()" << endl;
}
virtual void d(){
cout << "Drive: d()" << endl;
}
};
验证代码,可直接看输出结果:
int main(){
Drive* d = new Drive();
typedef void (*func)(void);
func f;
cout << "顺序执行派生类Drive每个虚函数表中的函数:" << endl << endl << "第一个虚函数表:" << endl;
int* pDrive = (int*)*(int*)d; //此时pDrive存储着第一个虚函数表的地址
f = (func)*(pDrive + 0);
f();
f = (func)*(pDrive + 1);
f();
f = (func)*(pDrive + 2);
f();
f = (func)*(pDrive + 3);
f();
f = (func)*(pDrive + 4);
f();
cout << endl << "第二个虚函数表:" << endl;
pDrive = (int*)*((int*)d + 1); //此时pDrive存储着第二个虚函数表的地址
f = (func)*(pDrive + 0);
f();
f = (func)*(pDrive + 1);
f();
f = (func)*(pDrive + 2);
f();
return 0;
}
//输出
顺序执行派生类Drive每个虚函数表中的函数:
第一个虚函数表:
Drive: f()
Base1: g()
Base1: h()
Drive: i()
Drive: d()
第二个虚函数表:
Drive: i()
Base2: j()
Base2: k()
1、新增:对于第一个虚函数表而言,类Drive的Drive:i()和Drive:d()方法都是新增的,因为第一个虚函数表没有这两个函数,所以Drive:i()和Drive:d()会添加到第一个虚函数表的末尾。
2、覆盖:Drive重写了Base1:f()和Base2:i(),所以在虚函数表1中Drive:f()覆盖了Base:f(),在虚函数表2中Drive:i()覆盖了Base2:i()。
析构函数和虚函数
考虑这样一种情况:
class Base{
public:
Base(){};
~Base(){ //正确做法是加上virtual
cout << "Base decnst" << endl;
}
};
class Drive: public Base{
public:
Drive(){};
~Drive(){
cout << "Drive decnst" << endl;
}
};
int main(){
Base* b = new Base();
b = new Drive();
delete b;
}
//输出
Base decnst
因为Base的析构函数不是虚函数,所以当父类指针指向子类,调用方法时,执行的还是父类的方法。只析构了父类对象,没有析构子类对象。
所以在这种情况下,需要把父类的析构函数声明为virtual,这样才会调用子类的析构函数,正确地析构对象,上述代码改正后输出:
Drive decnst
Base decnst
先析构子类对象,再析构父类对象。
纯虚函数
纯虚函数的意义是提供一个公共接口,供派生类实现。格式为在虚函数后面加上"=0"。例如:virtual void attack() = 0;
1、带有纯虚函数的类是抽象类,抽象类不能实例化,但是可以声明为指针类型或者引用。
2、如果派生类没有实现纯虚函数的话,那么该派生类也是抽象类。