C++学习笔记——多态

前面说到了继承,那么就衍生出了多态。

目录

概念

实现

1、前提

2、虚函数

3、重写

4、示例

4、抽象类

原理

1、虚表指针

2、虚表

多继承和单继承的虚函数表

1、多继承的虚表指针

2、菱形继承

彩蛋


概念

多态的概念用举例子的方式就比较好理解了。

例子①:买票,对于买票这同一件事情,但是却有成人票、学生票、儿童票等。这是多态。

例子②:变形,对于变形金刚变形,大黄蜂和擎天柱等变形后的形态不一样,这也是多态。

例子③:书写,每个人对书写这件事情,有的苍劲有力,有的龙飞凤舞,这也是多态。

总结一下,多态就是对同一个事件或者行为,不同的对象就会产生不同的现象。在c++里,事件或者行为就是函数,不同的现象就是函数执行后的结果。不同的对象对象调用相同的函数会有不同结果。就是多态了。说到这儿可以联想到前面的类型萃取。

实现

  • 1、前提

构成多态需要有两个条件。

  1. 函数需要是虚函数
  2. 调用的对象需要是指针或者引用
  • 2、虚函数

虚就是虚拟的意思,如同前面的虚拟继承一样。所以加的关键字也是一样的——virtual。但是两者的意义是不一样的。

  • 3、重写

说到重写,又叫覆盖。由于前面的隐藏,基类的成员函数还存在,这里用到覆盖了,当定义了和父类同名的函数时,我们将其重写,这个时候,基类的函数和派生类的函数就是一个函数了。想到这,说一下前面的重载,重定义(隐藏)了,毕竟又有一个概念产生。所以这里要对比记忆一下,防止混淆。

  1. 重载:相同的作用域,函数名相同,但参数列表不同
  2. 重定义:在不同的作用域(继承),函数名相同,就构成了隐藏,即重定义
  3. 重写:基类的函数是虚函数的时候,派生类重新定义。

注意:

  1. 当基类定义成虚函数是,派生类继承过来也是虚函数,即使派生类不写virtual,编译器也会自动加上,它们现在共享这个函数。
  2. 协变:返回值的类型构成符字关系,并且是指针或者引用(了解)。
  • 4、示例

//头文件Polymorphism.h
#pragma once
#include<iostream>
using namespace std;

class A
{
public:
	A(int a = int())
		:a_(a)
	{
		cout << "A()" << endl;
	}
	void func()
	{
		cout << "A的成员函数func" << endl;
	}
	virtual void func1()
	{
		cout << "A的虚函数" << endl;
	}
private:
	int a_;
};
class	B :public A
{
public:
	B(int b = int())
		:b_(b)
	{
		cout << "B()" << endl;
	}
	void func()
	{
		cout << "B的成员函数func" << endl;
	}
	virtual void func1()
	{
		cout << "B的虚函数" << endl;
	}
private:
	int b_;
};
void Poly(A& a)
{
	a.func1();
}

 

注意:一定要是引用或者指针,在上面的代码中,poly就是引用传参。不能传值,

  • 4、抽象类

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

class Person
{
public:
	Person(const string name = "张三")
		:name_(name)
	{}
	virtual void Print() = 0;
private:
	string name_;
};
class Student : public Person
{
public:
	Student(const int& stunum = 0)
		:Person()
		,stunum_(stunum)
	{}
private:
	int stunum_;
};

这个时候子类重写虚函数

class Student : public Person
{
public:
	Student(const int& stunum = 0)
		:Person()
		,stunum_(stunum)
	{}
	virtual void Print()
	{
		cout << "Student::Print()" << endl;
	}
private:
	int stunum_;
};

父类依旧不能创建对象,但是子类却可以。

原理

  • 1、虚表指针

首先来看一个类,我们知道空类的大小,我们是给了一个字节类标识这个类的。那么下面的类是多大呢?

class Car
{
public:
//为了方便测试,先注释掉一些东西。
	Car(/*const string& band = "五菱宏光"*/)
		//:band_(band)
	{}
	virtual void Drive()
	{
		cout << "秋名山车王" << endl;
	}
//private:
//	string band_;
};
//该类是几个字节的大小?
//测试一下就知道了。
void test4()
{
	cout << "Car类的大小是:" << sizeof(Car) << "个字节" << endl;
}
int main()
{
	test4();
	system("pause");
	return 0;
}

 

下面来看看具体是怎么回事。

