一、虚函数 (类继承中的一种函数) 概念
概念
被virtual修饰的函数叫做虚函数。
为什么说虚函数是类继承中的一种函数呢? 因为虚函数的作用确实主要是在类的继承中体现的(即派生类和基类)
虚函数的作用就是当基类和派生类中都有一个同名字的函数。 当基类指针存放派生类对象的地址时。通过该指针访问同名成员函数的时候,会出现一下两种情况:
那到底是调用基类的还是调用派生类的呢?
.
.
如果该函数在基类中是虚函数:则调用派生类 — 这里就体现了虚函数能让基类类和派生类的成员函数突出独特性。
.
如果该函数在基类中不是虚函数:则调用基类。
例子
#include <iostream>
using namespace std;
class f
{
public:
int f_data;
f(int f_data = 10)
{
this->f_data = f_data;
}
virtual void display()
{
cout<<"我是基类的成员函数"<<f_data<<endl;
}
};
class c : public f
{
public:
int c_data;
c(int c_data = 10)
{
this->c_data = c_data;
}
void display()
{
cout<<"我是pai派生类的成员函数"<<c_data<<endl;
}
};
int main()
{
//只需要定义一个派生类
c c_1(9);
f * f_p = &c_1;//定义一个基类存放派生类对象的地址
f_p->display();//这个display是基类的还是派生类的? 如果display在基类中不是虚函数则调用基类的,如果是虚函数则调用派生类的
return 0;
}
用virtual修饰(即基类中的是虚函数) 运行效果:
不用virtual修饰(即基类中的不是虚函数)运行效果:
注意
在基类用virtual声明成员函数为虚函数。
.
这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。
.
在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
.
C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。
.
如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
.
定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
二、类中的虚表中的虚函数
前置概念
只有类中有虚函数的前提下,在实例化类对象的时候才会生成虚表,虚表其实一个指针变量,这这个指针变量解引用之后得到一个地址,这个地址上存放了第一个虚函数的首地址(一级指针),则这个地址是二级指针变量的地址(三级指针),所以这个虚表指针变量只能是一个四级指针变量(四级针变量解引用才得到三级指针变量),而这个二级指针变量就是存放第一个虚函数的地址。如下图所示:
.
重要的概念:类对象的地址等于虚表的地址
有了虚表的存在,我们就可以通过类对象的地址调用虚表里面的虚函数
通过类对象输出虚表中的虚函数首地址以及通过虚函数指针调用虚函数
#include <iostream>
using namespace std;
class demo
{
public:
virtual int Fun_1()
{
cout<<"我是第一个虚函数" <<endl;
return 0;
}
virtual int Fun_2()
{
cout<<"我是第二个虚函数" <<endl;
return 0;
}
};
int main()
{
class demo d_1;
unsigned long long * p = (unsigned long long * )(*((unsigned long long * )(&d_1)));
cout<<"这是存放第一个虚函数首地址的首地址:"<< p <<endl;
cout<<"这是存放第二个虚函数首地址的首地址:"<< p+1 <<endl;
int(*v_p1)() = (int (*)())(*p);//就是第一个虚函数的函数地址
int(*v_p2)() = (int (*)())(*(p+1));//就是第二个虚函数的函数地址
return 0;
}
注意:指针解引用获取虚表和获取虚表元素的时候,因为虚表是没有类型的,所用需要把指针类型强转成 long long * int * *等等等,只要解引用能获取8个字节即可,因为解引用是获取地址一个地址一般是8个字节 。
运行效果如下: