温馨提示:在观看本文前确保已经了解了C++中继承的相关知识,若不了解,可以查看我的这篇文章进行学习:【C++】继承:深度剖析-CSDN博客https://blog.csdn.net/2301_80555259/article/details/141829528?spm=1001.2014.3001.5501
目录
一.多态的概念
二.多态的定义和实现
三.虚函数
1.概念
2.重写
3.重写的两个例外
3.1 协变(返回类型不同)
3.2 析构函数的重写(函数名不同)
4.C++11:override和final
5.重载、重写、隐藏的对比
四.抽象类
五.虚函数原理
六.多态的原理
一.多态的概念
通俗来讲,多态就是拥有多种状态,父子对象完成相同的任务却会产生不同的结果;例如普通人和学生去买票(进行相同的任务),普通人(父类)是全价,而学生(子类)是半价,这就是多态的一种现实体现。
从程序角度来讲,多态是指同一个函数名可以根据调用对象的不同而具有不同的实现。多态分为两种类型:编译时多态(静态多态)和运行时多态(动态多态)
- 编译时多态:通过函数重载和运算符重载实现,是在编译阶段确定函数调用
- 运行时多态:通过虚函数和继承实现,是在运行阶段确定函数调用
二.多态的定义和实现
多态的实现通常依赖于虚函数。在基类(父类)中声明虚函数,然后在派生类(子类)中进行重写(覆盖)。通过基类指针或引用调用虚函数时,将根据对象的实际类型调用相应的派生类函数
在继承中构成多态需要有两个前提条件:
- 必须通过基类的指针或者引用调用函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
那么,什么是虚函数?接下来就对虚函数进行深度解析
三.虚函数
1.概念
虚函数是在基类中使用关键字virtual进行声明的成员函数,它的存在允许在派生类中进行该函数的重写(覆盖)。
class Person
{
public://虚函数virtual void BuyTicket() { cout << "买票-全价"; }
};
2.重写
虚函数的重写(Override)是指在派生类中重新实现上述基类中已经声明为虚函数的函数。在进行重写时,子类中的虚函数的返回类型、函数名、参数列表类型必须与基类中虚函数完全一致(参数名称、缺省值可以不相同)
注意:在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,这依然构成重写,因为继承后基类的虚函数在派生类中依然保持虚函数属性,但这种写法其实不规范,不建议这样书写
3.重写的两个例外
3.1 协变(返回类型不同)
派生类重写虚函数其返回类型为派生类指针或引用,而基类虚函数其返回类型为基类指针或引用时,此时尽管两虚函数返回类型不同,也依然是虚函数重写,称为协变
class A
{
public://返回类型为基类指针virtual A* f() { return new A; }
};class B:public A
{
public://返回类型为派生类指针virtual B* f(){return new B;}
};
3.2 析构函数的重写(函数名不同)
如果基类的析构函数为虚函数,那么派生类的析构函数只要定义,无论是否加virtual关键字都构成重写,尽管函数名字不同。这里其实是编译器做了特殊处理,编译后析构函数的名称统一处理为destructor
至于为什么要这样处理,可以用以下样例解释:
class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B :public A
{
public://不加virtual也依旧构成重写~B(){cout << "~B()" << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;
}
若基类A和派生类B不构成虚函数重写的话,那么此处调用delete p2时,p2是基类A的指针,只会调用A的析构,从而没有调用B的析构,此时B内申请的资源int* _p就没有释放,造成了内存泄漏,如下图,为了防止这种情况,才使用继承中的虚函数析构
4.C++11:override和final
1.override
派生类虚函数的格式有严格要求,返回类型、函数名、参数类型都必须完全相同,但这在编译时是不会报错的,哪怕函数名打错了一个字母也不会报错让你发现,这时就需要override来检查是否正确地重写了该函数了,例如:
class Car
{
public:virtual void Drive() {}
};class Benz :public Car
{
public:virtual void Drive() override {}
};
此时如果函数名写错就会在编译阶段报错了
2.final
final用于在派生类中阻止对虚函数的进一步重写,或者在类定义中阻止类被继续派生
//阻止该类继续派生,该类就是"最终"(final)类
class Base final {// ...
};
//阻止该虚函数被继续重写
class Base {
public:virtual void f() final {// ...}
};
5.重载、重写、隐藏的对比
四.抽象类
在虚函数的后面加上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,只有其派生类重写纯虚函数后,派生类才能实例化出对象。也就是说,纯虚函数规定了派生类必须重写。
实际上很好理解,例如把动物当做一个抽象类,你能实例化找出一个“动物”吗?动物是个抽象的概念,无法实际存在,只有将其重写为猫、狗等实际存在的动物后,才能实例化地找到。
五.虚函数原理
在调试一个含有虚函数的类Base时,我们发现该类除了成员变量_base外,还包含一个变量_vfptr,这个变量就是虚函数表指针(virtual function pointer)
一个含有虚函数的类中至少都有一个虚函数表指针,该指针指向的是一个数组,该数组是储存函数指针的。也就是说,该指针指向一个函数指针数组。该数组存放虚函数的地址,不同的派生类该表也不同。
通过一下例子,我们可以再详细谈谈这整个过程
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}
private:int _base = 1;
};class Derive :public Base
{
public:virtual void Func1(){cout << "Func111()" << endl;}//virtual void Func2()//{// cout << "Func222()" << endl;//}
protected:int _derive = 2;
};int main()
{Base b;Derive d;return 0;
}
通过该结果可以发现,派生类Derive的虚函数表与基类Base是不同的,Derive中对虚函数Func1进行了重写,但没有对Func2重写,这就导致了d类的虚表中第一个Func1的地址被覆盖了,而第二个Func2没有被覆盖,和基类相同。因此继承时,派生类是继承了基类的虚函数表的,不过当虚函数重写时就进行函数地址的覆盖而已。
顺带一提,如果在Derive中在新写一个虚函数,vs的调试窗口上d类的虚表中是看不见的,但可以通过内存窗口去观察
六.多态的原理
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person man;Func(man);Student Jack;Func(Jack);return 0;
}
根据下图的红色箭头可以发现,p是指向man对象时,p.BuyTicket在虚表中找到的是Person::BuyTicket,而蓝色箭头,p是指向Jack对象时,p.BuyTicket在虚表中找到的就是Student::BuyTicket,这不是在编译时确定的,而是运行以后到对象中找到的。
当然这里的核心依然是切割,编译器看到的是父类,不过是指向子类时是切割过去的而已,里面的虚表也是被子类覆盖后的结果。
好了,多态的知识解析就是这些,最后再来一道很容易错的习题来结束吧
以下程序输出结果是什么()
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;
}
答对了吗?
解析: