多态:顾名思义就是具有多种形态。是C++的三大特性之一。
静态多态:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
如下面代码为静态多态,根据实参类型调用函数。
#include <iostream>
using namespace std;
int add(int left, int right)
{
return left + right;
}
double add(double left, double right)
{
return left + right;
}
int main()
{
cout << add(1, 2) << endl;
cout << add(1.0, 2.0) << endl;
return 0;
}
动态多态:亦称动态绑定,是在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,派生类需要重新实现该虚函数。编译器实现动态绑定。
如下面代码为动态多态,根据其实际类型调用函数。
#include <iostream>
using namespace std;
class B
{
public:
virtual void FunTest()
{
cout << "B::FunTest()" << endl;
}
};
class D :public B
{
public:
virtual void FunTest()
{
cout << "D::FunTest()" << endl;
}
};
void FunTest(B& b)
{
b.FunTest();
}
int main()
{
B b;
D d;
FunTest(b);
FunTest(d);
return 0;
}
【动态绑定条件】
1、必须是虚函数
2、通过基类类型的引用或者指针调用虚函数
静态多态是通过函数重载和运算符重载实现的。动态多态是通过继承和虚函数重写实现的。下面我们来看下继承体系同名函数关系。
上图说了协变除外,那什么是协变呢?
协变是指在派生类中重新实现虚函数时,返回值类型可以和基类不同,但基类和派生类中的返回值类型必须为该类型指针,仅此一项不同其余均与函数重写条件一致构成协变。
#include <iostream>
using namespace std;
class CBase
{
public:
virtual CBase* GetPtr()
{
cout << "CBase::GetPtr()" << endl;
return this;
}
};
class CDerived :public CBase
{
public:
virtual CDerived* GetPtr()
{
cout << "CDerived::GetPtr()" << endl;
return this;
}
};
void FunTest(CBase* p)
{
p->GetPtr();
}
int main()
{
CBase b;
CDerived d;
FunTest(&b);
FunTest(&d);
return 0;
}
纯虚函数
在成员函数的形参列表后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化对象。纯虚函数在派生类中重新定义以后,派生类才能实例化对象。
class B
{
public:
virtual void FunTest() = 0; // 纯虚函数
};
// D也是一个抽象类
class D :public B
{public:
virtual void FunTest()
{}
};
总结:
1、派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)。
2、基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3、只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。友元函数不属于类的成员函数,不能被定义为虚函数。
4、如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。
5、构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆。
6、不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
7、最好将基类的析构函数声明为虚函数。(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)。
8、虚表是所有类对象实例共用的。
#include <iostream>
using namespace std;
class CTest
{
public:
CTest()
{ iTest = 10;
cout << "this=" << this << endl;
}
virtual~CTest(){};
private:
int iTest;
};
int main()
{
CTest test;
cout << sizeof(CTest) << endl;
return 0;
}
对于有虚函数的类,编译器都会维护一张虚表,在里面存放各个虚函数的地址,最后以0结尾,而对象的前四个字节就是指向虚表的指针。如上图中虚表指针地址为0x001bfbf8,虚函数存放于虚表指针所指向地址0x00b5dc74中。
虚函数在多态中的调用次序:
单继承:
(1).首先调用基类中的虚函数,按在基类中声明的顺序调用。
(2).派生类先调用基类中继承的虚函数,基类中继承的虚函数调用完后,按照在派生类中声明的顺序调用派生类自己的虚函数,放在基类中继承的虚函数的后面。如果派生类重写了基类中的某个虚函数,就会替换相同偏移量位置的基类虚函数。
多继承:
(1)基类中虚函数参照单继承中基类虚函数声明的顺序调用。
(2)派生类先调用基类中继承的虚函数,基类中继承的虚函数调用完后,自己的虚函数也参照单继承中派生类虚函数声明的顺序调用。派生类中重写的虚函数替换相同偏移量位置的基类虚函数。
(3)派生类自己的虚函数放在第一个继承基类的虚表后面调用。
补充说明:
1虚表在编译时产生。
2.相同类型对象共用一张虚表。
3.若调用声明为虚函数的析构函数时,总是先调用派生类的析构函数,再调用基类的析构函数。