由浅入深分析c++多态原理

背景

多态就是多种状态,不同的对象去完成同一件事时,会有不同的结果。
例如买票:普通人买票是全价买,学生买票就是半价买。

  • 我们设计一个Person类是父类,Student是子类,分别实现买票的成员函数。
#include<iostream>
using namespace std;
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 p;
	Func(p);   //全价票
	Student s;
	Func(s);   //半价票
	return 0;
}

在这里插入图片描述

该功能就是由多态实现的。

多态

构成多态的两个条件

  • 必须通过基类的指针或者引用去调用虚函数
  • 被调用的函数必须是虚函数且派生类必须对虚函数进行重写

虚函数

使用virtual修饰的类成员函数称为虚函数。

class Person
{
    
    
public:
	virtual void BuyTicket() {
    
     cout << "全价票" << endl; }
};

虚函数重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即两个函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

  • 参数列表完全相同:只要参数的类型相同即可!

注意引言中的例子,若子类不加virtual,也可以构成重写(因为子类在继承基类的虚函数之后,已久保持虚函数的属性),编译器仍然认为构成重写。实际上这样写并不规范,还是建议显式声明一下。

//仍然构成重写
class Person
{
    
    
public:
	virtual void BuyTicket() {
    
     cout << "全价票" << endl; }
};

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

虚函数重写的两个例外

  1. 协变(基类与派生类返回值类型不同)
    只能是基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用。
    返回值可以不同,必须是父子关系指针或者引用(可以是其他父子的指针或引用)
    如下例子:
class Person
{
    
    
public:
	virtual Person* BuyTicket() {
    
     cout << "全价票" << endl; return this; }
};

class Student : public Person
{
    
    
public:
	virtual Student* BuyTicket() {
    
     cout << "半价票" << endl; return this; }
};

如何有需求其他父子的指针或引用,也可以:

class A
{
    
    };
class B :public A
{
    
    };


class Person
{
    
    
public:
	virtual A* BuyTicket() {
    
     cout << "全价票" << endl; return new A; }
};

class Student : public Person
{
    
    
public:
	virtual B* BuyTicket() {
    
     cout << "半价票" << endl; return new B; }
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都构成重写。因为编译器会对析构函数名称做特殊处理,编译后析构函数的名称统一处理成destructor
class Person
{
    
    
public:
	virtual ~Person() {
    
     cout << "~Person()" << endl; }
};

class Student : public Person
{
    
    
public:
	~Student() {
    
     cout << "~Student()" << endl; }
};

在这里插入图片描述

如果不写virtual,就不构成多态,就是什么类型调用谁的成员函数。
在这里插入图片描述

c++11的override和final

可以感受到,c++对函数重写的要求比较严格,为了防止出错c++提供了两个关键字来帮助程序员更好完成任务。

  1. final:修饰虚函数,表示该虚函数不能再被重写。
class Person
{
    
    
public:
	virtual void BuyTicket() final {
    
     cout << "全价票" << endl; }
};

class Student : public Person
{
    
    
public:
	void BuyTicket() {
    
     cout << "半价票" << endl; }   //报错,因为父类的函数使用final修饰,不能再重写。
};
  1. override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则编译报错。
class Person
{
    
    
public:
	virtual void BuyTicket() {
    
     cout << "全价票" << endl; }
};

class Student : public Person
{
    
    
public:
	void BuyTicket(int a=0) override {
    
     cout << "半价票" << endl; }   //报错,因为子类没有完成重写!
};

重载、覆盖(重写)、隐藏(重定义的对比)

在这里插入图片描述

抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

