C++多态 及底层如何实现

一,多态

多态是c++面向对象三大特性之一,关于什么是多态,我们需要先来了解一下虚函数和重写。

1.虚函数

在类的成员函数前面加上virtual关键字,就构成了虚函数。

2.虚函数的重写

当在子类定义一个与父类完全相同的虚函数时,我们就称,子类的虚函数重写(覆盖)了父类的虚函数。

重写(也称覆盖),想要构成重写,需要如下几个条件:

  1. 不在同一个作用域(分别在父类和子类)
  2. 函数名相同,参数相同,返回值相同(协变除外)
  3. 基类函数必须带有virtual关键字
  4. 访问限定符不限

重写是专门为多态而生的,至于虚函数重写能够实现什么功能,我们在第三点多态的简单举例中详细明。

3.多态的简单举例

下面是实现多态的例子:

#include<iostream>
#include<stdlib.h>
using namespace std;

class Person//父类
{
public:
	virtual Person& BuyTicket()
	{
		cout << "买票全价" << endl;

		return *this;
	}

	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person//子类
{
public:
	virtual Student& BuyTicket() //重写父类的虚函数
	{
		cout << "买票半价" << endl;

		return *this;
	}

	 ~Student()
	{
		 // free
		cout << "~Student()" << endl;
	}
};

void Func(Person& p)
{
	// 多态调用:对象有关,指向哪个对象,调用就是谁的虚函数
	// 1.对象父类指针/引用
	// 2.重写的虚函数
	p.BuyTicket();
}


int main()
{
	Person p;
	Student s;

	// 普通调用:跟类型有关
	p.BuyTicket();
	s.BuyTicket();
	//多态调用:跟传入对象有关
	Func(p);
	Func(s);
	system("pause");
	return 0;
}

在上面的代码中,子类student继承了父类people,且父子类中都有虚函数BuyTicket,构成虚函数重载。又在类外定义了一个函数Func,在里面同样调用BuyTicket。
让我们运行一下:
在这里插入图片描述
我们发现,前两句输出来此与对象p和对象s的成员函数调用,后两次输出来自func函数。
这里就需要重点注意一下func函数了,func函数的参数是一个父类对象的引用,但是如果给他传一个子类对象,他会调用子类中的BuyTicket函数,输出“买票半价”,这就是多态,“一类代码,多种形态”。

当使用基类的指针或引用调用重写的虚函数时,使用父类对象调用调的就是父类的虚函数,子类对象调用的就是子类的虚函数。

4.构成多态的充分条件

1.函数的参数一定是父类的指针或引用
2.子类一定要重写父类的虚函数

这两个条件缺一不可,少任意一个都不能构成多态。

5.探索多态的底层—虚表

5.1什么是虚表

虚函数表是通过一块连续内存来存储虚函数的地址,这张表解决了继承,虚函数(重写)的问题,在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数。

#include<iostream>
#include<stdlib.h>
using namespace std;

class Person//父类
{
public:
	virtual Person& BuyTicket()
	{
		cout << "买票全价" << endl;

		return *this;
	}

	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person//子类
{
public:
	virtual Student& BuyTicket() //重写父类的虚函数
	{
		cout << "买票半价" << endl;

		return *this;
	}

	 ~Student()
	{
		 // free
		cout << "~Student()" << endl;
	}
};

void Func(Person& p)
{
	// 多态调用:对象有关,指向哪个对象,调用就是谁的虚函数
	// 1.对象父类指针/引用
	// 2.重写的虚函数
	p.BuyTicket();
}


int main()
{
	Person p;
	Student s;

	// 普通调用:跟类型有关
	p.BuyTicket();
	s.BuyTicket();
	//多态调用:跟传入对象有关
	Func(p);
	Func(s);
	system("pause");
	return 0;
}

为了更方便观察虚表,在上面的代码中,我向父类person中多定义一个虚函数vfunc,该函数没有进行任何操作。
在这里插入图片描述

在父类person和子类student中,都有一个变量_vfptr存储在头部,这是一个数组指针(虚表指针),所指的数组,就是我们说的虚表。

由上面的图片可以看出,虚表中依次存储了各个虚函数的地址,且存放的顺序和代码中定义的虚函数的顺序一致。当进行多态调用时,编译器根据传入对象的类别,找到对应的vfptr(虚表指针),再查看你要调用的函数在类中定义的位置,来找到该虚函数在虚表中存储的位置,实现调用。

5.2虚表(重点!)

在这里插入图片描述
为了更加深入的认识虚表,我又多创建了一个对象s1,它和s都是student类的实例化。
我们根据上图可以看到:

