C++ 多态详解
目录
多态的概念
定义
C++直接支持多态条件
举例
回顾继承中遇到的问题
虚函数-虚函数指针-虚函数列表
虚函数
虚函数指针
虚函数列表
虚函数调用流程
虚函数于普通成员函数的区别
多态实现的原理
前提
原理
流程
虚析构
问题
解决
纯虚函数
虚函数实现多态的缺点
多态的概念
定义
多态:相同的行为方式导致了不同的行为结果,同一行语句展现了多种不同的表现形态,即多态性。C++多态,父类的指针可以指向任何继承于该类的子类对象,父类指针具有子类的表现形态,多种子类表现为多种形态由父类指针统一管理,那么这个父类指针就具有了多种形态,即多态。
C++直接支持多态条件
1.在继承关系下,父类指针指向子类对象(而非子类指针指向子类对象),通过该指针调用虚函数。
2.父类中存在虚函数(virtual修饰),且子类中重写了父类的虚函数。
重写:在继承条件下,子类定义了与父类中虚函数一摸一样的函数(包括:函数名、参数列表、返回值)我们称之为重写。
举例
class CFather {
public:
//虚函数 virtual: 定义虚函数的关键字
virtual void fun() {
cout << __FUNCTION__ << endl;
}
};
class CSon :public CFather {
public:
void fun() { //子类的函数一旦重写了父类的虚函数,即使不加关键字,也会被认定为是虚函数
cout << __FUNCTION__ << endl;
}
};
int main() {
CFather* pFa = new CSon;//父类指针指向子类对象
pFa->fun();
CSon* pSon = new CSon;//不叫多态
pSon->fun();
return 0;
}
用子类指针调用出子类对象不叫多态。
回顾继承中遇到的问题
还记得我们在继承中遇到这样一个问题,我们无法通过父类指针调用子类中不统一的函数,而最后我们是通过类成员函数指针来解决这个问题的,不过实现的过程也是十分的艰难。如今我们学了多态,那么这个问题解决起来就十分简单了。
我们直接在父类中创建一个eat虚函数,使子类中的eat也变为虚函数,那么他就可以指向子类中的不统一的函数了。
class CPeople {
public:
int m_money;
CPeople() :m_money(100) {}
void cost(int n) {
cout << __FUNCTION__ << endl;
m_money -= n;
}
void play() {
cout << "溜溜弯" << endl;
}
void drink() {//如果增加公共的功能,属性,只需要在父类中增加一份即可
cout << "喝饮料" << endl;
}
virtual void eat() {
}
};
class CWhite : public CPeople {
public:
//CWhite():m_money(100){}
void eat() {
cout << "吃牛排" << endl;
}
};
class CYellow : public CPeople {
public:
void eat() {
cout << "小烧烤" << endl;
}
};
class CBlack : public CPeople {
public:
void eat() {
cout << "西瓜炸鸡" << endl;
}
};
调用的时候就可以直接用父类指针指向子类成员函数了
void fun(CPeople* pPeo) {
pPeo->cost(10);
//(pPeo->*p_fun)(); //类成员函数指针
pPeo->eat(); //多态解决
pPeo->play();
pPeo->drink();
}
int main() {
fun(new CBlack);
fun(new CYellow);
return 0;
}
虚函数-虚函数指针-虚函数列表
虚函数
定义虚函数使用关键字virtual,虚函数是实现多态必不可少的条件之一。
我们知道,如果创建的类为空类,那么这个类所占用的空间为1个字节,并且如果在这个类中创建一个函数,那么此时类所占空间仍为1个字节,因为普通函数不会占用类的空间。但是如果我们在类中创建一个或多个虚函数,那此时类所占的空间就为4个字节了,那是为什么呢?
class CTest {
public:
void fun() {
cout << __FUNCTION__ << endl;
}
virtual void fun1() {
cout << __FUNCTION__ << endl;
}
virtual void fun2() {
cout << __FUNCTION__ << endl;
}
};
int main() {
cout << sizeof(CTest) << endl;
return 0;
}
由于我们不管创建多少个虚函数类占的内存空间都为4,所以可以得出占的这个内存与虚函数有关,但是与虚函数的数量无关。
我们通过调试器发现,在局部变量中多出了一个名为__vfptr二级指针,由于我的系统为x86 32位操作系统,所以这个指针占的字节为4,那么这个指针就是虚函数指针。
虚函数指针
__vfptr (虚函数指针):在一个类中,当存在虚函数时,在定义对象的内存空间的首地址会多分配出一块内存,在这块内存中增加一个指针变量(二级指针 void**),也就是虚函数指针。
· 属于对象的,由编译器默认添加,可以看作是一般的成员属性。
· 定义对象时才存在(内存空间得以分配),多个对象多份指针。
· 指向了一个函数指针数组(虚函数列表,vftable)。
· 每个对象中的虚函数指针指向的是同一个虚函数列表。
· 定义对象调用构造函数,执行初始化参数列表时,被初始化才指向了虚函数列表。
class CTest {
public:
//int m_a;
CTest()/* : __vfptr(vftable) */ /*:m_a(1)*/{
cout << __FUNCTION__ << endl;
}
void fun() {
cout << __FUNCTION__ << endl;
}
virtual void fun1() {
cout << __FUNCTION__ << endl;
}
virtual void fun2() {
cout << __FUNCTION__ << endl;
}
};
int main() {
cout << sizeof(CTest) << endl;
CTest tst;
CTest tst2;
return 0;
}
注意:这里两个对象的虚函数指针是指向的地址相同,并不是他们俩本身的地址相同,不要弄混了。
测试虚函数指针在对象内存空间的首地址被创建:
class CTest {
public:
int m_a;
CTest()/* : __vfptr(vftable) */ :m_a(1){
cout << __FUNCTION__ << endl;
}
void fun() {
cout << __FUNCTION__ << endl;
}
virtual void fun1() {
cout << __FUNCTION__ << endl;
}
virtual void fun2() {
cout << __FUNCTION__ << endl;
}
};
int main() {
cout << sizeof(CTest) << endl;
CTest tst;
CTest tst2;
CTest tst3;
cout << &tst3 << " " << &tst3.m_a << endl;
return 0;
}
虚函数列表
虚函数列表(vftable):是一个函数指针数组,数组每个元素为类中虚函数的地址。
· 属于类的,在编译期存在,为所有对象共享。
· 必须通过真实存在的对象调用,无对象或空指针对象无法调用虚函数。
CTest* ptst = nullptr;
ptst->fun(); //普通 可以调
//ptst->fun2(); //虚函数 不能调 程序异常 虚函数指针要找到对象的首地址,但是对象指向空根本就没有地址
虚函数调用流程
1.定义对象获取对象内存首地址中的__vfptr。
2.间接引用找到虚函数指针指向的虚函数列表vftable。
3.通过下标定位到要调用的虚函数元素(虚函数地址)。
4.通过这个地址(函数入口地址)调用到了虚函数。
模拟虚函数调用过程:
//*(int*)&tst == vfptr;
typedef void (*P_FUN)();
P_FUN p_fun1 = (P_FUN)((int*)(*(int*)&tst))[0];
P_FUN p_fun2 = (P_FUN)((int*)(*(int*)&tst))[1];
(*p_fun1)();
(*p_fun2)();
虚函数于普通成员函数的区别
· 调用流程不同:虚函数的调用流程相比普通成员函数而言复杂得多,这是他们的本质区别。
· 调用效率不同:普通的成员函数通过函数名(即函数入口地址)直接调用执行函数,效率高速度快,虚函数的调用需要虚函数指针-虚函数列表的参与,效率低,速度慢。
· 使用场景不同:虚函数主要用于实现多态,这一点是普通函数无法做到的。
多态实现的原理
前提
虚函数列表是属于类的,父类和子类都会有各自的虚函数列表,__vfptr属于对象的,每个对象都有各自__vfptr。
原理
- 由于子列继承父类,不但继承了父类的成员,也会继承父类的虚函数列表。
- 编译器会检查子列是否有重写父类的虚函数,如果有,在子类的虚函数列表中会替换掉父类的虚函数,一般称之为覆盖,覆盖后便指向了子类的虚函数。
- 如果子类没有重写的父类虚函数,父类虚函数会保留在子类的虚函数列表中。
- 如果子类定义了独有的虚函数,按顺序依次添加到虚函数列表结尾。
以上这些过程在编译阶段就完成了。
流程
父类指针指向子类对象,__vfptr在子类的初始化参数列表中被初始化,指向子类的虚函数列表,申请哪个子类对象__vfptr就指向了哪个子类的虚函数列表。调用虚函数时执行虚函数的调用流程,则实现了多态。
class CFather {
public:
virtual void fun1() {
cout << __FUNCSIG__ << endl;
}
virtual void fun2() {
cout << __FUNCSIG__ << endl;
}
};
class CSon :public CFather {
public:
virtual void fun1() {
cout << __FUNCSIG__ << endl;
}
virtual void fun3() {
cout << __FUNCSIG__ << endl;
}
void fun4() {
cout << __FUNCSIG__ << endl;
}
};
定义(new)哪个子类对象,虚函数指针就会指向哪个类的虚函数列表,与哪个指针无关
int main() {
CFather fa;
CSon son;
//定义(new)哪个子类对象,虚函数指针就会指向哪个类的虚函数列表,与哪个指针无关
CFather* pFa = new CSon; //虚函数指针->子类的虚函数列表(而不是父类)
pFa->fun1(); //void __thiscall CSon::fun1(void)
pFa->fun2(); //void __thiscall CFather::fun2(void)
((CSon*)pFa)->fun3(); //void __thiscall CSon::fun3(void)
return 0;
}
虚析构
问题
在多态下,父类的指针指向子类的对象。最后在回收空间的时候,是按照父类的指针类型delete的,所以只调用了父类的析构,子类的析构并没有执行,这样的话就有可能导致内存泄漏。
注意:delete 自动调用哪个析构函数,取决于传递指针的类型。
CFather* pFa = new CSon;
delete pFa; //delete 自动调用哪个析构函数,取决于传递指针的类型
//delete (CTest*)pFa; //仅用于测试,不要去写
//pFa->~CFather(); //ok
//pFa->~CSon(); //error
pFa = nullptr;
解决
这个问题用虚析构来解决,即把父类的析构函数变为虚析构函数,delete pFa;时,调用析构会发生多态行为,从而真正调用的是子类的析构,最后回收对象内存空间时,再调用父类的析构。
class CFather {
public:
CFather() {
cout << __FUNCSIG__ << endl;
}
virtual ~CFather() { //虚析构
cout << __FUNCSIG__ << endl;
}
};
class CSon :public CFather{
int* m_p;
public:
CSon() {
m_p = new int(1);
cout << __FUNCSIG__ << endl;
}
~CSon() {
cout << __FUNCSIG__ << endl;
if (m_p)
delete m_p;
m_p = nullptr;
}
};
注意:在用多态时,父类的析构一定为虚析构。
纯虚函数
在多态下,有时抽象出来的父类的虚函数作为接口函数,并不知道如何实现或不需要实现,就是为了多态而生的,只有继承的子类才明确如何实现,可以把父类的虚函数变为纯虚函数。
纯虚函数写法:在一般的虚函数后加上=0; 只声明 不需要定义。
纯虚函数的特点是:当前类不必实现,而子类必须要重写实现父类的纯虚函数。
//抽象类:包含纯虚函数的类 称之为抽象类,是不允许定义对象的。
class CPeople {
public:
//纯虚函数:在一般的虚函数后加上=0; 只声明 不需要定义,在子类中一定要重写这个纯虚函数
virtual void eat() = 0;
};
//具体类:
class CTeacher :public CPeople {
public:
virtual void eat() { //必须要重写父类的纯虚函数
cout << "在食堂饺子" << endl;
}
};
包含纯虚函数的类叫抽象类,抽象类不能实例化对象(也就是不允许定义对象),继承这个抽象类的派生类(子类)叫具体类,具体类必须重写定义抽象类里面的所有的纯虚函数。
注意:如果父类的纯虚函数没有被重写,不允许定义子类对象。
虚函数实现多态的缺点
1.效率问题:调用虚函数效率低,速度慢。
2.空间问题:定义每一个对象都会额外开辟指针大小的空间,虚函数列表占用程序的内存空间,并且会随着继承的层级递增,占用的空间越来越大。
3.安全问题:通过其他方法可以模拟虚函数的调用,跨过了访问修饰符的限制。私有的函数最好不要变为虚函数,否则会有安全隐患。