  • 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。体现了接口继承。
class Car
{
    
    
public:
	virtual void Drive() = 0;
};

class BWM : public Car
{
    
    
	int a = 0;   
};

int main()
{
    
    
	BWM a;  // 报错,因为没有重写纯虚函数
	return 0;
}

修改成以下形式即可:

class Car
{
    
    
public:
	virtual void Drive() = 0;
};

class BWM : public Car
{
    
    
	virtual void Drive(){
    
    }
	int a = 0;
};

接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口继承,派生类继承的是纯虚函数的接口,目的是为了重写,达成多态。

如果不实现多态,不要把函数定义成虚函数。

多态底层原理

先看一道题:sizeof(A)是多少?

class A
{
    
    
public:
	virtual void Func() {
    
     cout << "Func" << endl; }

protected:
	char _str;
	int _a;
};

int main()
{
    
    

	cout << sizeof A << endl;   //12
	return 0;
}

我们观察下面这段代码:

int main()
{
    
    
	A a;
	cout << sizeof A << endl;
	return 0;
}

在这里插入图片描述

对象a里面还存着一个指针(*vftable),该指针指向虚函数表。

虚函数表

一个含有虚函数的类中,至少有一个虚函数表指针,因为虚函数的地址要放在虚表中。我们再定义几个函数来看看。

class A
{
    
    
public:
	virtual void Func() {
    
     cout << "Func" << endl; }
	virtual void Func1() {
    
     cout << "Func1" << endl; }
	virtual void Func2() {
    
     cout << "Func2" << endl; }
	virtual void Func3() {
    
     cout << "Func3" << endl; }

protected:
	char _str;
	int _a;
};

在这里插入图片描述
可知虚函数表中又多了几个虚函数的地址。


继承下的虚函数表的结构,如下代码:

class A
{
    
    
public:
	virtual void Func1() {
    
     cout << "Func1" << endl; }
	virtual void Func2() {
    
     cout << "Func2" << endl; }
	void Fun3() {
    
     cout << "Func3()" << endl; }

protected:
	char _str;
	int _a;
};

class B :public A
{
    
    
public:
	virtual void Func1() {
    
     cout << "*******B" << endl; }
private:
	int _b;
};

在这里插入图片描述

观察监视图可得到如下结果:

  • 派生类对象b由两部分组成,一部分是基类A,一部分是自己的成员。其中A类中也包含着一个虚基表指针。
  • 子类重写了Func1函数,虚表里面Func1的地址是不一样的(两个函数,完成了重写)。相当于重写就是将本来Func1的地址,覆盖为修改后的地址。
  • Func2继承下来由于是虚函数,也被放进了虚表中。Func3也被继承下来了,但是Func3不是虚函数,所以没有放进虚表中。

总结一下派生类虚表形成过程:(是在派生类对象构造函数中初始化列表形成的)

  1. 先将基类中的虚表内容拷贝一份到派生类的虚表中
  2. 如果派生类重写了某个虚函数,就把虚表中相应位置覆盖成重写后的地址。
  3. 派生类自己新增加的虚函数按其在派生类的声明次序增加到派生类的虚表最后。

易错问题:

虚表指针存在哪?虚函数存在哪?虚表存在哪?
虚表指针存在对象中,虚函数存在代码段(常量区),虚表也存在代码段(常量区)。
解释:因为虚表在编译期就已经生成了,在构造函数的初始化列表初始化了。它也是不变的,因此存在代码段比较合适。

下面的程序印证了虚函数表在代码段。

int main()
{
    
    
	A aa;   
	B bb;
	int a = 0;   //栈区
	int b = 0;

	static int sta_a = 1;   // 数据段(静态区)
	static int sta_b = 1;

	const char* c= "cccccc";   // 代码段(常量区)
	const char* d = "ddddd";
	
	int* p1 = new int;     //堆区
	int* p2 = new int;

	printf("  栈区地址%p\n", &a);
	printf("  栈区地址%p\n", &b);
	printf("静态区地址%p\n", &sta_a);
	printf("静态区地址%p\n", &sta_b);
	printf("常量区地址%p\n", c);
	printf("常量区地址%p\n", d);
	printf("  堆区地址%p\n", p1);
	printf("  堆区地址%p\n", p2);
	printf("aa虚表地址%p\n", *((int*)&aa));
	printf("bb虚表地址%p\n", *((int*)&bb));
	return 0;
}

在这里插入图片描述
下面两张图证明了子类的虚表是在初始化列表中进行。再Debug模式下,按了一下F11.
在这里插入图片描述在这里插入图片描述

多态原理

我们看到的是学生买半价票,普通人买全价票。那么底层到底是怎么调用的呢?有了上面的知识,这个问题就轻而易举了!

下面看汇编代码:

在这里插入图片描述
四个框框:

