C++之多态
1.多态的概念
多态简而言之就是多种形态的意思。多态分为编译时多态(静态多态)和运行时多态(动态多态)。
静态多态:主要就是函数重载和函数模版两类,因为实参传给形参是在编译时完成的,所以又称为编译时多态。
动态多态:具体点来说就是去完成某个行为,可以传给不同的对象从而打到完成不同的行为。
以买票为例:成人买成人票,学生则会买优惠的学生票。这属于动态多态
2.多态的定义及实现
2.1多态的构成条件
多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。
2.1.1实现多态还有两个必须重要条件
1.必须指针或者引用调用虚函数
2.被调用的函数必须是虚函数
说明:要实现多态效果,第一必须是基类的指针或者引用,因为只有基类的指针或引用才能指向派生类。第二派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到
2.1.2虚函数
类函数前面加virtual修饰,那么这个成员函数被称为虚函数。(注意非成员函数不能加virtual修饰)
2.1.3虚函数的重写/覆盖
虚函数的重写/覆盖:派生类中有一个基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),即称派生类的虚函数重写了基类的虚函数。
注意:在写派生类的虚函数也可以不用带上virtual因为派生类把基类的虚函数属性继承下来了,但是这种写法不规范,尽量不要这样写。
2.1.4 多态场景的一个选择题(重要)
以下程序输出结果是什么()
A:A—>0 B: B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确
class A{public:virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}virtual void test(){ func();}};class B : public A{public:void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }};int main(int argc ,char* argv[]){B*p = new B;p->test();return 0;}
正确答案是B,原因: 重写的本质是重写函数实现,函数声明是不可替换的,所以还是用的父类的int val = 1;
2.1.5 虚函数重写的一些其他问题
1.协变
派生类重写基类函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变,该知识点意义不大稍作了解即可。
2.析构函数的重写
基类函数一定要虚化,并且派生类的析构函数一定要重写,这样可以避免内存泄漏问题。
下面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为B的资源没有释放。
注意:根据下面例子可以很好解释基类中的析构函数为什么建议设计为虚函数。
class A{public:virtual ~A(){cout << "~A()" << endl;}};class B : public A {public:~B(){cout << "~B()->delete:"<<_p<< endl;delete _p;}protected:int* _p = new int[10];};// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。int main(){A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;}
2.1.6 override 和 final关键字
C++对函数的重写比较严格,但是有些情况下由于疏忽(写错参数名之类),而这种错误在编译时不会报出,但程序无法正常运行,因此C++提供了override,可以帮助用户检测是否重写,如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。
// error C3668: “Benz::Drive”: 包含重写说明符 “override” 的⽅法没有重写任何基类⽅法class Car
{
public:
virtual void Dirve()
{}
};
class Benz :public Car
{
public:
virtual void Drive() override { cout << " Benz-舒服" << endl;}
};
int main()
{
return 0;
}
// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{
public:
virtual void Drive() final{}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << " Benz-舒服 " << endl;}
};
int main()
{
return 0;
}
2.1.7 重载/重写/隐藏的对比
注意:这个概念不要死记硬背,要多加理解
3.纯虚函数和抽象类
在虚函数的后面写上 = 0,则这个函数为纯虚函数,纯虚函数需要定义实现,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写纯虚函数,因为不重写实例化不出对象。
class Car{public:virtual void Drive() = 0;};class Benz :public Car{public:virtual void Drive(){cout << "Benz-舒适" << endl;}};class BMW :public Car{public:virtual void Drive(){cout << "BMW-操控" << endl;}};int main(){// 编译报错:error C2259: “Car”: ⽆法实例化抽象类Car car;Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;}
4.多态的原理
4.1多态是如何实现的
满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
4.2动态绑定与静态绑定
1.对不满足多态条件的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
2.满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
4.3 虚函数表
1.基类对象的虚函数表中存放基类所有虚函数的地址
2.派生类有两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。
注意:这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个。
3.派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
4.派生类的虚函数表中包含:基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。
5.虚函数表本质是一个存虚函数指针的指针数组。
6.虚函数和普通函数一样的,编译好后是一段指针,都是存在代码段的,只是虚函数的地址又存到了虚表中。