学习博客:
目录
多态
- 多态是面向对象的三大特征之一,其它两大特征分别是 封装 和 继承。
- 面向对象的三大特征:封装、多态、继承
- 所谓 多态,简单来说,就是当发出一条命令时,不同的对象接收到同样的命令后所做出的动作是不同的。
书本上的定义:指相同对象收到不同消息或不同对象收到系统消息时产生不同的动作。
- 分为动态多态和静态多态:
静态多态
静态多态,也叫 早绑定
看如下实例:定义一个矩形类:Rect,其中有两个同名成员函数:calcArea(),显然
二者互为重载(名字相同,参数可辨)
在使用时:当实例化一个 Rect 的对象后,就可以通过对象分别调用这两个函数,
计算机在编译时,就会自动调用对应的函数
即程序运行之前,在编译阶段就已经确定下来到底要使用哪个函数,可见:很早就已经将函数编译进去了,称这种情况为早绑定或静态多态
动态多态
动态多态,也叫晚绑定。看如下实例:
当前要计算面积,于是分别给圆形和矩形下达计算面积的指令,作为圆形来说,它有自己计算面积的方法,作为矩形来说,它也有自己计算面积的方法。
显然,两种计算面积的方法肯定不同,即对不同对象下达相同指令,却做着不同的操作,称之为晚绑定或动态多态
动态多态是有前提的,它必须以封装和继承为基础。在封装中,将所有数据封装到类中,在继承中,又将封装着的各个类使其形成继承关系。
只有以封装和继承为基础,才能谈到动态多态,动态多态最起码有两个类,一个子类,一个父类,只有使用三个类时,动态多态才表现的更为明显。
看如下实例:定义一个形状类:Shape,它有一个计算面积的成员函数:calcArea()
再定义一个圆类:Circle,它公有继承了形状类 Shape,并有自己的构造函数和计算面积的函数。
圆类计算面积的成员函数在实现时:3.14 * m_dR * m_dR
再定义一个矩形类:Rect,它公有继承了形状类 Shape,并有自己的构造函数和计算面积的函数。
矩形类计算面积的成员函数在实现时:m_dWidth * m_dHeight。
在使用时:可以使用父类指针指向子类对象,但结果却不尽如人意,因为调用到的都是父类的计算面积的函数,即会打印出两行calcArea。
如果想要实现动态多态,就必须使用 虚函数。
虚函数
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本。
- 用virtual 修饰成员函数,使其成为 虚函数
- 如下:在父类中,把想要实现多态的成员函数前加上virtual 关键字,使其成为虚函数。
在定义子类Circle 时,给计算面积的同名函数也加上virtual 关键字:
这里的virtual 关键字不是必须的,如果不加,系统会自动为你加上,而加上了,会在后续的使用中看的更加明显,推荐在子类的定义中也加上virtual 关键字。
同理,定义子类Rect 时,给计算面积的同名函数也加上virtual 关键字:
最后,在使用时:
使用父类指针指向子类对象,调用函数时,调用的就是对应子类的计算面积函数。
#include <iostream>
class Person
{
public:
virtual void BuyTicket(int)
{
std::cout << "Adult need Full Fare!" << std::endl;
}
};
class Child : public Person
{
public:
virtual void BuyTicket(int)
{
std::cout << "Child Free!" << std::endl;
}
};
void fun(Person& obj)
{
obj.BuyTicket(1);
}
int main(void)
{
Person p;
Child c;
fun(p);
fun(c);
return 0;
}
- 调用函数就是这里的fun,参数int没有实际意义,就是为了体现函数重写必须要返回值一样、函数名一样和参数一样。
- 被调用的函数必须是虚函数,也就是说必须要在两个产生多态的函数前面加virtual关键字。
- 调用函数的形参对象必须是基类对象,这里是因为派生类只能给基类赋值,会发生切片操作。基类不能给派生类赋值。
- 调用函数的参数必须是指针或引用,因为派生类改变了虚表,那么这个虚表就属于派生类对象,赋值的时候只会把基类的成员给过去,虚表指针不会给。所以在调用函数的时候会发生语法检查,如果满足多态的条件,就会触发寻找虚表中虚函数地址。如果不满足条件,则会直接用基类对象调用基类函数。
上面牵扯出两个概念:
-
虚函数:虚函数就是在类的成员函数前面加
virtual
关键字。 -
虚函数重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数。
完全相同是指:函数名、参数、返回值都相同。另外虚函数的重写也叫作虚函数的覆盖
虚函数重写有一个例外错误:协变
-
例子:C++中virtual(虚函数)的用法_foreverhuylee的专栏-CSDN博客_virtual
虚函数常见问题:
1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。
2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。
3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。
5) 纯虚函数通常没有定义体,但也完全可以拥有。
6) 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
7) 非纯的虚函数必须有定义体,不然是一个错误。
8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。
抽象类
在虚函数的后面写上 =0 ,则这个函数就变成纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
这个纯虚函数的作用就是强迫我们重写虚函数,构成多态。这样更加体现出了接口继承。
#include <iostream>
class Person
{
public:
virtual void Strength() = 0;
};
class Adult : public Person
{
public:
virtual void Strength()
{
std::cout << "Adult have big Strength!" << std::endl;
}
};
class Child : public Person
{
public:
virtual void Strength()
{
std::cout << "Child have Small Strength!" << std::endl;
}
};