抽象类和虚函数表是 C++中实现多态性的重要概念,它们对于学习 C++非常重要。
掌握抽象类和虚函数表的使用方法对于理解 C++的多态性是非常重要的。在 C++中,通过使用抽象类和虚函数表,可以实现基于多态性的各种功能,如继承、多态、模板等。同时,在实际应用中,抽象类和虚函数表也是常用的设计模式之一,如抽象工厂模式、观察者模式等。
目录
- 抽象类
- 接口继承和实现继承
- 虚函数表
- 多继承关系的虚函数表
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
和普通的虚函数不一样,一个纯虚函数无需定义。而且 =0 只能出现在类内部的虚函数声明处。我们也可以为虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个纯虚函数提供函数体。
class Person
{
public:
virtual void work() = 0;//纯虚函数
};
class Student : public Person
{
public:
virtual void work()
{
cout << "Student-学生上学" << endl;
}
};
class Teacher : public Person
{
public:
virtual void work()
{
cout << "Teacher-老师教书" << endl;
}
};
Person obj; //错误写法,Person类中声明了纯虚函数,不能定义Person的对象
Student obj_stu; //正确写法
Teacher obj_per; //正确写法
派生类继承基类的纯虚函数,如果不重写虚函数,那么这个派生类仍然是抽象类。所以必须对其进行重新定义(重写)才能实例出对象:
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
虚函数表
在了解虚函数表之前我们先来看一道常见的面试题:
//sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char _ch;
};
我们很可能会简单的以为只是常见的类的内存对齐问题,认为结果是8,但是结果是:
为什么结果是12呢?原因就是有虚函数表的存在。
注意:我们要理清楚虚函数表(虚表)与虚基表的区别,不要搞混(虚基表)
那么派生类重写(覆盖)基类的虚函数,虚函数表又是以什么样的形式变化呢?
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
char _ch;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
完成重写的虚函数虚表对应的位置覆盖成重写的虚函数。、
那么虚函数表是存放在哪个区的呢?栈?堆?静态区?常量区?
我们通过下面代码可以大致猜测一下:
int main()
{
int a = 0;
cout << "栈:" << &a << endl;
int* p1 = new int;
cout << "堆:" << p1 << endl;
const char* str = "hello world";
cout << "代码段/常量区:" << (void*)str << endl;
static int b = 0;
cout << "静态区/数据段:" << &b << endl;
Base be;
cout << "虚表:" << (void*)*((int*)&be) << endl;
return 0;
}
运行结果:
所以我们猜测虚表应该存放在静态区
这里给大家提供一个打印虚表的办法:
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
VFPTR * vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
1.先取b的地址,强转成一个 int * 的指针
2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3.再强转成 VFPTR *,因为虚表就是一个存VFPTR类型(自己重定义的虚函数指针类型)的数组。
4.虚表指针传递给PrintVTable进行打印虚表
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
相同类型的对象共用一个虚表
多继承关系的虚函数表
且看下面代码分析:
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 func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
主要的关系就是Derive同时继承Base1与Base2 ,Derive重写了func1但没有重写func2,且自生还有一个func为虚函数。
多继承虚函数表:
我们使用上面说过的打印虚表的方法:
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