1. 多态的概念
多态:通俗的来说,就是同一件事物,在不同场景下表现出的不同的状态。
例如:买票(普通人全价票、学生半价票、军人优先)。
2.多态的定义及实现
2.1 多态的实现条件—必须在继承体系中
- 基类中必须有虚函数(被virtual修饰的函数),派生类必须对基类中的虚函数进行重写
- 必须通过基类的指针或引用调用虚函数
2.2 虚函数的重写
- 虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价票" << endl;
}
protected:
string _name;
string _gender;
string _job;
int _age;
};
class Student:public Person
{
public:
virtual void BuyTicket()
{
cout << "半价票" << endl;
}
protected:
int _stuId;
double _score;
};
class soldier :public Person
{
public:
virtual void BuyTicket()
{
cout << "军人优先" << endl;
}
};
//在程序运行时,根据基类引用实际引用的子类对象,选择调用对应类的虚函数
void TestBuyTicket(Person& p)
{
p.BuyTicket();
}
//如果多态的实现条件没有完全满足,全部调用基类的虚函数
int main()
{
Person p;
Student st;
soldier so;
TestBuyTicket(p);
TestBuyTicket(st);
TestBuyTicket(so);
system("pause");
return 0;
}
虚函数重写的两个例外
1.协变(基类与派生类虚函数返回值类型不同)
基类虚函数返回基类的指针或引用
派生类虚函数返回派生类的指针或引用
class Base
{
public:
virtual Base* TestFunc()
{
cout << "Base::TestFunc()" << endl;
return this;
}
};
class Derived :public Base
{
public:
virtual Derived* TestFunc()
{
cout << "Derived::TestFunc()" << endl;
return this;
}
};
void TestVirtualFunc(Base* pd)
{
pd->TestFunc();
}
int main()
{
Base b;
Derived d;
TestVirtualFunc(&b);
TestVirtualFunc(&d);
system("pause");
return 0;
}
2.析构函数的重写(基类与派生类析构函数的名字不同)
只要基类中的析构函数是虚函数,派生类的析构函数一旦提供,就可以与基类的析构函数构成重写
建议:在继承体系中,如果派生类中涉及资源管理,最好把基类中的函数给成虚函数
//常见的面试题:析构函数能不能是虚函数?如果可以,什么情况下建议将基类的析构函数设置成虚函数
class Base
{
public:
virtual ~Base()
{
cout << "Base::~Base()"<<endl;
}
protected:
int _b;
};
class Derived :public Base
{
public:
Derived()
{
_p = new int[10];
}
~Derived()
{
if (_p)
{
delete[] _p;
}
cout << "Derived::~Derived()" << endl;
//call Base::~Base();
}
protected:
int* _p;
};
int main()
{
Base* pb = new Derived;
delete pb;
system("pause");
return 0;
}
c++11 final和override
- final:修饰虚函数,表示该虚函数不能再被继承
- override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
3.抽象类
3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的 继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所 以如果不实现多态,不要把函数定义成虚函数。
4.多态的原理
4.1 虚函数表
经过测试,我得到了虚表的构建规则:
对于上面的代码,我们再增加些东西,来看看派生类虚表的构建规则:
1.增加一个派生类Derived去继承Base
2.Derived中重写TestFunc3()和TestFunc2()
3.Derived中新增虚函数TestFunc5()和TestFunc4()
由此我们可以得到派生类虚表构建规则:
1. 将基类虚表内容拷贝一份到派生类虚表中
2. 如果派生类重写了基类某个虚函数,用派生类虚函数地址覆盖相同偏移量位置的基类虚函数地址
3. 派生类新增加的虚函数按照其在派生类中的声明次序依次添加到派生类虚表的最后
说了这么多,那么多态的原理到底是什么呢?
这个问题我们通过汇编代码就可以一目了然,我在这里就不展开放图了。其实就是寄存器拿到对象的前四个字节,也就是虚表指针,然后加上一定的偏移量,就可以指向虚表中函数的入口地址,最后在编译时,直接call地址,就能够直接调用那个函数了。
我们在上面代码的前提下,再加入一个函数指针,然后遍历虚表,就可以访问到所有的函数了。
代码如下:
//函数指针变类型
typedef void(*PVFT)();
int main()
{
PVFT p;
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;
Base b;
b._b = 10;
Derived d;
d._b = 1;
d._d = 2;
//需要获取虚表的地址:对象前4个字节
//&d; 对象首地址
//(int*)&d 整形指针---指向对象前4个字节
//*(int*)(&d) 将对象前4个字节中的内容取出--->整形数据 和虚表地址在数据值上是相等的
PVFT* pVFT=(PVFT*)(*(int*)(&d));
while (*pVFT)
{
(*pVFT)();
++pVFT;
}
system("pause");
return 0;
}
上面讨论的都是单继承下的多态,最后我们看看多继承下的多态是什么样子的。
多态的分类: