- 一、多态的概念
- 二、多态的实现
- 虚函数
- 虚函数的重写
- 构成多态的条件
- 虚函数重写的两个例外
- 1.构成协变
- 2.析构函数的重写
- final和override关键字的介绍
- 重载、重写、隐藏(重定义)的区别
- 三、抽象类的简单介绍
- 纯虚函数
- 抽象类
- 接口继承
- 四、多态原理剖析
- 虚函数表
- 虚函数表的概念
- 单继承和多继承中的虚函数表
- 静态绑定和静态绑定
多态字面意思就是多种形态,实际上多态就是要完成同一种动作,不同的对象去调用相同的函数,会有不一样的形态,这就是多态。
-
举个栗子:
-
我们出行的时候需要提前购买火车票或者飞机票,那么不同类型的人买票就会享受不同的待遇。比如普通人买票是全价票,老年人及残疾人优先买票,儿童半价买票。这个例子就很好的展示了多态,同样是人,不同的人买票享受的待遇不尽相同。
多态的实现离不开虚函数,那么什么是虚函数呢?
所谓的虚函就是在普通函数的开头加上关键字virtual的函数。
例:
class A { public: //普通函数 void func1(){} //虚函数 virtual void func1(){};//在开头加了关键字virtual };虚函数的重写
多态的实现的关键就是派生类实现了堆基类虚函数的重写,那么什么是虚函数的重写呢?
- 虚函数的重写:派生类中存在一个虚函数与基类中的某个虚函数的函数名,参数列表,返回值完全相同(协变除外),就叫派生类的虚函数重写了基类的虚函数。
例:
class Person { public: //虚函数 virtual void buyticket() { //基类的具体实现 cout<<"普通人全价买票"<public: //虚函数 virtual void func() { //派生类的具体实现 cout<<"儿童半价买票"< 构成多态的条件 1.必须通过基类的指针或引用去调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
例:
class Person { public: virtual void buyticket()const { cout << "正常买票,全价!" << endl; } }; class child:public Person { public: virtual void buyticket()const { cout << "儿童买票,半价!" << endl; } }; class soldier:public Person { public: virtual void buyticket()const { cout << "军人买票,优先,全价!" << endl; } }; //多态的两个要求: //1.子类虚函数重写父类虚函数,重写规则是满足函数名、参数、返回值都相同,重写必须是虚函数,不能是普通函数 //2.必须是父类的指针或者引用调用虚函数 void buyticket(const Person& p)//基类的引用调用虚函数 { p.buyticket(); } void buyticket(const Person* p)//寄了的指针调用虚函数 { p->buyticket(); } void testbuyticket() { Person p; child c; soldier s; Person* p1 = new Person; child* c1 = new child; soldier* s1 = new soldier; //传参调用函数 引用 cout << "基类的引用调用虚函数" << endl; buyticket(p); buyticket(c); buyticket(s); cout<运行结果:
可以观察到父类的指针和引用调用虚函数都是可行的,且结果一致,实现了多态。
思考:为什么必须是基类的指针或者引用去调用虚函数呢?不可以直接用基类对象去调用虚函数吗?
我们先来看一下用基类对象直接调用虚函数的结果到底能否实现多态
class Person { public: virtual void buyticket()const { cout << "正常买票,全价!" << endl; } }; class child:public Person { public: virtual void buyticket()const { cout << "儿童买票,半价!" << endl; } }; class soldier:public Person { public: virtual void buyticket()const { cout << "军人买票,优先,全价!" << endl; } }; //将参数换成基类对象 让基类对象去调用虚函数 void buyticket(const Person p) { p.buyticket(); } void testbuyticket() { Person p; child c; soldier s; cout << "通过基类调用虚函数" << endl; buyticket(p); buyticket(c); buyticket(s); cout<可以看出通过基类对象调用虚函数不可以实现多态!!!
解析:
如果一个对象存在虚函数,那么其成员模型中就会存在一个虚函数指针,指向一张虚函数表(存放虚函数的地址),当一基类指针指向一个派生类对象,通过该基类指针去调用虚函数的时候,就会去该指针指向的对象的虚函数表中找调用函数的地址,完成函数调用,这就是多态的大致实现原理。(引用调用也是如此,引用的底层也是指针)
class A { public: virtual void func() { cout << "A:;func()" << endl; } }; class B :public A { public: virtual void func() { cout << "B::func()" << endl; } }; void test() { A a; B b; }对象内存成员模型:
但是如果是通过基类对象去调用虚函数,当这个基类是由派生类对象赋值来的时候,那么这里就存在切片,但是这里的切片不会将派生类的虚函数表指针也切出来赋值给基类对象的虚函数表指针的。因为如果将派生类的虚函数表指针也切出来拷贝给基类,会造成混乱,基类对象中到底是基类的虚表指针还是派生类的虚表指针都有可能,那么通过基类指针调用虚函数的时候就会混乱
虚函数重写的两个例外 1.构成协变
- 虚函数的重写涉及协变的时候就不需要派生类和基类的虚函数的返回类型相同,但是基类返回值类型得是基类的指针或引用,派生类的返回值类型得是派生类的指针或引用才行!!!
class A{}; class B : public A {};//继承自A class Person { public: virtual A* func()//基类的虚函数返回值类型是A* { return new A; } }; class Student : public Person { public: virtual B* func() //派生类的虚函数返回值类型为B* { return new B; } }; //A B中的func构成重写,因为其只有返回值类型不同,但是返回值类型是构成父子关系的指针,满足协变2.析构函数的重写如果基类的析构函数是虚函数,此时只要派生类的析构函数定义无论是否加关键字virtual(派生类中函数与基类中的虚函数重名那么该函数就算不写关键字virtual也会默认继承基类的虚函数属性,但最后还是写上),都会与基类的析构函数构成重写,因为编译器会对析构函数的名字左特殊处理,会将基类和派生类的析构函数的名字统一处理成distructor,从而满足了重写的条件构成重写。
class Person { public: virtual ~Person()//析构函数为虚函数 { cout << "~Person()" << endl; } }; class Student :public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; void test() { Person p; Student s; Person* pp = &p; Person* sp = &s; cout << "测试开始:->" << endl; pp->~Person(); sp->~Person(); cout << "测试结束!以下是对象出作用域自动调用的析构函数清理资源!" << endl; }final和override关键字的介绍final关键字用来修饰函数的时候,则该虚函数不能被再重写。(final 是为了防止重写,一般出现在基类中)
class Person { public: virtual void func()final {} }; class Teacher:public Person { public: //程序编译不通过,因为基类中的func函数被final 修饰,表示不可以被重写!!! virtual void func() {} };c++为了帮助程序员检测子类的是否对父类的虚函数进行重写,提供了override关键字。(override是 为了检查重写,一般出现在派生类中)
class Person { public: virtual void func() {cout<<"Person::func"} }; class Teacher:public Person { public: //这样写程序编译不通过,参数列表与基类不同,不构成重写!!!会被override检查出来 virtual void func(int a)override {} //下面才是正确的 virtual void func()override {cout<<"Teacher::person"} };重载、重写、隐藏(重定义)的区别以下面的思维导图总结:
三、抽象类的简单介绍 纯虚函数抽象类的判定标准就是一个类中有没有纯虚函数,有纯虚函数的类就是抽象类,否则就不是。那么什么是纯虚函数呢?
- 纯虚函数:在虚函数的后面加上=0的函数就是纯虚函数。
virtual void func()=0;//该虚函数就是纯虚函数抽象类包含纯虚函数的类就叫抽象类(或者接口类),抽象类不能实例化出对象,派生类继承后也不能实例化出对象,必须重写类纯虚函数后才可以实例化出对象,纯虚函数规范了派生类必须重写,纯虚函数更体现出了接口继承。
class Person//抽象类 { public: virtual void describe()=0;//纯虚函数 }; class Man:public Person { public: virtual void describe()//对抽象类中的describe函数进行重写 { cout<<"强壮,顶天立地"<接口继承public: virtual void describe()//对抽象类中的describe函数进行重写 { cout<<"贤惠,温柔"< //重写了纯虚函数可以实例化 Man boy; Women girl; } 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的
继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所
以如果不实现多态,不要把函数定义成虚函数下面这道题考查的就是接口继承的知识。
class A { public: virtual void func(int val = 1) { cout << "A->" << val << endl; } //virtual void test() void test() { func(); } int a = 10; }; class B:public A { public: virtual void func(int val = 0)//父类中的函数带了virtual子类重写可以不写virtual一样可以实现重写,因为会默认继承父类属性 { cout << "B->" << val << endl; } }; void test() { B* p = new B; p->test(); //这里的输出结果会是B->1 //解析 这里的p->test 中p是B* 类型,test中传过去的指针类型就是B* 但是这里类B中只是继承了A中的虚函数test而已,没有重写 //那么这里就是函数的接口继承,只会将virtual void test(A* this)继承下来,函数的实现是不会继承下来的,那么这里继承下来 //this指针就是父类A的指针 A* 类型,而我们调用的时候传的实参是B* 类型 那么这里就会有父类指针接收子类对象指针,构成多态 //那么这里的形参就是A* 本质是B* 因为多态就调用的时候回去调用B中的fucn ,往func里传参的时候this指针就是 A* //但是打印的val是原来父类中的val 为1,因为this指针是A* }四、多态原理剖析 虚函数表先来看一个问题:下面A的大小是多少?
class A { public: virtual void func(){} }; void test() { cout<虚函数表的概念
根据前面学类和对象的知识知道类中的函数是不会存在类中的,只有成员变量才会,结合这个知识点,我们可能会觉得这里的A的大小为0。实则不是,这里的答案是4,因为类A中有一个虚函数,那么类就得有个虚函数表指针指向一个虚函数表,A的对象可以通过虚函数表指针指向的虚函数表里找对应的虚函数地址。所以类A的大小为4(这里是在32位平台下,在64位平台下就是8)
回忆:如果上面类里的不是虚函数,而是普通函数,并且没有成员变量,那么类的大小就是1。C++编译器不允许对象为零长度。试想一个长度为0的对象在内存中怎么存放?怎么获取它的地址?为了避免这种情况,C++强制给这种类插入一个缺省成员,长度为1。如果有自定义的变量,变量将取代这个缺省成员。
虚函数表是一张存放虚函数地址的表,有虚函数的对象都有一个虚函数表指针指向自己的虚函数表。构成多态的时候,父类指针或者引用会到其指向或引用的对象的虚函数表中找对应的函数的地址进行调用,实现多态。
单继承和多继承中的虚函数表单继承
//每个对象只要有虚函数就会有一个虚函数指针表,用来存放虚函数的地址,虚表是存在常量区/代码段的!!! class A { public: virtual void func1() { cout << "A::func1()" << endl; } virtual void func2() { cout << "A::func2()" << endl; } void func3() { cout << "A::func3()" << endl; } int _a=1; }; class B:public A { public: virtual void func2() { cout << "B::func2()" << endl; } virtual void func4() { cout << "B::func4()" << endl; } int _b=2; }; typedef void (*Fp)();//重定义函数指针 void printfunc(Fp* p) { for (int i = 0; p[i] != nullptr; i++) { printf("%pn", p[i]); Fp f = p[i]; f(); } } void test() { A a; B b; //虚函表里放的是函数的地址,那么虚函数表就相当于一个函数指针数组,虚函数表指针指向该数组。 //一个对象的虚表指针始终是放在对象成员模型的第一个,那么把这四个字节取出来再转化为函数指针数组的指针就可以通过这个函数指针数组的指针调用printfunc()函数 遍历函数指针数组,并通过函数指针调用函数! printfunc((Fp*)(*((int*)(&b))));//单继承的情况!!! 下面有多继承,就会有多个虚表指针 }a对象的内存模型:
b对象的内存成员模型:
运行结果:
补充:派生类的虚函数表是先拷贝基类的虚函数表,如果有对基类的虚函数进行重写就用重写的虚函数的地址去覆盖虚函数表里继承自基类的虚函数地址,再将自己独有的虚函数地址放到虚函数表里面,生成自己的新的虚函数表。
多继承
class A { public: virtual void func1() { cout << "A::func1()" << endl; } virtual void func2() { cout << "A::func2()" << endl; } int _a=1; }; class B { public: virtual void func3() { cout << "B::func2()" << endl; } virtual void func4() { cout << "B::func4()" << endl; } int _b=2; }; class C:public A,public B//多继承 { public: virtual void func2() { cout << "C::func2()" << endl; } virtual void func4() { cout << "C::func4()" << endl; } virtual void func5() { cout << "C::func5()" << endl; } int _c = 3; }; typedef void (*Fp)(); void printfunc(Fp* p) { for (int i = 0; p[i] != nullptr; i++) { printf("[%d]:%pn",i, p[i]); Fp f = p[i]; f(); } } void test() { C c; printfunc((Fp*)(*((int*)(&c))));//多继承,有多个虚表指针 cout << endl; printfunc((Fp*)(*(int*)(((char*)&c) + sizeof(A)))); }c对象的内存成员模型:
C类继承了A 和 B 那么其会继承A 和 B的成员即虚函数,C对象的成员模型中第一个成员是基类A的虚函数表指针,第二个成员是A类中的_a;第三个成员是基类B的虚函数表指针,第四个成员是B类中的 _b,第五个成员是自己类中的 _d。
其中基类A的虚函数表中有A中的func1(),C类中重写了A类中的虚函func2(),故这里会对原本的A中的func2()进行覆盖,C中还有一个自身的非继承而来的虚函数func5(),也会被放到第一张虚函表中;第二张虚函数表是继承自类B的虚函数表,里面放了B中的func2()和func4(),但是C类中重写了B中的func4(),所以会在虚函数表中进行覆盖,变成C中的func4()。
运行结果:
静态绑定和静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数
重载- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用
具体的函数,也称为动态多态待补内容:菱形继承、菱形虚拟继承中的多态
s