C++中的多态

一、概念

多态为C++中面向对象三大特征之一,通俗地讲,就是多种形态。作用为完成某个行为,当不同(具有继承关系)的对象去完成时会产生不同的状态。

二、虚函数

1、概念

在基类中被virtual修饰并且在派生类中重新定义的类成员函数。

2、重写(覆盖)

  • 派生类中有一个跟基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名字和参数列表完全相同,称派生类的虚函数重写(覆盖)了基类的虚函数。
  • 在重写基类虚函数时,派生类的虚函数不加virtual关键字修饰虽然也可以构成重写。因为继承结束后,基类的虚函数被继承下来了,在派生类中依旧保持虚函数的属性。但是该种写法不是很规范,一般还是会加上virtual关键字进行修饰。

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

(1)重载

  • 两个函数在同一作用域。
  • 函数名相同,参数(类型或者数量)不同。

(2)重写(覆盖)

  • 两个函数分别在基类和派生类的作用域中。
  • 函数名、参数、返回值都必须相同(协变除外)。
  • 两个函数都必须是虚函数。

(3)隐藏(重定义)

  • 两个函数分别在基类和派生类的作用域中。
  • 函数名相同。
  • 两个基类和派生类的同名函数不构成重写就是重定义。

三、多态的定义与使用

1、多态的构成条件

  • 派生类必须继承基类。
  • 必须通过基类的指针或者引用调用虚函数。
  • 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写(覆盖)。

2、示例代码

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

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

//void Func(Base* b)
void Func(Base& b)
{
	//b->Print();
	b.Print();
}

int main()
{
	Base b;
	Derive d;
	Func(b);
	Func(d);
	
	/*Func(&b);
	Func(&d);*/
	return 0;
}

3、运行结果

在这里插入图片描述

四、虚函数重写的两个例外

1、协变

(1)概念

当派生类重写基类虚函数时,重写的虚函数与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用。返回类型除了基类和派生类(或具有其他继承关系的基类和派生类)的指针或者引用,其他的类型都不构成协变。

(2)示例代码

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

class Base
{
public:
	virtual A* Func1()
	{
		return new A;
	}

	virtual Base* Func2()
	{
		return new Base;
	}
};

class Derive :public Base
{
	virtual B* Func1()
	{
		return new B;
	}

	virtual Derive* Func2()
	{
		return new Derive;
	}
};

2、析构函数的重写

(1)概念

  • 如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
  • 虽然基类与派生类的析构函数名字不同。但可以理解为编译器对它们的析构函数名称做了特殊处理,编译后它们的析构函数名称统一处理成destructor。
  • 当基类指针指向派生类时,只有派生类Student的析构函数重写了Person的析构函数,delete该基类指针时,调用析构函数,才能构成多态。这样才能保证指针指向的对象正确的调用析构函数,否则它将调用基类的析构函数而不是调用派生类的析构函数。

(2)示例代码

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

class Derive :public Base
{
public:
	virtual ~Derive()
	{
		cout << "~Derive()" << endl;
	}
};

int main()
{
	Base* p1 = new Base;
	Base* p2 = new Derive;
	
	delete p1;
	delete p2;

	return 0;
}

(3)运行结果

在这里插入图片描述

五、override和final

1、override

(1)作用

检查用override修饰的派生类虚函数是否重写了基类的某个虚函数,如果没有重写则报错。

(2)代码

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

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

(3)编译器报错

在这里插入图片描述

2、final

(1)作用

修饰虚函数,表明该虚函数不能被重写。

(2)代码

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

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

(3)编译器报错

在这里插入图片描述

六、抽象类

1、概念

  • 在虚函数的后面写上 =0 ,则这个虚函数称为纯虚函数。包含纯虚函数的类称为抽象类,也称为接口类。
  • 抽象类不能实例化出对象,派生类继承抽象类后也不能实例化出对象。只有派生类重写纯虚函数,派生类才能实例化出对象。
  • 纯虚函数规范了派生类必须重写基类的纯虚函数,体现了接口继承。