  1. 将p内容取出来放到eax中。
  2. 将eax内容前4字节取出来,放在edx中。(找到了虚表指针)
  3. 将edx内容的前四字节取出来,放到eax中。(找到了虚函数地址)
  4. call 该地址。

这就是多态的原理,通过虚函数表来找对应函数。


当不构成多态时,普通对象调用函数,根据类型去调用。这种情况函数的地址其实在编译时已经从符号表确认了函数的地址,调用时直接call就好了。

在这里插入图片描述

动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为 ,也称为静态多态。
    比如函数重载。

  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体函数,也成为动态多态。

打印虚函数表

最后将虚函数表打印出来看一看,表里面到底是什么。

单继承下的虚函数表

//A有虚函数Func1/Func2和自己的函数Func3
//B重写了Func1,并且自己定义了新的虚函数Func4
class A
{
    
    
public:
	virtual void Func1() {
    
     cout << "A::Func1" << endl; }
	virtual void Func2() {
    
     cout << "A::Func2" << endl; }
	void Func3() {
    
     cout << "A::Func3()" << endl; }

protected:
	char _str='x';
	int _a=0;
};

class B :public A
{
    
    
public:
	B()
		:_b(0)
	{
    
    }
	virtual void Func1() {
    
     cout << "B::Func1" << endl; }
	virtual void Func4() {
    
     cout << "B::Func4" << endl; }
private:
	int _b;
};

定义打印的虚函数:

//将函数指针重定义为 VFPTR
typedef void(*VFTPTR)();

//得传进来虚函数的指针
void PrintVFTable(VFTPTR* table)
{
    
    
	for (int i = 0; table[i] != nullptr; i++)
	{
    
    
		printf("[%d]:%p->", i, table[i]);
		VFTPTR func = table[i];
		func();

	}
	cout << endl;
}


int main()
{
    
    

	A a;
	B b;
	//1.先取a的地址,强转成int*型(为了得到前4B的地址)
	//2.得到前4B的地址后解引用,获得虚表指针
	//3.因为虚表指针的类型是VFTPTR*,所以要强转一下,就可以了
	PrintVFTable((VFTPTR*)(*((int*)&a)));     

	//因为虚表的内容就是VFTPTR,并且虚表也是一个数组。
	//因此指向虚表的指针就是一个2级指针。
	PrintVFTable(*(VFTPTR**)&b);
	return 0;
}

测试结果可知:
在这里插入图片描述
结论:

  1. 不是虚函数不进虚函数表。
  2. 继承下来的虚函数,如重写则覆盖,未重写,继承父类虚函数的地址。
  3. 子类新增的虚函数,从虚表的末尾开始增加,按声明顺序增加。

多继承下的虚函数表


//类A.B都有虚函数Func1/Func2
//类C继承AB,并且重写Func1.
//类C有两个虚函数表,分别是继承AB得来的。
class A
{
    
    
public:
	virtual void Func1() {
    
     cout << "A::Func1" << endl; }
	virtual void Func2() {
    
     cout << "A::Func2" << endl; }

protected:
	char _str = 'x';
	int _a = 0;
};

class B
{
    
    
public:
	B()
		:_b(0)
	{
    
    }
	virtual void Func1() {
    
     cout << "B::Func1" << endl; }
	virtual void Func2() {
    
     cout << "B::Func2" << endl; }
private:
	int _b;
};

class C :public A,public B
{
    
    
public:
	virtual void Func1() {
    
     cout << "C::Func1" << endl; }
	virtual void Func3() {
    
     cout << "C::Func3" << endl; }

};

在这里插入图片描述
观察后发现,先声明的A表为主表,因为自己写的Func3,进入到了A表中。B表修改只跟是否重写有关。

总结

多态的知识大而庞杂,关于菱形继承的多态概念本篇没有介绍,若想了解可以参考下面两篇文章。

c++虚函数表解析
c++对象内存布局

猜你喜欢

转载自blog.csdn.net/weixin_45153969/article/details/134892492