多态与虚函数:
什么是虚函数:
用virtual关键字声明的函数都是虚函数。虚函数存在的唯一目的,就是为了实现多态(动态绑定/运行时绑定)。
virtual 关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。所有派生类中具有覆盖关系的同名函数都将自动成为虚函数。
静态成员函数不能是虚函数。
再说简单点:有virtual声明的函数都是虚函数。如果没有virtual,那么派生类中的同名函数会把基类中的同名函数隐藏了。如果有,那么派生类的同名函数(同参同返回)会在虚函数表中将基类的同名函数覆盖掉。
什么是多态:
多态性可以简单概括为“一个接口,多种行为”。
动态绑定(运行阶段)是多态的基础。
基类指针或引用,指向一系列的派生类对象, 调用派生类对象的同名覆盖方法(也就是那个与基类虚函数同名同参同返回的函数),指针指向谁,就会调用谁的方法。
多态分为两种:
(1)编译时多态(也叫静态的多态):主要通过函数的重载和模板来实现。
(2)运行时多态(也叫动态的多态):主要通过虚函数来实现。
覆盖:
基类的某个成员函数为虚函数,派生类又定义一个成员函数,函数名、形参、返回类型都与基类的成员函数相同。那么就会用派生类的函数覆盖掉基类的虚函数。
说一下多态是如何实现的:
在代码编译阶段产生一张虚函数表vftable。运行的时候,会载入内存,加载到数据段。一个类的虚函数表中列出了该类的全部虚函数地址。
如果成员里有虚函数,在成员变量里只会多一个虚函数指针(对象的前4个字节),指向虚函数表vftable()存放虚函数地址。虚函数的个数只会影响表的大小。不会影响对象的大小。
基类有虚函数,派生类如果有同名同参数列表同返回值的函数,派生类的函数会自动变成虚函数,在虚函数表中派生类会覆盖掉原有的函数。
举个栗子:
#include <iostream>
using namespace std;
class A
{
public:
int i;
virtual void func() {}
virtual void func2() {}
};
class B : public A
{
int j;
void func() {}
};
int main()
{
cout << sizeof(A) << ", " << sizeof(B);
return 0;
}
设 pa 的类型是 A*,则 pa->func() 这条语句的执行过程如下:
1) 取出 pa 指针所指位置的前 4 个字节,也就是虚函数指针。如果 pa 指向的是类 A 的对象,则这个地址就是类 A 的虚函数表的地址;类 B 同
2) 根据虚函数指针找到虚函数表,在其中查找要调用的虚函数的地址。
如果 pa 指向的是类 A 的对象,自然就会在类 A 的虚函数表中查出 A::func 的地址;类 B 同
类 B 没有自己的 func2 函数,因此在类 B 的虚函数表中保存的是 A::func2 的地址,这样,即便 pa 指向类 B 的对象,pa->func2();
这条语句在执行过程中也能在类 B 的虚函数表中找到 A::func2 的地址。
3) 根据找到的虚函数的地址调用虚函数。
由以上过程可以看出,只要是通过基类指针或基类引用调用虚函数的语句,就一定是多态的,也一定会执行上面的查表过程,哪怕这个虚函数仅在基类中有,在派生类中没有。
动态绑定与静态绑定:
绑定就是函数调用。
在使用的时候,用一个基类的指针指向了一个派生类的对象,如果调用这个虚函数,会先找虚函数指针,再找虚函数表,再再找虚函数地址。而这个绑定过程就是动态绑定(运行时期)。
如果你不用指针调用,而是用对象本身调用函数,不论是否是虚函数,都是静态绑定(编译时期)。
用指针调用如果是虚函数,指针识别的就是运行时期的类型;如果调用的是一般的函数,指针识别就是在编译时期。
- 没有virtual -> 静态绑定
- 有 virtual 用引用或指针 -> 动态绑定
- 有 virtual 但用对象调用-> 动态绑定
纯虚函数:
一般情况下,基类是不希望定义对象的。基类只是为了将共有的属性统一起来。
为了实现这一目的:在基类提供的一个虚函数,为所有派生类提供统一的虚函数接口,具体实现让派生类自己去重写的。
virtual void show() = 0; // 在虚函数后面加 = 0 就是纯虚函数,不用去实现。
纯虚函数实际上是不存在的,引入纯虚函数就是为了便于实现多态。
拥有纯虚函数的类叫做抽象类。抽象类不能实例化。一般基类都应该实现为抽象类。
那么问题来了:
1、基类在没有更多方法的时候,把谁实现成纯虚函数呢?--------> 析构函数
首先明确一个函数想要成为虚函数 1、它得有地址;2、得依赖对象
1)构造函数能不能是虚函数?
构造函数不依赖对象,构造函数执行完才有对象,有对象才有虚函数指针,所以不能是虚函数。
2)static成员方法能不能是虚函数? virtual static
静态函数也不依赖对象,可以直接使用类名调用,也不能是虚函数。
3)inline函数能不能是虚函数? => virtual inline
内联函数直接在程序内展开,没有地址,无法往虚函数表放。也不能是虚函数。
4)析构函数能不能实现成虚函数?
析构函数依赖对象,有地址。可以写成虚函数,