目录
多态的概念
定义
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)();
虚函数于普通成员函数的区别
· 调用流程不同:虚函数的调用流程相比普通成员函数而言复杂得多,这是他们的本质区别。
· 调用效率不同:普通的成员函数通过函数名(即函数入口地址)直接调用执行函数,效率高速度快,虚函数的调用需要虚函数指针-虚函数列表的参与,效率低,速度慢。
· 使用场景不同:虚函数主要用于实现多态,这一点是普通函数无法做到的。