关于c++的三大特性 --- 多态(底层原理)

发布时间:2024年01月19日

目录

多态的原理

虚函数表

底层

?????????打印虚表

多继承的虚函数表


多态的原理

虚函数表

建议看下面的内容之前,先看一下c++特性之多态


这里我们先来看一个笔试题:请问 sizeof(Base)是多少?

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

private:
     int _b = 1;
};

答案是:8? ?为什么呢?按照内存对齐的规则,这里应该是4byte,为什么是8byte呢?

调试一下:

???????? 通过观察测试我们发现,除了_b成员,还多一个__vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
????????开始下面的内容之前,这里简单总结一下关于多态的知识点,值得注意的是:多态的底层原理是严格按照构成多态的条件来执行的?

构成多态的两个条件:

1. 虚函数的重写 ?--- 三同(函数名,参数,返回值)?
? ? a.例外(协变):返回值可以不同,必须是父子关系指针或者引用
? ? b.子类虚函数可以不加virtual
2. 必须是基类指针或者引用调用虚函数

a.不满足多态 --- 看调用者的类型,调用这个类型的成员函数
b.满足多态 --- ? 看指向对象的类型,调用这个类型的成员函数 ?

示例:

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
}

调试:

通过观察和测试,我们发现了以下几点问题:
  • 1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,一部分是虚表指针,也就是存在的另一部分是自己的成员。

  • 2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

  • 3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

  • 4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。(vs系列)

  • 5. 总结一下派生类的虚表生成:
  • a.先将基类中的虚表内容拷贝一份到派生类虚表中
  • b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

  • 6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
  • 答:虚函数存储虚表,虚表存在对象中。
  • 注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。
底层
上面分析了这个半天了那么多态的原理到底是什么?
示例:
class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

  • 1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。

  • 2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。

  • 3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

  • 4. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

例图:(构成多态的调用)

例图:(不构成多态的调用)

例图:(为什么重写也被称为覆盖,原因如下)
????????这里有个问题,为什么这里构成多态的条件必须是基类的引用或者指针,为什么不能是基类的对象?(在继承中我们知道,子类是可以切片赋给父类的,不熟悉的可以看一下 c++特性之继承

可以吗?答案是,不可以!!!?
  • 我们知道,在切片的时候,子类给给父类的成员会被拷贝构造过去,而这里的问题是,虚表能不能被拷贝?
  • 如果能,那么传父类对象也是能实现多态的。
  • 如果不能拷贝过去,那么父类的虚函数指针永远指向的是父类的虚表。
  • 但是,子类对象切片的时候,能不能拷贝虚表? 不能拷贝
  • 如果对虚表进行深拷贝或者拷贝指针过去,我们会发现父类的虚表就乱了,父类虚表指向的虚函数是父类的还是子类的根本分不清,并且一个父类对象拥有子类的虚表是十分不合理的
  • 所以结论是:子类切片给给父类(基类)只拷贝成员,不拷贝虚表

示例:

打印虚表
class Base{
public:
	virtual void Func1(){cout << "Base::Func1()" << endl;}
	virtual void Func2(){cout << "Base::Func2()" << endl;}
	void Func3(){cout << "Base::Func3()" << endl;}
};

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

//打印虚表 vf:virtual function  table:一览表

//注意:( vs系列 ) 虚函数表本质是一个存虚函数指针的指针数组
//      一般情况这个数组最后面放了一个nullptr。( g++没有 )

typedef void(*VF_PTR)() ; 

void print_vf_table(VF_PTR table[])
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]: %p -> ",i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;

	print_vf_table((VF_PTR*)(*(int*)&b));
	print_vf_table((VF_PTR*)(*(int*)&d));

	//虚表是在什么阶段生成的呢?  - 编译
	//对象中的虚表指针是在什么时候进行初始化的呢?   - 构造函数(初始化列表)
	//虚表存储在什么地方? - 代码段(常量区)

	//print_vf_table((*(VF_PTR**)&b));
	//print_vf_table((*(VF_PTR**)&d));
	
	return 0;
}

多继承的虚函数表
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;
};

//base1 , base2各自有两个虚函数
//derive多继承base1,base2 
//重写虚函数 func1()
//新增虚函数func3()

class derive : public base1, public base2 {
public:
    virtual void func1() { cout << "derive::func1" << endl; }
    virtual void func3() { cout << "derive::func3" << endl; }
private:
    int d1;
};

问题1:derive一共会生成几张虚表?

问题2:func3()会放进哪张虚表,还是所有虚表都会放进去?

调试一下:

????????好像只生成了两张虚表,没看到func3().这样,我们再打印一下虚表看一下,但是这里遇到一个问题,第一张虚表指针刚好指向对象的头四个字节,怎样取到第二张虚表表指针?

????????结论是: func1完成了重写,func3放进了第一张虚表,但是这里存在一个问题,为什么多继承之后,虚表中重写的func1地址不同?
可以看到,这里调用的确实是同一个函数,但是为什么地址不同?
  • 可以理解为后声明的对象虚表地址被封装过
    ?
很粗糙,将就看一下:


以上仅供参考
文章来源:https://blog.csdn.net/m0_73969113/article/details/135485149
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。