01 什么是多态
所谓的多态就是一个"对象"具有多种形态,具体:
a. 不同的对象,收到相同的消息,产生不同的行为(动态多态:继承和虚函数实现)
b. 调用相同的接口,参数不同,会产生不同的行为(静态多态:函数重载......)
说明:不同的对象:指的是类的层次关系中,具有相同基类的类型的对象
如:
Circle,Rect ------> shape show
runtime_error,bad_alloc,out_of_range .... ------> exception what
收到相同的消息:指的就是调用这些不同对象的同名成员函数
产生不同的行为:指的是函数名相同,但是函数的定义和实现不同
作用:
在一定程度上忽略相似类型的区别,而使用统一的方式使用它们(具有相同基类的类型)的对象,最终实现接口复用
// 使用统一的方式使用它们的对象(形状类相关的对象)
// 基类的引用可以绑定到派生类对象,s虽然是基类类型,但是实际绑定的可能是派生类对象
void printShapeArea(Shape &s) {
s.show(); // 虽然s是绑定到了派生类对象,但是依然调用了基类的show函数,因为s的类型还是基类
// 想法:能不能让s调用实际绑定的派生类的函数版本
// 可以!!! 在基类定义show函数前面加一个关键字virtual(show函数变成虚函数)
// 没加virtual修饰,永远调用的是基类本身的show函数
// 加了virtual修饰,调用的是s实际绑定的对象的show函数
}在C++中,从实现的角度来说,多态可以分为两种:
编译时多态(静态多态):
一个函数有可能有多种实现方式(重载),静态多态是编译器在编译期间完成的
编译器会根据实参的类型来选择调用合适的函数,如果有合适的函数调用就调用,没有的话就发出警告或者错误
实现方式:
函数重载
运算符重载
模板 ----->STL
一个名字能够根据参数不同完成不同的功能!!!
在编译的时候,就已经可以确定调用的是哪一个版本的函数了!!!运行时多态(动态多态):
一个成员函数有可能在继承体系中有多种实现方式,并且基类引用/指针能够绑定/指向派生类对象
当使用基类引用/指针调用该函数的时候,在实际运行的时候才能决定调用哪一个函数
因为在实际运行的时候才知道基类的引用/指针实际指向的是哪一个对象!!!实现方式:
虚函数!!!
02 虚函数
使用virtual修饰的函数,称为虚函数
格式:
virtual 返回值类型 函数名(参数列表) {
函数体;
}
为什么需要虚函数?为了实现动态多态
在设计基类的时候,有些成员函数希望派生类重新定义该函数以覆盖(重写)基类的版本
这种函数应该被声明为虚函数!!!
特点:
当使用基类指针或者基类引用调用虚函数的时候,该过程会发生动态绑定,会根据基类指针/引用实际绑定的对象来确定应该调用哪一个版本的函数!!!
实际指向什么对象,就会调用什么对象的函数版本!!!前提:
a. 函数是虚函数
b. 基类指针/引用绑定到派生类对象
动态绑定:一般情况下,指针(引用)的类型必须与它指向(绑定)的实际对象类型一致,但是有两种特殊的情况:
1. 允许一个指向常量的指针,指向一个非常量对象
int i = 10;
const int *p = &i; // YES
const int i = 10;
int *p = &i; // ERROR
======>
const int i = 10;
int *p = const_cast<int *>(&i);
2. 允许将一个基类指针(引用)指向(绑定到)派生类对象Derived d; // 派生类对象
Base *pb = &d;
Base &rb = d;
也就是说,当我们使用指针(引用)的时候,我们并不清楚该指针(引用)实际指向的对象的真实类型,指向的对象可能是基类对象,也有可能是派生类对象只有在运行的时候,才可以根据指针去访问实际指向的对象,来确定自己应该调用哪一个类的函数
简单的说:
如果在运行的时候,指针(引用)所以指向的对象是基类对象,则调用基类定义的函数版本
如果在运行的时候,指针(引用)所以指向的对象是派生类类对象,则调用派生类定义的函数版本
前提:函数必须是一个虚函数!!!!
C++的虚函数的作用是用来实现动态多态的
动态多态的现象:
使用父类的指针指向子类对象的实例,可以通过父类指针调用实际指向的子类函数
这种技术可以让父类指针(引用)有"多种形态",这是一种泛型技术,就是试图通过不变的代码实现可变的算法!!!
03 派生类中的虚函数
当我们在派生类中覆盖一个虚函数时,可以再次使用virtual说明,但是不是必须的,因为一旦基类的某一个函数声明为虚函数,那么在他的派生类中,与该函数原型相同的那些函数,自动成为虚函数
函数原型:
返回值类型 函数名 参数列表都必须相同(包括后面的const / noexcept 等修饰也必须相同)
c++11也提供了一个保留字(override:重写),用于说明某一个函数是覆盖了基类的虚函数,说明这个函数是重写了基类的虚函数(在编程中建议加上)
目的:
1. 在成员函数比较多的情况下,可以提示用户某一个函数是重写了基类的虚函数,提高可读性
2. 编译器会强制检查某一个函数是不是重写了基类的虚函数,如果不是则报错(防止程序员粗心),override修饰的函数原型必须和父类虚函数相同
如果一个基类的某一个虚函数不希望被派生类覆盖,可以使用final修饰virtual void show() const final { cout << "shape:name:" << name_ << endl; cout << "shape:area:" << 0.0 << endl; }
同时final也可以修饰一个类,在声明的时候,写在类名后面,表示这个类不允许被继承
也就是说,被final修饰的类不能作为基类!!!class Shape final {};
04 虚函数与默认参数
虚函数也可以设置默认参数
如果虚函数设置了默认参数,则基类与派生类中设置的默认参数数值最好保存一致,如果不一致,则以基类中设置的默认值为准
注意:
1. 当虚函数的声明和定义分开的时候,只需要在声明的地方加上virtual的说明,定义的时候,不能再加上virtual说明了(virtual只需要出现在类里面)
2. 当声明与定义分开的时候,定义处不能再加上override说明了(override只需要出现在类里面)
05 虚函数回避机制
在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是执行某一个特定的函数版本,此时需要虚函数回避机制
使用作用域运算符就可以达到这个目的
如:Circle c{10};
Shape &rc = c; // 基类的引用绑定到派生类对象
rc.show(); // show是虚函数,会发生动态绑定
rc.Shape::show(); // 通过作用域运算符,避免发生动态绑定
06 纯虚函数和抽象类
有时候在设计基类的时候,某些函数可能不知道如何去定义或者不应该去定义
如:形状类是一个基类,形状应该有面积吗?
由于形状类是一个通用的概念,在形状基类中不知道如何定义形状的面积,同时也不希望用户去实例化形状对象
这种情况下,我们可以把这些不知道/不应该定义的函数定义为纯虚函数(pure virtual)
格式如下:
virtual 返回值类型 函数名(参数列表) = 0;
纯虚函数不需要定义,只需要在函数声明时,在形参列表后面写上"=0"即可
“=0”不是说函数的值为0,它仅仅是一种形式,用来告诉编译器,这个函数本来没有定义,需要派生类去实现它
如果一个类中含有纯虚函数,这种类叫做抽象类(Abstract Base class)
抽象类不能用来实例化对象,但是可以创建基类指针或者基类引用绑定派生类对象如果一个类继承自一个抽象类,但是本身依然没有实现那个纯虚函数,则这个派生类依然是抽象类
07 虚函数和构造函数
构造函数可以是虚函数吗?
不可以!!!
constructors cannot be declared ‘virtual’
因为访问虚函数前对象必须存在并且可用!!!
因为虚函数对应一个虚函数表(VTable),调用虚函数前,需要访问虚函数表,但是虚函数表的地址存储在对象的存储空间中
如果构造函数可以是虚函数,那么就需要通过VTable来调用构造函数
可对象在调用构造函数前还不存在(未初始化,不可以使用),无法找到VTable,所以构造函数不能是虚函数
虚函数调用过程:
调用虚函数---->找到实际指向的对象--->虚函数表地址--->虚函数表 ----->虚函数
C++中的struct中可以有虚函数吗?
可以把代码中所有的class都换成struct,代码可以正常运行
08 虚函数和析构函数
析构函数可以是虚函数吗?
可以,因为在析构前,对象已经存在并且可以使用了,并且基类的析构函数建议设置为虚函数!!!
Base *pb = new Derived{};
delete pb; // 析构Base::constructor
Derived::constructor
Base::de-structor
pb是基类指针,释放pb的时候,默认只能调用基类的析构函数
在这种情况下,实际上是有想要调用派生类的析构函数的
使用基类指针去指向派生类对象,当通过基类指针释放派生类对象时
如果基类的析构函数不是虚函数(不会发生动态绑定,不会调用实际指向的对象的函数版本),此时只会调用基类本身的析构函数,而不会调用派生类的析构函数,可能造成资源浪费所以,我们应该把基类的析构函数设置为虚函数,当通过基类指针释放派生类对象时,就能够发生动态绑定实现多态效果,会调用基类指针实际指向的对象的析构函数
如果基类的析构函数设置为虚函数,则它所有的派生类中的析构函数都自动成为虚函数!!!
09