栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

c++(22)多态和虚函数、虚函数表和vptr、对象指针的步长

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

c++(22)多态和虚函数、虚函数表和vptr、对象指针的步长

1、什么是多态

父类指针指向子类对象

由继承而产生的相关却不同的类,其对象对同一消息会做出不同的响应。就是说:基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。

多态是对象对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统 升级、维护、调试的工作量和复杂度。

C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

2、多态的三个必要条件

(1)必须存在继承关系;

(2)继承关系中必须有同名的虚函数,并且它们是覆盖关系(虚函数重写)。

(3)存在基类的指针或引用,通过该指针调用虚函数。(父类指针指向子类对象)

3、虚析构函数

在设计类的时候,只要考虑到该类可能被继承,就要把析构函数写成续析构函数。

然后我们来解释上面这句斜体的话,为什么一定要这么做。先看一下下面的例子。

#include 
#include 
#include 
using namespace std;

class father
{
public:
	father()
	{
		cout<<"father()....."<showname();//期望发生多态
	
	delete p;//期望按照构造顺序发生,相反顺序的析构
	
}

int main(void)
{
	test();
	
	cout<<"---------------------"< 

当我们用基类指针指向子类对象时,virtual虚函数方法实现了多态。但是当delete 基类指针的时候,并没有调用子类的析构函数。

这是因为编译器在delete的时候,只针对指针p的类型father *,做了释放。只调用了父类的析构函数。乜有析构子类。我们希望父类子类两个析构函数都被调用,以免出现内存泄露。

要做到以上的目的,其实只要调用子类的析构就可以了,子类析构调用会自动调用父类的析构,所以析构函数也一定要是虚函数,也就是父类析构函数必须定义为虚析构函数。

将父类的定义改为以下的内容

class father
{
public:
	father()
	{
		cout<<"father()....."< 

执行结果达到了预期

4、虚函数表和vptr指针

下面我们来研究一下多态的原理,为什么父类指针指向子类对象时,调用virtual成员函数会发生多态。

当类中声明虚函数时,编译器会在类中生成一个虚函数表,是一个用来储存类成员函数指针的数据结构。这个虚函数表示存在于只读区。

类中的virtual 虚函数会被编译器放入虚函数表中。只要当类中存在虚函数,那么每个对象中对会有一个指向虚函数表的指针(vptr指针)。

#include 
#include 
#include 
using namespace std;

class parent
{
public:
	virtual void func1()
	{
		cout<<"parent::func1 ... "<func1();
	parent *pc = new child;
	pc->func2();
	
	return 0;
}


 上面的例子,当编译器在编译的时候会分别为类parent和类child创建属于各自类的虚函数表。并d当创建父类指针指向对象的时候,会初始化vptr指针,指向该对象所属类的虚函数表。

 通过指向子类对象的父类指针,调用成员函数时,会先判断是否为virtual 虚函数,如果是虚函数的话,会通过vptr访问对应的类虚函数表中所存放的成员函数指针。依次实现多态。

5、vptr指针分步初始化

当创建父类指针指向子类对象时。parent *p = new child;

*p所指向的空间,只有一个vptr,这个毋庸置疑。但是当new child时,会先调用父类的构造函数,此时vptr指向的是父类的虚函数表。当父类构造函数完成的时候,再调用子类构造函数的时候,vptr才会指向子类的虚函数表。这就叫做vptr指针分步初始化。

#include 
#include 
#include 
using namespace std;

class parent
{
public:
	parent(int pid)
	{
		cout<<"parent(int pid)..."<pid = pid;
	}
	
	virtual void func()
	{
		cout<<"parent::func ... "<cid = cid;
	}
	
	virtual void func()
	{
		cout<<"child::func ... "< 

上面的例子,new 子类对象创建的空间地址赋值给父类指针。在父类构造函数中,调用了虚函数func,此时由于还没有完成父类构造的执行,所以当前的vptr指向的其实是父类的虚函数表,所以调用func,打印的也是父类的func。

完成父类构造后,执行子类构造,vptr也重新指向了子类的虚函数表。这个就是vptr分步初始化的表现。

但是我们常规写代码的时候不会把成员方法写进构造函数里,这样容易出现歧义,不易于走读代码。正常构造函数里面只对成员变量进行初始化。

6、类对象指针的步长

我们知道类其实也是一种数据结构,我们可以用数组的方式来实例化对象。这个时候使用多态的话,有一个大坑!!!我们先看下面的

例1

#include 
#include 
#include 
using namespace std;

class parent
{
public:
	parent(long a)
	{
		this->a = a;
	}
	
	virtual void print()
	{
		cout<<"parent::print ... a="<a<class child : public parent
{
public:
	child(long a):parent(a)
	{
	}
	
	virtual void print()
	{
		cout<<"child::print ... a="<a< 

执行结果:

在打印完child数组的第一个对象的方法后,就崩溃了。

因为for循环中的pc++并不代表pc所指向的数组元素,向后+1。而是代表所指向空间的大小++,通俗点说就是pc++  ==》  pc += sizeof(parent)。他会在&ArrChild[0]的地址上,向后偏移sizeof(parent)个字节。

 第一次能够执行成功,纯属侥幸,因为子类没有自己的成员变量,在空间大小上完全和父类是一样的。上面黑框表示的是ArrChild数组的地址空间,pc指针的类型是parent *,pc++会按照父类parent对象的大小偏移。恰好按照一样的大小来偏移。(上图画的其实不够好,应该还有vptr的空间大小)

第二次,当我们给子类加上一个long b的成员变量的时候,空间上子类和父类的大小就不一样了。在执行的pc++的时候,就不会恰好停在元素对象的地址上,从而出现段错误。所以在使用基类指针的时候,也要额外小心,尽量不要使用数组访问的方式。

ps

Q:猜猜我上面的例子,整数数据为什么用long,而不用常见的int?

A:因为我的linux系统是64位的,而且数据结构按照8对齐。父类中一个int是4个字节,再加上vptr 相当于是void *的大小 8个字节,加起来是12字节。我们说过class跟struct是一样的,会有对齐规则,按8对齐就变成了16字节大小。子类,即使再加上一个int,也就恰好才是16字节,也不会出现崩溃。  

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1019197.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号