1、多态的定义
多态就是多种形态,就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
#include<iostream>
using namespace std;
class Person
{
public:
virtual void display()
{
cout<<"Person()"<<endl;
}
};
class Student:public Person
{
public:
virtual void display()
{
cout<<"Student()"<<endl;
}
};
void func(Person& people)
{
people.display();
}
int main()
{
Person p;
func(p);
Student s;
func(s);
return 0;
}
虚函数
被virtual修饰的类成员函数称为虚函数
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
虚函数重写的两个例外:
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。
#include<iostream>
using namespace std;
class A{};
class B:public A{};
class Person
{
public:
virtual A* fun()
{
return new A;
}
};
class Student:public Person
{
public:
virtual B* fun()
{
return new B;
}
};
int main()
{
return 0;
}
- 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
#include<iostream>
using namespace std;
class Person
{
public:
virtual ~Person()
{
cout<<"~Person()"<<endl;
}
};
class Student:public Person
{
public:
virtual ~Student()
{
cout<<"~Student()"<<endl;
}
};
int main()
{
Person * p = new Person();
Person * s = new Student();
delete p;
delete s;
return 0;
}
C++11:override和final
C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
- final:修饰虚函数表示该虚函数不能再被继承
#include<iostream>
using namespace std;
class A
{
public:
virtual void fun() final{}
};
class B:public A
{
public:
/*final修饰基类虚函数不能被继承
virtual void fun()
{
cout<<endl;
}
*/
};
int main()
{
return 0;
}
- override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
#include<iostream>
using namespace std;
class A
{
public:
virtual void fun(){}
};
class B:public A
{
public:
//override检查派生类是否重写基类虚函数
virtual void fun() override
{
cout<<endl;
}
};
int main()
{
return 0;
}
重载、重写(覆盖)、隐藏(重定义)
- 重载:两个函数在同一个作用域,函数名相同
- 重写(覆盖):两个函数分别在基类和派生类的作用域,函数名/参数/返回值相同(协变除外),仅函数体不同,两个函数必须是虚函数
- 隐藏(重定义):两个函数分别在基类和派生类的作用域,函数名相同,两个基类和派生类的同名函数不构成重写就是隐藏
2、抽象类
在虚函数的后面写上=0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun() = 0;//纯虚函数
};
class A:public Base
{
public:
virtual void fun()
{
cout<<"A"<<endl;
}
};
class B:public Base
{
public:
virtual void fun()
{
cout<<"B"<<endl;
}
};
int main()
{
Base * pA = new A();
Base * pB = new B();
pA->fun();
pB->fun();
return 0;
}
接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
- 所以如果不实现多态,不要把函数定义成虚函数。
3、多态的原理
- 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
- 派生类对象中也有一个虚表指针,派生类对象由两部分构成,一部分是父类继承下来的成员、虚表指针,另一部分是自己的成员。
- 基类对象和派生类对象虚表是不一样的,派生类的虚表中存的是重写的虚函数,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。
- 派生类继承下来的虚函数放进了虚表,不是虚函数不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
- 派生类的虚表生成:先将基类中的虚表内容拷贝一份到派生类虚表中;如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外,对象中存的不是虚表,存的是虚表指针。
- 多态有两个条件:一是虚函数覆盖;二是对象的指针或引用调用虚函数。
- 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中找的。不满足多态的函数调用是编译时确定的。
动态绑定与静态绑定
- 静态绑定又称前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
4、单继承和多继承关系中的虚函数表
单继承的虚函数表
单继承派生类的未重写的虚函数放在基类部分的虚函数表中。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun1()
{
cout<<"Base::fun1()"<<endl;
}
virtual void fun2()
{
cout<<"Base::fun2()"<<endl;
}
private:
int _b = 1;
};
class Inher:public Base
{
public:
virtual void fun1()
{
cout<<"Inher::fun1()"<<endl;
}
virtual void fun3()
{
cout<<"Inher::fun3()"<<endl;
}
virtual void fun4()
{
cout<<"Inher::fun4()"<<endl;
}
private:
int _i = 2;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout<<"Virtual Table Address > "<<vTable<<endl;
for(int i = 0; vTable[i] != nullptr; ++i)
{
printf("The %d Virtual Funcation Address : 0X%x, -> ", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout<<endl;
}
int main()
{
Inher i;
VFPTR * vTableI = (VFPTR *)(*(int *)&i);
PrintVTable(vTableI);
Base b;
VFPTR * vTableB = (VFPTR *)(*(int *)&b);
PrintVTable(vTableB);
return 0;
}
多继承的虚函数表
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
#include<iostream>
using namespace std;
class Base1
{
public:
virtual void fun1()
{
cout<<"Base1::fun1()"<<endl;
}
virtual void fun2()
{
cout<<"Base1::fun2()"<<endl;
}
private:
int _b1 = 1;
};
class Base2
{
public:
virtual void fun1()
{
cout<<"Base2::fun1()"<<endl;
}
virtual void fun2()
{
cout<<"Base2::fun2()"<<endl;
}
private:
int _b2 = 1;
};
class Inher:public Base1,public Base2
{
public:
virtual void fun1()
{
cout<<"Inher::fun1()"<<endl;
}
virtual void fun3()
{
cout<<"Inher::fun3()"<<endl;
}
virtual void fun4()
{
cout<<"Inher::fun4()"<<endl;
}
private:
int _i = 2;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout<<"Virtual Table Address > "<<vTable<<endl;
for(int i = 0; vTable[i] != nullptr; ++i)
{
printf("The %d Virtual Funcation Address : 0X%x, -> ", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout<<endl;
}
int main()
{
Inher i;
VFPTR * vTable1 = (VFPTR *)(*(int *)&i);
PrintVTable(vTable1);
Base1 b1;
VFPTR * vTable2 = (VFPTR *)(*(int *)&b1);
PrintVTable(vTable2);
Base2 b2;
VFPTR * vTable3 = (VFPTR *)(*(int *)&b2);
PrintVTable(vTable3);
return 0;
}
5、其他
- inline函数不能是虚函数,因为inline函数没有地址,无法把地址放在虚函数表中
- 静态成员函数不能是虚函数,因为静态成员函数没有this指针,使用"类型::成员函数"方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
- 构造函数不能是虚函数,因为对象中虚函数表指针是在构造函数初始化阶段才初始化的,而构造函数是用于初始化对象的
- 虚构函数可以是虚函数,且最好把基类的析构函数定义成虚函数
- 普通对象访问普通函数和访问虚函数一样快。指针对象和引用对象访问普通函数更快,因为调用虚函数需要去虚函数表中查找
- 虚函数表在编译阶段生成,一般情况下存在代码段
- 抽象类强制重写虚函数,体现了接口继承关系