我创建了一个car对象,发现对象模型当中并不是空的,而是有一个_vfptr。这就是这节的主人公——虚表指针(v代表virtual,f代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中, 虚函数表也简称虚表,虚表指针就是指向虚函数表的。下面来看一下在继承中,虚标指针的作用。

class Car
{
public:
	Car(const string& band = "五菱宏光")
		:band_(band)
	{}
	virtual void Drive()
	{
		cout << "秋名山车王" << endl;
	}
private:
	string band_;
};

class BWM : public Car
{
public:
	BWM(const string& band = "宝马")
		:Car(band)
	{}
	virtual void Drive()
	{
		cout << "驾驶之王" << endl;
	}
};

发现派生类的_vfptr是来自基类的_vfptr。但是两个地址是不一样的。其实这里是发生了拷贝,派生类把基类的虚表指针和虚表都拷贝了一份,当派生类把继承来的虚函数重写了之后,就用重写后的虚函数的地址把原先的虚函数的地址覆盖掉。所以重写又叫覆盖。下面来说一下几个概念。

虚函数表:存放的是虚函数地址,它是一个指针数组

虚表指针:指向虚函数表的指针。只要有虚函数,就会有虚表指针。

  • 2、虚表

上面说虚函数表是一个指针数组存放的虚函数的地址,那么如果派生类有自己的虚函数,或者继承的普通函数怎么存储的呢?

class Car
{
public:
	Car(const string& band = "五菱宏光")
		:band_(band)
	{}
	virtual void Drive()
	{
		cout << "秋名山车王" << endl;
	}
	virtual void Func()
	{
		cout << "我是不想重写的虚函数" << endl;
	}
	void Func1()
	{
		cout << "我是基类的普通函数" << endl;
	}
private:
	string band_;
};

class BWM : public Car
{
public:
	BWM(const string& band = "宝马")
		:Car(band)
	{}
	virtual void Drive()
	{
		cout << "驾驶之王" << endl;
	}
	virtual void Print()
	{
		cout << "大家好,我是宝马" << endl;
	}
};

虚表生成:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中
  2. 如果派生类重写了基 类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后,这里没有显示而已,是编译器的监视窗口故意隐藏了这两个函数,后面用代码的形式展示。

虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。我们发现基类的普通函数是没有放进虚表的,

所以不是虚函数,所以不会放进虚表。最后再强调一下:

虚表存的是虚函数指 针,不是虚函数,虚函数和普通函数一样的,存在代码段的,只是他的指针又存到了虚表中,对象中存的不是虚表,存的是虚表指针

多继承和单继承的虚函数表

  • 1、多继承的虚表指针

上面说,只要有虚函数,那么就至少有一个虚表指针,所以如果该类继承了多个类,就会有多个虚表指针。

class Base1
{
public:
		Base1(const int& b):b1(b){}
		virtual void Func1()
		{
			cout << "我是Base1的虚函数" << endl;
		}
private:
	int b1;
};
class Base2
{
public:
	Base2(const int& b) :b2(b) {}
	virtual void Func2()
	{
		cout << "我是Base2的虚函数" << endl;
	}
private:
	int b2;
};

class Child : public Base1, public Base2
{
public:
	Child()
		:Base1(1)
		, Base2(2)
		,c_(3)
	{}
	virtual void Func2()
	{
		cout << "我是重写Base2的虚函数" << endl;
	}
	virtual void Func3()
	{
		cout << "我是派生类的虚函数" << endl;
	}
private:
	int c_;
};

可以看到上面派生类是有两个虚表指针的。在继承中,我们知道派生类的对象的对象模型是基类的成员在前面,自己的成员在后面,如果有多个继承的基类,会按照继承的顺序来存放。

如果是派生类自己虚函数应该刚在哪呢?同构一段测试代码来看一下

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		cout << " 第" << i << "个虚函数地址 :" << vTable[i];
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
void test5()
{
	Base1 b1(1);
	Base2 b2(2);
	Child c;
	// 思路:取出c对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的
	//指针数组,这个数组最后面放了一个nullptr
	// 1.先取b的地址,强转成一个int*的指针
	// 2.再解引用取值,就取到了c对象头4bytes的值,这个值就是指向虚表的指针
	// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
	// 4.虚表指针传递给PrintVTable进行打印虚表
	VFPTR* vTableb = (VFPTR*)(*(int*)&c);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)((char*)&c+ sizeof(b1)));
	PrintVTable(vTabled);

}

通过上面的测试可以发现,我们上面画的对象模型是没有错的。而且验证了派生类自己虚函数是放在第一个虚表指针指针的虚表的。

  • 2、菱形继承

下面我们再来说一下菱形继承。

前面说菱形继承是一个大坑,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗,现在看到虚函数表又是这么复杂。

前面说这里虚函数的virtual和虚拟继承的virtual是有区别的,区别是啥?

虚函数virtual是把基类的虚表,虚表指针都拷贝了一份,重写了就覆盖掉复制的虚表里面的函数地址。

而虚拟继承,是把有二义性的数据弄成共享的,可以通过VS的监视发现数据的变化。

现在就变成了

彩蛋

彩蛋其实就是三个问题:

1、静态成员可以是虚函数吗?

2、构造函数可以是虚函数吗?

3、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答案请见下一篇博文——智能指针

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值