C++三大特性—多态 “抽象类与虚函数表”

抽象类和虚函数表是 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呢?原因就是有虚函数表的存在。
在这里插入图片描述
在这里插入图片描述

(12的原因是vs的X86默认对齐数码是根据平台的字长(即指针的大小)来确定的,也就是4。换为Linux环境则可能为8)

注意:我们要理清楚虚函数表(虚表)与虚基表的区别,不要搞混(虚基表)


那么派生类重写(覆盖)基类的虚函数,虚函数表又是以什么样的形式变化呢?

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);

在这里插入图片描述
在这里插入图片描述


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

侠客cheems

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值