【C++】12.多态(超详解)
一、多态的概念
1.什么是多态?
通俗点说就是一个事物有多种形态,就是“—个接口,多种实现”,也就是说调用同一个接口,而产生不同的行为。
2.多态的分类
多态分为编译时多态(也叫静态多态)和运行时多态(也叫动态多态)。
①编译时多态(静态多态)主要是函数重载和函数模板。
他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。
②运⾏时多态
就是去完成某个⾏为(函数),传不同的对象就会完成不同的⾏为,就达到多种形态。
比如:买车票这件事,有普通票,有学生票半价,军人优先买票等等。
二、多态的实现
1.两个必要条件(★)
- 必须是基类的指针或者引用调⽤虚函数
- 被调⽤的函数必须是虚函数,并且虚函数完成了重写/覆盖。
2.虚函数
虚函数,就是在成员函数前面加上virtual关键字 。
class A
{
public://虚函数virtual void fun(){//...}
};
注意:
- 只能是成员函数,不是函数!不能在类外声明和定义
- 静态成员函数不能设置为虚函数,因为没有 this 指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3.虚函数的重写/覆盖
class A
{
public://虚函数virtual void fun(){//...}
};
class B : public A
{
public://虚函数重写virtual void fun(){//...}
};
4.一个选择题目
p->test()
→ 执行 A::test
的函数体 → A::test
中调用 func()
→ func
动态绑定到 B::func
,且默认参数取 A::func
的 val = 1
→ 执行 B::func(1)
→ 输出 B->1
。在 A::test
中调用 func()
时,this
的类型是 A*
(因为 test
是 A
的成员函数),所以默认参数由 A
类的 func
决定,即 val = 1
。
5.虚函数重写的两个例外
1) 协变(了解)
协变,指的是派生类重写基类虚函数时,与基类虚函数返回值类型不同,此时需要满足:基类虚函数的返回值是基类对象的指针或引用,派生类虚函数的返回值是派生类对象的指针或引用。
2)析构函数的重写
class A
{
public:~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:"<<_p<< endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;}
这个程序父类析构函数没有加virtual,运行结果是两个“ ~A() ”
我们创建了分别为A、B类型的两个对象,将它们的地址赋值给两个A*指针p1和p2。指针p1,它所指向的对象是A类型,对象销毁时直接调用A类型的析构函数;指针p2,它所指向的对象是B类型,B是A的子类,其在销毁时首先要调用子类析构,然后调用父类析构,但是程序并没有调用子类析构,导致内存泄漏。这是由于指向它的指针是A*类型,所以只会调用A的析构函数。
6.override 和 final关键字
C++对虚函数重写的要求较为严格,为防止因为函数名写错、参数不匹配等疏忽导致无法构成重写,这类错误在编译阶段不会报出,直到程序运行未达预期结果时才来调试。
C++11 提供了 override
关键字来帮助检测是否成功重写
class B : public A
{
public:virtual void Buy() override{//...}
};
final,修饰虚函数,表示该虚函数不能继承;
class A
{
public:virtual void Buy() final {// ... }
};
7.重载、重写、隐藏的对比
三、纯虚函数和抽象类
- 纯虚函数不需要定义实现
- 抽象类不能实例化出对象
- 如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。
class A
{
public://纯虚函数virtual void fun() = 0;
};
四、多态的实现原理
1.函数虚表
先看这段代码,请问类的大小是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _a = 1;char _ch = 'x';
};
int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
按照内存对齐,我们肯定会认为是 8,思路:int 类型_a 偏移量从 0 开始排到 3,然后放的是 char类型的_ch,然后输出的字节应该是成员最大对齐数的整数倍也就是 8,但是事实确实 12,为什么呢?这里就涉及到了虚表的概念。
调试一下可以看到,b里面除了成员变量_a和_ch外还有一个_vfptr。_vfptr叫做虚函数表指针,是一个指针类型,在x32环境下,它的大小是4个字节,8+4正好等于我们得到的答案12。
虚函数表就是存放虚函数地址的一个表。【0】就是func1的地址,实际上,对于一个有虚函数的类至少都有一个虚函数表指针,该指针指向一个虚函数表,虚函数表简称虚表,虚函数表指针简称虚表指针!
注意:
- 虚表里面存的不是虚函数,而是虚函数的地址,表里只有虚函数。
- 虚表的本质就是一个存虚函数指针的指针数组。
- 虚表是在编译阶段就生成的。
- 虚函数跟普通函数一样存在代码段。
- 如果派生类重写了基类的虚函数,那么派生类的虚函数表中对应的虚函数地址就会被覆盖成重写的新虚函数地址。
如果子类重写了父类的虚函数,子类里虚函数表存放的指针也会对应改变。
2. 动态绑定和静态绑定
根据多态的实现原理,编译时多态与运行时多态的主要区别在于绑定时机不同,由此引出静态绑定与动态绑定的概念:
静态绑定发生在程序编译期间,确定程序行为,例如函数重载、函数模板,对应编译时多态;
动态绑定则在程序运行期间,根据实际对象类型确定具体行为并调用特定函数,对应运行时多态。