
目录一、多态的概念二、多态的定义及实现2.1 多态的构成条件2.2 虚函数2.3 虚函数的重写2.3.1 虚函数重写的两个例外2.4 override和final2.41 override关键字2.42 final关键字2.5 重载重写重定义的区别三、抽象类四、多态的原理4.1 虚函数表4.2 多态原理4.3 静态绑定和动态绑定五、单继承多态和多继承多态5.1 单继承多态5.2 多继承多态一、多态的概念多态的概念简单说就是多种形态具体的就是不同对象做同一种行为时会产生不同的结果举个例子在我们买火车票时有一个学生票和成人票我使用学生身份和成人身份购买同一趟班次时学生票会有优惠成人票没有。此时学生和成人就是两个不同的对象他们做了同一种行为买票产生了不同的结过学生票优惠成人票原价二、多态的定义及实现2.1 多态的构成条件多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。那么在继承中构成多态还有两个条件1、必须是通过父类的指针或引用去调用虚函数2、被调用的函数必须是虚函数且该虚函数必须在子类中重写2.2 虚函数虚函数就是被virtual修饰的类成员函数下列代码中BuyTicket()就是虚函数这里func中就是通过父类的引用进行的调用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 p; Student sd; func(p); func(sd); return 0; }2.3 虚函数的重写虚函数重写就是在子类中有一个与父类完全形同的虚函数子类虚函数与父类虚函数的返回值函数名参数类型都相同。2.3.1 虚函数重写的两个例外上面介绍了虚函数重写的条件但有两个例外1、协变虚函数返回值类型不同协变父类虚函数的返回值与子类虚函数的返回值不同这里的不同指的是当父类返回值是另一个父类的指针或引用而子类的返回值是对应子类的指针或引用时依然构成多态。下面代码中父类Person的虚函数的返回值为A*子类Student的虚函数的返回值为A的子类B的指针此时构成多态。注意这里的返回值不可以是Person类和Student类的指针或引用class A {}; class B : public A {}; class Person { public: virtual A* f() { return new A; } }; class Student : public Person { public: virtual B* f() { return new B; } };2、另一个是对析构函数的重写父类和子类的析构函数的函数名不同但依然构成多态且在一些情况下必须重写析构函数下面代码中定义了两个父类对象让他们分别指向Person和Student类此时如果不对析构函数进行重写就会出现下方第二张图的结果不会出现调用子类Student类的析构函数。这是因为p1和p2是父类的指针对于普通函数编译器只看指针类型所以不会调用子类析构而加上virtual成虚函数后编译器会看指针指向的对象那么此时就可以调用子类的析构了下方第一张运行结果。class Person { public: virtual ~Person() { cout ~Person()endl; } }; class Student:public Person { public: virtual ~Student() { cout ~Student endl; } }; int main() { Person* p1 new Person; Person* p2 new Student; delete p1; delete p2; return 0; }2.4 override和final2.41 override关键字该关键字的作用是检查子类对父类的重写函数是否是虚函数下方代码中我们在子类Benz的重写函数Drive()后加了一个override关键字但我们没有加virtual因此这里会直接报错2.42 final关键字在我们写代码中会有一些情况使得一个类不允许其他类继承它这时有两个方法可以使用1、将该类的构造函数私有化下面代码中我们不想让其他类继承A类此时我们将A的构造函数私有化后 此时想像平常一样创建A对象和继承它的B对象时就会报错。class A { private: A(){} }; class B :public A {}; int main() { A aa;//报错 B bb;//报错 }上面方法确实可以使其他类无法继承但同样的A类也无法创建对象了那这有什么用呢。此时我们只要在A类中创建一个静态的A类函数Createobj然后返回A的构造函数就可以了那为什么静态成员函数可以呢首先如果不加static则是普通函数普通函数想要被调用就需要创建对象但创建对象就需要调用构造函数但类外又不可以访问私有函数所以会先入死循环而这里我们加上static后就可以通过A成员函数 的方式访问A类。class A { public: static A Createobj() { return A(); } private: A(){} }; class B :public A {}; int main() { A::Createobj(); B b; }2、使用final关键字只要在类名后加一个final那此时该类就无法被继承如果在父类的虚函数名后加一个final则该函数无法被子类重写2.5 重载重写重定义的区别分别在子类和父类的作用域中的两个函数名相同的函数不是重写就是重定义。三、抽象类在虚函数的后面加上 0 此时该函数就变成了纯虚函数纯虚函数所在的类就叫做抽象类接口类抽象类不可以实例化对象。下面代码中在父类A的虚函数func1后加了一个0此时func1就是纯虚函数A类就是抽象类并且它的子类也不可以实例化对象了。class A { public: virtual void func1() 0; }; class B :public A { public: }; int main() { A* a new A;//报错 A* a new B;//报错 }当我们在子类函数中对父类的纯虚函数进行重写后父类依然无法实例化对象但子类可以实例化对象了。class A { public: virtual void func1() 0; }; class B :public A { public: virtual void func1() { cout B::func1() endl; } }; int main() { A* a new A;//报错 A* a new B;//不报错 }抽象类虽然无法实例化对象但可以作为形参进行调用下面代码中创建了Func函数将A指针作为参数类型此时我们调用Func函数时传入哪个子类对象就会调用哪个子类对象class A { public: virtual void func1() 0; }; class B :public A { public: virtual void func1() { cout B::func1() endl; } }; class C :public A { public: virtual void func1() { cout C::func1() endl; } }; void Func(A* a) { a-func1(); } int main() { Func(new B); Func(new C); }注意因为父类无法创建对象而虚表是在初始化列表前创建的后面会说到所以父类没有虚表但子类有。四、多态的原理4.1 虚函数表前面多次提到了虚函数但虚函数实际的存储方式与普通函数不同我们先看一下下方的代码下方代码中我们将BuyTicket虚函数进行了重写在main函数中实例化了它的一个对象p1此时在监视窗口中我们可以看到一个新的名字vfptr它的名字叫虚函数指针v时virtualf是functionptr代表指针每个虚函数都要存放在虚函数表虚表中而虚函数表实际上就是一个存放虚函数指针的一个指针数组。我们根据这个虚函数指针的地址在内存中可以找到对应的虚函数表下方的内存1中该地址中存放的是重写的Buyticket虚函数在虚函数表的最后一般会以0nullptr的意思收尾如果我们在修改代码后没有进行重写编译最后一行的0是不会出现的。class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } virtual void func1() { } virtual void func2() { } int _a; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票-半价 endl; } int _b; }; int main() { Person* p1 new Person; return 0; }注意同类型的对象共用一个虚函数表。class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } virtual void func1() { } virtual void func2() { } int _a; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票-半价 endl; } int _b; }; int main() { Student sd1; Person* pp1 sd1; Person* pp2 sd1; Person* pp3 new Person; Person* pp4 new Person; return 0; }上面代码中我们声明了四个Person指针让pp1和pp2指向子类Student让pp3和pp4指向父类Person。此时在监视窗口中我们可以看到pp1和pp2的虚函数指针的地址是相同的而pp3和pp4的虚函数指针地址是相同的这就进一步验证同一个类型的对象共用一个虚函数表。4.2 多态原理我们前面提到了形成多态的条件其中一个是必须是通过父类的指针或引用去调用虚函数那为什么不能是普通调用下方代码我们声明了一个父类对象p2一个父类指针p1一个父类引用p3让他们分别调用子类。运行后我们在监视窗口可以看到父类对象p2调用子类时没有将虚函数重写依然是父类的。所以我们可以验证上方的结论是正确的。class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } virtual void func1() { } virtual void func2() { } int _a; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票-半价 endl; } int _b; }; int main() { Student sd; Person p2 sd; Person* p1 sd; Person p3 sd; return 0; }上方我们通过代码验证了必须是父类指针或引用的调用那此时我们再具体看一下子类中包含了哪些内容。这里我们声明了一个父类对象p和一个子类对象sd通过这种方式来看一下父类和子类里都有什么。class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } virtual void func1() { } virtual void func2() { } int _a; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票-半价 endl; } int _b; }; int main() { Student sd; Person p; return 0; }通过观察这个代码的监视我们可以看到父类p)中包含着一个虚函数指针和一个常量_a子类(sd)中将父类对象中的内容继承下来然后将没有重写的函数和变量放到后面。这里子类重写的步骤是1先将父类的虚函数表复制一份然后拷贝到子类的虚表中。2如果子类中对父类中的某个虚函数进行了重写那么子类的该虚函数会直接覆盖父类的虚函数。我们知道指针可能存放在栈或静态区那虚函数指针存放在哪里栈堆静态区数据段常量区代码段我们用下面的代码来验证一下这里的 *((int*)pr) 对pr取地址找到pr在内存中的地址然后强转为int*使这个指针指向虚函数表的前四个字节最后解引用取出这个int*指向的内存数据。这里使用int*强转的原因该对象在虚表中的前四个字节表示这个虚表的地址因此这里使用int*进行强转后会直接指向虚表的地址。class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } virtual void func1() { } virtual void func2() { } int _a; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票-半价 endl; } virtual void func3(){} int _b; }; int main() { Person pr; Student st; int a 10; printf(栈%p\n, a); int* p new int; printf(堆%p\n, p); static int b 1; printf(静态区%p\n, b); const char* ch hello world; printf(常量区%p\n, ch); printf(虚表1%p\n, *((int*)pr)); printf(虚表2%p\n, *((int*)st)); return 0; }上面的运行结果中我们可以看到常量区与两个虚表最为接近我们可以得出结论虚表为与常量区。4.3 静态绑定和动态绑定1静态绑定又称为前期绑定在程序编译期间确定了程序的行为也成为静态多态例如函数重载2动态绑定又称为后期绑定是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态五、单继承多态和多继承多态5.1 单继承多态上面我们看到的都是单继承多态但还有一个隐藏问题如果我们在子类中添加了一个虚函数但父类没有改虚函数那此时这个虚函数在哪里class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } virtual void func1() { cout Person::func1 endl; } virtual void func2() { cout Person::func2 endl; } int _a; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票-半价 endl; } virtual void func3() { cout Student::func3 endl; } }; typedef void(*FUNC_PTR)();//定义一个函数指针用来存放每个虚函数, 把 void(*)() 这种难写的函数指针类型简化成 FUNC_PTR //这里的FUNC_PTR可以当作虚表 void PrintVFT(FUNC_PTR* table) { //因为虚表中最后是以一串0也就是nullptr收尾的所以这里用nullptr用作结束条件 for (int i 0; table[i] ! nullptr; i) { printf([%d]:%p-, i, table[i]);//打印出每个虚函数的地址 FUNC_PTR f table[i]; f();//这里调用table[i]中的虚函数 } printf(\n); } int main() { Person p; Student st; int vft1 *((int*)p);//获取p虚表中的前四个字节也就是该虚表的地址 int vft2 *((int*)st); PrintVFT((FUNC_PTR*)vft1); PrintVFT((FUNC_PTR*)vft2); return 0; }我们使用上面的代码的作用就是打印出每个虚表中每个地址对应的虚函数可以看到func3实际只存放在子类的虚表中。5.2 多继承多态了解了单继承的多态多继承与单继承类似只不过此时在子类中定义的func3虚函数就要重新思考它的位置了是否还与单继承一样放到最后class Base1 { public: virtual void func1() { cout Base1::func1 endl; } virtual void func2() { cout Base1::func2 endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout Base2::func1 endl; } virtual void func2() { cout Base2::func2 endl; } private: int b2; }; //Derive大小只有整形4字节 class Derive : public Base1, public Base2 { public: virtual void func1() { cout Derive::func1 endl; } virtual void func3() { cout Derive::func3 endl; } private: int d1; }; int main() { Derive d; cout sizeof(d) endl; int vft1 *((int*)d);//获取第一个虚表的地址 Base2* b2 d;//这里会自动计算偏移量然后让b2指向Base2的初始位置 int vft2 *((int*)b2); PrintVFT((FUNC_PTR*)vft1); PrintVFT((FUNC_PTR*)vft2); }通过上面的代码及运行结果我们可以看到func3放到了第一个虚表的最后。这里我把子类对象的监视窗口也截出来了我们可以看到一个细节子类中对Base1和Base2中func1虚函数重写后结果是一样的都是Drieve::func1但他们两个的地址不一样。首先我们先要了解Derive类中每一部分所占用的内存大小是多少将上述代码中main函数修改为下方代码。我们来看一下整个运算过程。int main() { Derive dst; Base1* btr1 dst; btr1-func1();//断点 Base2* btr2 dst; btr2-func1();//断点 return 0; }通过监视窗口和反汇编的查看首先运行后进入到反汇编中运行到call eax后f11跳转到该地址中可以看到此时eax中存放的就是监视窗口中Derive中的func1的地址但此时实际存放的是jmp的地址再次进入后才会跳转到重写后的func1函数中。而Base2这个类就不一样了它的eax寄存器中存放的也是一个jmp的地址但这个jmp跳转后进入的是一个减操作这里先将ecx存放着this指针减八后再通过一次jmp跳转到Base1中的位置。所以通过两次分析我们知道了Base2中重写的func1函数与Base1中重写的func1函数的不同就在于Base2是先执行减操作后才跳转到func1调用的。原因上面我们先讨论的内存大小就是为了解释现在的情形。在eax调用前先要传一个this指针ecx这里传的是Base1/Base2的this指针对于Base1来说因为Base1的首地址与Derive的首地址相同所以Base1的this指针可以直接传过去但是对于Base2来说它的this指针指向的是Base2的首地址而在这个代码中Base2的首地址与Derive的首地址之间相差8个字节所以编译器会先通过减8使this指针指向Derive的首地址后再进行func1函数的调用。