虚函数表以及单继承多继承对象模型

首先来说说什么是虚函数?

简单来说,那些被virtual关键字修饰的成员函数,就是虚函数。

我们为什么要使用虚函数呢?它的作用是什么?

虚函数就是用来实现多态性的,多态是将接口与实现分离(实现以共同的方法,但因个体差异,而采用不同的策略)。


纯虚函数


在虚函数后面写上=0,这就是纯虚函数。包含纯虚函数的类叫做抽象类(接口类)。


概念:

1.派生类重写基类的虚函数实现多态,要求函数名,参数列表,返回值完全相同。(协变除外)


协变返回类型:在c++中,只要原来的返回类型是指向基类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型。

通过下面这个例子我们来引出虚函数表

#include<iostream>
#include<windows.h>


using namespace std;


class Base
{
public:
virtual void func1()
{}


virtual void func2()
{}


private :
int a;
};
void Test1()
{
Base b1;
int a = 1;
}


int main()
{
Test1();
system("pause");
return 0;
}

我们来看看b1的监视窗口


这里的_vfptr就是虚函数表

一个类的虚函数表就是存在这个类的首地址(最前边的地方)


我们来研究一下单继承的内存格局

//单继承

#include<iostream>
#include<windows.h>


using namespace std;


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 << "Derived::func1" << endl;

}


virtual void func3()
{
cout << "Derived::func3" << endl;


}


virtual void func4()
{
cout << "Derived::func4" << endl;


}


private:
int b;
};


int main()
{
Derive d1;
system("pasue");
return 0;
}

对于Derire来说,它的虚表里会有什么呢?

由于子类的func1重写了父类的func1,所以我们猜想子类的虚表里有子类的func1,父类的func2,子类的func3,子类的func4,现在我们就来调出监视窗口来看看子类的虚表里存的是什么。


我们发现监视窗口只有func1和func2,这是为什么呢?

下面我们就来用一段代码看看它到底是怎么回事,是我们分析错了吗?


#include<iostream>
#include<windows.h>


using namespace std;


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 << "Derived::func1" << endl;

}


virtual void func3()
{
cout << "Derived::func3" << endl;


}


virtual void func4()
{
cout << "Derived::func4" << endl;


}


private:
int b;
};


//打印虚函数表
typedef void(*FUNC)(void);
void PrintVTable(int *VTable)
{
cout << "虚表地址" << VTable << endl;


for (int i = 0; VTable[i] != 0; ++i)
{
printf("第%d个虚函数地址:OX%x,->", i, VTable[i]);


FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}



int main()
{
Derive d1;
PrintVTable((int*)(*(int*)(&d1)));
system("pasue");
return 0;
}


PrintVTable((int*)(*(int*)(&d1)))是挺难理解的

首先我们拿到的是d1的地址,然后把它强转成int*,这里强转成int*是为了让它读取到前四个字节的内容(指向虚表的地址),然后再对这个地址解引用我们现在已经拿到虚表的首地址的内容(虚表里面存储的第一个函数的地址)了,但是此时这个变量的类型解引用后是int,不能传入参数,所以我们需要再对它进行一个int*的强制类型转换,这样我们就可以传入参数了,开始执行函数,我们来看看输出的结果:



那么输出结果到底对不对呢,我们来看看它的内存分布:







我们发现内存里边的地址和我们打印出来的地址一模一样,所以我们前边对单继承输出结果的判断是正确的,那为什么在没有打印地址的函数时,它打印出来的为什么和我们的推测不一样呢,那可能就是VS的bug了吧!


下来,我们来看看多继承的内存格局

#include<iostream>
#include<Windows.h>
using namespace std;


class Base1
{
public:
virtual void func1()
{
cout << "Base1::func1" << endl;
}


virtual void func2()
{
cout << "Base1::func2" << endl;
}


private:
int b1;
};


class Base2
{
public:
virtual void func1()
{
cout << "Base2::func1" << endl;
}


virtual void func2()
{
cout << "Base2::func2" << endl;
}


private:
int b2;
};


class Derive :public Base1, public Base2
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}


virtual void func2()
{
cout << "Derive::func3" << endl;
}


private:
int d1;
};


typedef void(*FUNC)(void);
void PrintVTable(int *VTable)
{
cout << "虚表地址" << VTable << endl;




for (int i = 0; VTable[i] != 0; ++i)
{
printf("第%d个虚函数地址:OX%x,->", i, VTable[i]);




FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}


void Test1()
{
Derive d1;
//Base虚函数表在对象Base1后面
int* VTable = (int*)(*(int*)&d1);
PrintVTable(VTable);
int*VTable2 = (int*)(*((int*)&d1 + sizeof(Base1) / 4));
PrintVTable(VTable2);
}


int main()
{
Test1();
system("pause");
return 0;
}


我们发现它的输出结果是: 在Base1中有Derive::func1,Base1::func2,Derive::func3;

                                                                               在Base2中有Derive::func1,Base2::func2;

从这组输出结果我们就可以总结出:当涉及多继承时,子类的虚函数会存在先继承的那个类的虚函数表里。




那么为什么静态成员函数不能定义为虚函数?

因为静态成员函数它是一个大家共享的一个资源,但是这个静态成员函数没有this指针而虚函数表只有对象才能调到,但是静态成员不需要对象就可以调用,所以这里是有冲突的。

为什么不要在构造函数和析构函数里面调用虚函数?

构造函数当中不适合用虚函数的原因是:在构造对象的过程中,还没有为“虚函数表”分配内存。所以这个调用也是违背先实例化后调用的准则。


析构函数当中不适用虚函数的原因是:一般析构函数先析构子类的,当你在父类中调用一个重写的fun()函数,虚函数表里面就是子类的fun()函数,这时候子类已经被析构了,当你调用的时候就会调用不到。


但我们要尽量最好把基类的析构函数声明为虚函数


主要这里我用的是声明而不是调用,以防止有的人不仔细看和前边在析构函数中不能调用虚函数搞混了




















猜你喜欢

转载自blog.csdn.net/huaijiu123/article/details/77916649
今日推荐