目录
多态的使用
多态的概念
多态的定义和实现
虚函数
构成多态的条件
特殊情况:协变
析构函数的重写
怎么实现
为什么实现
override和final关键字
override
final
重载/重写/隐藏的对比
纯虚函数和抽象类
纯虚函数
抽象类
多态的实现
虚函数表指针
多态的实现
实现方式
动态绑定与静态绑定
虚函数表的存储地址
多态的使用
多态的概念
多态从字面意思来看就是多种状态,比如说我们前面使用的函数模版swap,重载函数就是多态的一种体现只不过是在编译时就决定的是静态的,我们今天要了解的是多态的动态形式,比如我们在12306购买车票时,作为成人是原价票,学生可以享受优惠,军人可以买到提前票!
多态的定义和实现
虚函数
在前面继承方面我们可能了解到了虚继承,那么什么是虚函数呢?虚函数和虚继承一样需要使用到virtual关键字,虚函数指的是被virtual修饰的成员函数,并且虚函数的出现是为了多态!
虚函数在类域内外声明和实现时,只需要在类域声明处加上即可!
注意: 静态变量static和virtual不可以同时修饰成员函数
class Base
{
public:// 虚函数virtual void Func(){//...}
};
构成多态的条件
- 需要存在继承关系,如基类和派生类
- 被调用的成员函数需要是虚函数,并且构成重写/覆盖关系
- 必须是基类的指针或者引用指向派生类对象进行调用函数
就拿我们12306买票来收,Person类和Student类,这两者是继承关系,我们需要他们的买票函数构成重写关系,重写关系就是在原本是隐藏关系的基础上给成员函数加上virtual关键字!
// 买票举例
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}
};class Student : public Person
{
public:virtual void Buy_Ticket(){cout << "买票-打折" << endl;}
};
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用!
// 买票举例
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}
};class Student : public Person
{
public:void Buy_Ticket(){cout << "买票-打折" << endl;}
};
那么最后我们需要满足最后的第三点,使用基类的指针和引用指向派生类对象,由于继承的规定,只有基类的指针和引用可以指向派生类对象,基类的对象可以赋值派生类对象(切片的原因),为了实现多态效果我们必须使用基类的指针或者引用指向派生类对象进行调用函数!
// 买票举例
//写法一:
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}void Test(){Buy_Ticket();}
};class Student : public Person
{
public:virtual void Buy_Ticket(){cout << "买票-打折" << endl;}
};int main()
{Person* p = new Student;p->Test();delete p;return 0;
}
这段代码由于继承关系,Student继承了Person的两个函数,其中Buy_Ticket()函数构成多态被重写了,但是Test()并没有,该函数在调用时this指针是Person*的类型,我们传入的对象是Student刚好构成多态的条件!
// 写法二:
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}
};class Student : public Person
{
public:virtual void Buy_Ticket(){cout << "买票-打折" << endl;}
};void Test(Person* p)
{p->Buy_Ticket();
}int main()
{Person* p = new Student;Test(p);delete p;return 0;
}
第二种写法同样构成的多态的第三个条件,但是我们需要注意只有在多态的情况下,Test函数才会去掉用Student:: Buy_Ticket(),如果没有构成多态,那么函数Test函数调用的内容由参数类型决定!也就是Person类型。
说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向派⽣类对象;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。
特殊情况:协变
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
析构函数的重写
怎么实现
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。
为什么实现
为什么基类中的析构函数建议设计成虚函数
防止内存泄漏:
如果不设计成虚函数
class A
{
public:~A(){cout << "~A()" << endl;}
};class B : public A
{
public:~B(){cout << "~B()->delete" << _p << endl;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
我们可以看到并没有释放B类中申请的内存!
class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A
{
public:virtual ~B(){cout << "~B()->delete" << _p << endl;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
override和final关键字
override
- 这个关键字和assert的作用差不多,但是override是在编译时检查,assert是在运行时检查
- override可以检查用户是否重写成功虚函数,如果重写失败,编译时就会报错
final
- 我们不希望派生类去重写基类的某个虚函数时,可以加上这个关键字,这样就无法重写了
重载/重写/隐藏的对比
纯虚函数和抽象类
纯虚函数
- 在虚函数的后⾯写上=0,则这个函数为纯虚函数
- 纯虚函数只需要声明即可,不用定义。注意:纯虚函数是可以定义的,只是没有必要
// 纯虚函数和抽象类
class Pumping_paper
{
public:virtual void using_feel() = 0;void Test(){using_feel();}
};class ManHua : public Pumping_paper
{
public:void using_feel() {cout << "蓬松" << endl;}
};class QingFeng : public Pumping_paper
{
public:void using_feel() {cout << "柔软" << endl;}
};int main()
{Pumping_paper* p1 = new ManHua;Pumping_paper* p2 = new QingFeng;p1->Test();p2->Test();delete p1;delete p2;return 0;
}
抽象类
- 只要类中存在纯虚函数都是抽象类
- 抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。
多态的实现
虚函数表指针
-
虚函数表是一个数组
-
这是数组存放的是指针
-
指针是函数指针
-
总结:存放虚函数指针的数组
/*下⾯编译为32位程序的运⾏结果是什么()
A.编译报错 B.运⾏报错 C.8 D.12*/
class A
{
public:virtual void Func(){//...}
protected:int _a = 1;char _b;
};int main()
{A a1;cout << sizeof(a1) << endl;return 0;
}
只要class存在虚函数,这些函数的指针都会存放在虚函数表中
- 相同类生成的多个对象共用同一种虚函数表
多态的实现
实现方式
class Person
{
public:virtual void Print(){cout << "全价" << endl;}virtual void Func(){//...}void Test(){Print();}protected:string _name = "欧阳";
};class Student : public Person
{
public:virtual void Print(){cout << "打折" << endl;}virtual void Func_S(){//...}
protected:int _id = 1;
};class Child : public Person
{
public:virtual void Print(){cout << "免费" << endl;}
protected:int _age = 6;
};int main()
{Person* p1 = new Person;Person* p2 = new Student;Person* p3= new Child;p1->Test();p2->Test();p3->Test();delete p1;delete p2;return 0;
}
类中虚函数的指针都会被存放在虚函数表中,派生类会额外开辟一块空间拷贝基类的虚函数表,然后将可以重写的虚函数地址更换为派生类的虚函数
动态绑定与静态绑定
- 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
- 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
静态绑定
动态绑定
虚函数表的存储地址
虚函数和普通函数一样存放在栈区域
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public :// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
可以看出虚函数表在vs2022是被编译器存放在常量区中 !