2、接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用基类的函数,继承的是函数的实现。
  • 纯虚函数的继承是一种接口继承,派生类继承的是基类该纯虚函数的接口,目的是为了重写它,达成多态。所以,如果不实现多态,不要把函数定义成纯虚函数。

3、代码

class Base
{
public:
	virtual void Func() = 0;
};

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

int main()
{
	//Base b;
	Derive d;
	d.Func();
	return 0;
}

4、运行结果

在这里插入图片描述

5、错误代码

class Base
{
public:
	virtual void Func() = 0;
};

class Derive :public Base
{
public:
	/*virtual void Func()
	{
		cout << "Derive::Func()" << endl;
	}*/
};

int main()
{
	Base b;
	Derive d;
	d.Func();
	return 0;
}

6、编译器报错

在这里插入图片描述

七、虚函数表

1、代码

class Base
{
	virtual void Func1()
	{
		cout << "Base::virtual void Func1()" << endl;
	}
	/*virtual void Func2()
	{
		cout << "Base::virtual void Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::void Func3()" << endl;
	}*/
private:
	int _b = 1;
};

//class Derive :public Base
//{
//	virtual void Func1()
//	{
//		cout << "Derive::virtual void Func1()" << endl;
//	}
//private:
//	int _d = 2;
//};

int main()
{
	Base b;
	cout << sizeof(b) << endl;
	//Derive d;
	return 0;
}

2、运行结果与调试窗口查看

在这里插入图片描述
在这里插入图片描述

3、说明

  • 在上方代码中,用sizeof函数计算具有虚函数的类时,得出的结果是16bytes(64位平台下的VS编译器)。
  • 在调式窗口下,对象b中除了_b成员,还有一个__vfptr放在对象的前面(__vfptr在对象中的位置与编译器有关,此处为VS编译器下的情况)。
  • 对象中的__vfptr指针称为虚函数表指针。其中,v代表virtual,f代表function。
  • 一个含有虚函数的类中至少都有一个虚函数表指针(简称为虚表指针),因为虚函数的地址要被放到虚函数表中,虚函数表简称为虚表,虚表指针指向虚表。

4、将代码中的注释部分展开后的调试窗口查看

在这里插入图片描述

5、说明

  • 派生类对象由两部分构成,一部分是从基类继承下来的成员,另一部分是自己的成员。派生类对象也有一个虚表指针。
  • 基类b对象和派生类d对象的虚表是不一样的。在派生类中,Func1完成了重写,所以d的虚表中存的是派生类重写的Derive::Func1,即派生类的Func1函数覆盖了基类的Func1函数。所以虚函数的重写(语法层次)也称为覆盖(原理层次)。
  • 因为基类中Func2函数被派生类继承下来后是虚函数,所以它会被放进派生类的虚表中。但因为派生类没有对它进行重写,所以Func2函数在该虚表中和在基类中的一致。
  • 函数Func3也会被继承下来,但因为它不是虚函数,所以不会放进派生类的虚表中。
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况下,这个指针数组最后面放了一个nullptr。

6、派生类虚表的生成

  • 先将基类中的虚表内容拷贝到派生类虚表中,如果派生类重写了基类中的某个虚函数,就用派生类重写的虚函数覆盖虚表中基类的虚函数。
  • 派生类新增加的虚函数按其在派生类中的声明次序,依次增加到派生类虚表的最后面。

7、虚函数与虚表

  • 因为虚表中存的是指向虚函数的指针,而不是虚函数。所以,虚函数和普通函数一样,都存在代码段中。
  • 对象中存的不是虚表,而是虚表指针。在VS编译器下,虚表存在代码段中。

八、动态绑定与静态绑定

  • 静态绑定又称为前期绑定(早绑定),指在程序编译期间就确定了程序的行为,也称为静态多态,如函数重载。
  • 动态绑定又称后期绑定(晚绑定),指在程序运行期间,根据具体拿到的类型确定程序的具体行为,进而调用具体的函数,也称为动态多态。

本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请务必点赞、收藏加关注💕💕💕

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值