  1. s和s1都继承了p,但他两个的vfptr与p的vfptr值不同,所以得出结论:编译器会根据类的不同而分别为2种类各自构建一个虚表。即虚表是从属于类的!

  2. 而同一类的s和s1,他们的vfptr值相同,且对应虚函数的地址也相同,所以得出结论:虚表指针是从属于对象的。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。

  3. 再看p和s,s继承p,s中重写了虚函数Buyticket,而不重写虚函数vfunc,在监视中,我们发现:s类中的虚函数Buyticket地址与p中的不同,而虚函数vfunc地址与p中的相同,所以得出结论:当子类继承父类的虚函数,不进行重写的话,虚函数地址不变,而如果重写(覆盖),那么就会改变对应虚函数的地址,分配一块新内存存储重写后的虚函数,这很像写时拷贝。这也说明了为什么重写又叫覆盖(新的虚函数地址覆盖了虚表中原有的父类虚函数地址)。

5.3虚表存储在什么地方?

虚表在编译时就开始创建,构造函数实际是初始化_vfptr指向的位置。也就是说虚表不可能放在堆,栈中的。虚表存放在代码段和数据段都可以,都有可能。
一般虚表放在静态区(数据段中)。

关于多态里面一些需要注意的地方(次重点!)

  1. 子类重写父类的虚函数实现多态,要求函数名,参数列表,返回值完全相同。(协变除外)
  2. 父类中定义了虚函数,在子类中该虚函数始终保持虚函数的特性。
  3. 只有类的成员函数才能被定义为虚函数。
  4. 各种类型的函数能否被定义成虚函数:
    4.1 静态成员函数:不能!
    静态函数不需要创建对象就可以调用,而虚表要进行初始化后,虚函数才能调用,所以没有必要;静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别。
    4.2 构造函数:不能!
    上面提到过,虚表是在构造函数中进行初始化的,你把构造函数弄成虚函数,此时虚表里还什么东西都没有呢,怎么找到他的地址呢,故不能定义成虚函数。
    4.3 赋值运算符重载函数 :不能!
    赋值运算符重载operator=虽然在定义上可以设为虚函数,但是最好不要这么做,容易在使用时引起混淆。
    4.4内联函数 :不能!
    我们知道内联函数的优势是在指定位置直接展开代码,省去了创建栈帧的开销,但这也同时使得内联函数没有地址,无法放进虚标内。
    4.5析构函数:能!
    最好把父类的析构函数声明为虚函数,子类析构和父类析构尽管不同名,但是在编译后实际会改成一个名为destructor的函数,构成同名函数。
    因为在某些情况下,如果不定义成虚函数,可能会发生错误,比如:
Student s;
Person *p=s;//父类指针可以指向子类对象,子类对象发生切片行为
delete p;//此时对p调用析构函数,如果析构函数不是虚函数,则调用的是父类的析构,没有释放全部空间
			//倘若定义成虚函数,则调用的是子类的析构函数,空间全部释放
  1. 如果在类外定义虚函数,只能在声明函数时加上virtual,类外定义函数时不能加virtual。
  2. 不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。

7.协变

上面谈到重写的条件时说到,必须函数名,参数列表,返回值都一样才能构成重写,但是协变除外。

协变是一种特殊的重写方式,他可以允许返回值不同,但是返回的一定要是父类的指针或引用。

8.纯虚函数和抽象类

在成员函数的形参后面写上=0,则成员函数为纯虚函数,包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化对象,纯虚函数在子类中重新定义后,子类才能实例化出对象。

class Person
{
	virtual void Display() = 0;//纯虚函数

protected:
	string _name;
};

class :Student : public Person//抽象类会强制让子类重写他的纯虚函数,否则子类就无法创建
{};
  • 14
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值