进阶了解C++(4)——多态

       在上篇文章中,简单的介绍了多态中的概念以及其相关原理。本文将针对多态中其他的概念进一步进行介绍,并且更加深入的介绍关于多态的相关原理。

目录

1. 抽象类:

2. 再谈虚表:

3. 多继承中的虚函数表:


1. 抽象类:

       在上篇文章中提到了,如果使用关键字virtual修饰一个成员函数,则这个成员函数被称为虚函数。此处,针对虚函数进行扩展,如果在虚函数的声明后面加上=0,则这个函数被称为纯虚函数。包含纯虚函数的类又叫抽象类,其特点是不能初始化出对象。即使是子类继承这个类,同样也不能初始化出对象。只有认为对纯虚函数进行重写,才能初始化出一个对象。

    给定一个抽象类及其子类如下:

//抽象类
class Person
{
public:
	virtual void func() = 0
	{
		cout << "Person-func()";
	}
};

class Teacher : public Person
{
public:

};

class Student : public Person
{
public:

};

如果向初始化出这三个类的对象,即:
 

int main()
{
	Person p;
	Student s;
	Teacher t;
}

此时编译器报错如下:

如果对子类中继承父类中的纯虚函数进行重写,即:

class Teacher : public Person
{
public:
	virtual void func()
	{
		cout << "Teacher-func()" << endl;
	}
};

class Student : public Person
{
public:
	virtual void func()
	{
		cout << "Student-func()" << endl;
	}
};

此时再去分别初始化两个子类的对象,即:

int main()
{

	Student s;
    s.func();
	Teacher t;
    t.func();
}

代码可以正常运行,且运行结果如下:

2. 再谈虚表:

在之前C++基础的文章中提到了,在构造函数中,存在初始化列表,初始化列表初始化成员变量的顺序并不是根据初始化列表的顺序,而是根据成员变量声明的顺序。对于虚函数,其也符合这个特性。具体可以用下面的代码进行证明:

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;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过监视窗口,查看对象b中虚表:

       可以看到,虚函数在虚表中存放的顺序,正是虚函数在类中声明的顺序。对于这一点,也同样可以在内存窗口中进行查看。

       

 从图中不难发现,对象b中的第一个地址,恰好对应了虚表指针的地址。此时再查看虚表中的内容,即:

不难看出,再内存窗口中,第二,第三条地址分别对应了虚表中两个虚函数的地址。

而对于子类,其生成的对象d中的内容如下:

对于子类对象的内容,可以分为两个部分,一是从父类中继承的内容,二是子类中自己的成员变量以及函数。在监视窗口中,可以看到子类继承了父类的虚表,并且对其中进行重写的虚函数的地址进行了覆盖。但是需要注意,在子类中,并不存在自己的虚表 。对于子类虚表中的函数指针如下:
在上面给出的图片中可以看出,蓝线连接的两个地址分别是父类、子类中的虚函数Func1(),但是因为这个函数在子类中发生了重写,因此,父类,子类中这两个虚函数的地址并不相同。

而对于紫线连接的两个虚函数,由于虚函数并未在子类中发生虚函数的重写,因此,父类,子类中俩个虚函数的地址相同。

如果对于子类,再添加一个虚函数,例如:

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}

	virtual void Func()
	{
		;
	}
private:
	int _d = 2;
};

 此时,在监视窗口中进行查看,子类对象的虚表中并没有出现新的虚函数的函数指针,但是在内存窗口中,却出现了一条新的地址,对于这个新的地址,一般认为就是子类中新加入的虚函数。至于具体的验证,将在文章后面给出。 

 

(注:为了方便演示,下面的代码在x86,即32位环境下运行)

在之前C++基础关于内存管理的文章中(C++(9)——内存管理-CSDN博客 )提到了系统根据不同的需求,将内存划分为不同的部分,具体如下:

1.栈:用于存储非全局、非静态的局部变量,函数参数,返回值等等

2.堆:用于程序运行时的内存的动态开辟

3.数据段(静态区):用于存储全局变量和静态变量

4.代码段(常量区):可执行代码\只读常量

在给出了上述概念后,文章将探讨一个 问题,即:虚表指针是存储在什么地方的。

为了方便测试,首先给出上面四个类型变量的地址,即:

int i = 1;//栈
	int* p = new int;//堆
	static int j = 0;//数据段(静态区)
	const char* p2 = "xxxxxxx";//代码段(常量区)

	printf("栈=%p\n", &i);

	printf("堆=%p\n", p);
	printf("静态区=%p\n", &j);
	printf("常量区=%p\n",p2);

打印结果如下:

对于如何获取虚表指针,本文提供一种方法:由于虚表指针存储在一个类的前四个字节,因此,只需要初始化出一个该类的对象,首先获取这个对象的指针,在将这个指针强转成int*类型,即可获取虚表指针,具体代码如下:

Base* B = &b;
	Derive* D = &d;

	printf("B=%p\n", *(int*)B);
	printf("D=%p\n", *(int*)D);

打印结果如下:

从上述区段以及两个虚表指针的指针对比来看,虚表指针应该存储在常量区,也就是代码段。

上面给出了如何获取虚表指针的存储地址,下面给出虚表中,如何获取虚表中存储各个虚函数的指针,具体方法如下:

typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{
	for (size_t i = 0; vf[i] != nullptr; i++)
	{
		printf("[%d] :%p", i, vf[i]);
	}
}
PrintVF((VF_PTR*) * (int*)&d);

打印结果如下:

如果在获取了上述指针后,直接调用这些函数指针,便可知道上述 获取的地址是否是类中的虚函数,即:
 

typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{
	for (size_t i = 0; vf[i] != nullptr; i++)
	{
		printf("[%d] :%p", i, vf[i]);
		VF_PTR f = vf[i];
		f();
	}
}

打印结果如下:

通过这个例子可以看出,虽然在上面添加新的虚函数Func3()时,在子类的虚表中并没有看到这个函数的地址,但是在次数,照样可以通过函数指针调用这个函数,这也间接证明了Func3()其实添加到了子类中,只是在监视窗口不可见。

3. 多继承中的虚函数表:

给定代码如下:

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

int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	return 0;
}

在上面给出的代码中,存在三个类,其中Base1,Base2被集成到了Derive中,由于Base1最先被继承到子类中,因此,可以认为,父类成员在子类的空间中的位置是最靠前的。对于&d表示取对象d的首地址,由于父类成员在空间中位置是最靠前的,因此,理论上p1==&d。而对于p2,由于其在Base1后继承,因此p2相对于p1是靠后的,因此,在子类中,存在着两张虚表,这两个虚表分别有着自己独立的地址。在监视窗口中,同样可以证明这一点:

而对于Derive中的虚函数func3(),为了验证func3()是存储在哪个虚表中的,可以用下面的代码进行检验:

PrintVF((VF_PTR*)*(int*)p1);

对于Base1中虚表中存储的函数指针打印结果如下:

下面打印Base2中虚表中的函数指针:

由此证明,子类中的虚函数func3()是存储在子类继承并且进行覆盖的Base1中的虚表。

  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

起床写代码啦!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值