一、多态概念
多种形态。
静态多态:编译时多态。(函数重载)
动态多态:运行时多态。(继承关系下,调用父类指针或引用,对于不同的对象有不同的行为)
二、多态的定义及实现
1)多态的构成条件
多态时一个继承关系下的类对象,去调用同一函数,产生了不同的行为。
实现多态的两个必须条件:
1.必须是父类的指针或引用调用虚函数。
2.被调用的函数必须是虚函数,子类必须对父类的虚函数重写/覆盖。
2)虚函数
易错点1:
virtual关键字只在声明时加上,在类外实现时不能加
易错点2:
static和virtual是不能同时使用的。
原因:static成员函数没有this指针,且virtual成员函数依赖于this指针来实现动态绑定。
易错点3:
重定义就是隐藏。
虚函数:类成员函数前加virtual修饰。注:非成员函数不能加virtual修饰。
虚函数的重写/覆盖:子类中有一个和父类完全相同的虚函数(参数列表,函数名,返回值)
例如:
cpp
#include <iostream> using namespace std; class Person { public: virtual void BuyTicket() {} }; class Teacher :public Person { public: virtual void BuyTicket() { cout << "老师,买全票" << endl; } }; class Student :public Person { public: virtual void BuyTicket() { cout << "学生,优惠买票" << endl; } }; void Buy(Person* ptr) { ptr->BuyTicket(); } int main() { Student s; Teacher t; Buy(&s); Buy(&t); return 0; }
其中,满足了多态,和ptr的类型无关,与ptr指向的对象有关。
注意:重写父类虚函数时,子类的虚函数可以不加virtual,也构成重写。
原因:继承后父类的虚函数被继承下来了在子类中保持虚函数属性,但这种写法不规范。
面试题:
以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
cpp
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; }
答案是B,输出结果为:B->1
分析:A类的func和B类的func构成重写,因为参数列表,返回值,函数名都一致,其中参数列表是否不同和缺省值具体是多少无关 ,B类中即使没加virtual,也没有问题,因为继承后父类的虚函数被继承下来在子类中仍旧保持虚函数属性。
编译器对于重写的函数的处理:
3)虚函数重写的一些其他问题
协变:
概念:子类重写父类虚函数时,与父类虚函数返回值类型不同。
不同指必须是:父类返回值返回父类的指针和引用,子类返回值返回子类的指针和引用。
可以是自身的继承关系,也可以是其他的继承关系。
测试代码:
cpp
#include <iostream> using namespace std; class A {}; class B :public A {}; class Person { public: virtual A* func() { cout << "Person::func()" << endl; return nullptr; } }; class Student :public Person { public: virtual B* func() { cout << "Student::func()" << endl; return nullptr; } }; class Teacher :public Person { public: virtual B* func() { cout << "Teacher::func()" << endl; return nullptr; } }; void f(Person* ptr) { ptr->func(); } int main() { Person p; Student s; Teacher t; f(&p); f(&s); f(&t); return 0; }
可以看到,用其他类的继承关系实现协变也可。
析构函数的重写
父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual,都构成重写。
原因:编译器对析构函数的名称特殊处理,编译后析构函数的名称统⼀处理成destructor。
为什么父类的析构函数建议设计成虚函数?
测试代码:
cpp
#include <iostream> using namespace std; class A { public: ~A() { cout << "~A()" << endl; } protected: int _a; }; class B :public A { public: ~B() { delete[] _b; cout << "~B()" << endl; } private: int* _b = new int[10]; }; int main() { B* b = new B; A* a = new B; delete b; delete a; return 0; }
结果为:可以明确看到,若A* a = new B; 这种情况,如果B类不重写A类的析构函数,那么就会导致delete a; 时,只调用了父类A的析构函数,没有调用子类B的析构函数,导致内存泄漏。
解决方法:重写父类A的析构函数。
4)重载/重写/隐藏的比较(同名函数的关系)
重载:
1.两个函数在同一作用域
2.函数名相同,参数不同(参数列表不同),返回值不限
重写/覆盖:
1.两个函数分别在继承体系的父类和子类作用域
2.函数名,参数,返回值必须相同,协变例外
3.两个函数必须都是虚函数
隐藏:
1.两个函数分别在继承体系的父类和子类作用域
2.函数名相同
3.父子类的成员变量名相同(成员变量方面)
三、纯虚函数和抽象类
定义方式:在纯虚函数后面加上=0。
cpp
class A { public: virtual void func() = 0; };
不需要定义实现(但在语法上可以实现,没有必要),只要声明即可。
抽象类:包含纯虚函数的类。
抽象类不能实例化出对象。
子类继承父类,若没有重写父类的纯虚函数,那么子类也是抽象类。(间接强制重写纯虚函数,不重写实例化不出对象)
四、多态的原理
下面编译为32位程序的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8 D. 12
cpp
class Base { public: virtual void Func1() { std::cout << "Func1()" << endl; } protected: int _b = 1; char _ch = 'x'; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }
答案是D,sizeof(b)是12字节。
原因,b对象中有_vfptr,虚函数表的指针,存在对象的起始位置,类型为void**,在32位环境下是指针是4字节,根据内存对齐规则,对齐后为12字节。
多态原理分析:
测试代码:
cpp
#include <iostream> using namespace std; class Person { public: virtual void func() { cout << "Person::func()" << endl; } protected: string _name; }; class Student :public Person { public: virtual void func() { cout << "Student::func()" << endl; } private: int _sid; }; class Teacher :public Person { public: virtual void func() { cout << "Teacher::func()" << endl; } private: int _tid; }; void f(Person* ptr) { ptr->func(); } int main() { Person p; Student s; Teacher t; f(&p); f(&s); f(&t); return 0; }
通过监视窗口可以看到,p,s,t对象各有一个虚函数表指针_vfptr(virtual function table 虚函数表指针),且指针的值不同,说明指向不同的空间,而且指向的内容函数指针不同,p对象为它的Person::func的函数指针,重写的s和t对象为各自重写的函数指针。
逻辑结构如图:
指向谁调用谁,运行时,到指向对象的虚函数表中找到对应虚函数的地址,进行调用,达到多态。
动态绑定和静态绑定
静态绑定:对不满足多态条件(指针或引用调用虚函数)在编译时确定,编译时确定调用函数的地址。
动态绑定:满足多态条件的函数调用是在运行时绑定,也就是运行时到指定对象的虚函数表中找到调用函数的地址。
虚函数表
易错点1:
虚函数表是在编译时生成的。
易错点2:
cpp
#include<iostream> using namespace std; class A { public: virtual void f() { cout << "A::f()" << endl; } }; class B : public A { private: virtual void f() { cout << "B::f()" << endl; } }; int main() { A* pa = (A*)new B; pa->f(); return 0; }
输出:B::f() 。
原因:即使B中重写的f()函数是私有的,但是已经构成重写了,把虚函数表中原有的A的f()函数指针覆盖了,最终调用的位置不变,只是执行函数发生变化 。访问权限的检查是基于指针的类型A而不是指向的对象实际类型B。
易错点3:
cpp
class A { public: A ():m_iVal(0){test();} virtual void func() { std::cout<<m_iVal<<' ';} void test(){func();} public: int m_iVal; }; class B : public A { public: B(){test();} virtual void func() { ++m_iVal; std::cout<<m_iVal<<' '; } }; int main(int argc ,char* argv[]) { A*p = new B; p->test(); return 0; }
输出结果为:0 1 2
分析:最开始,new B,开内存,调用B的构造,由于B继承A,初始化列表调用A的默认构造,**初始化列表m_iVal被初始化成0,然后调用test(),由于此时还处于父类对象构造阶段,多态机制还没有生效,调用的是父类A的func(),**打印0 。然后借着就是B的构造,调用test(),此时父类已经构造好了,func也实现了重写,满足多态机制,调用重写后的func,打印1 。然后A* p调用test(),符合多态机制,调用重写后的func,打印2 。
1)父类对象的虚函数表中存放所有虚函数的地址。同类型对象共用一张虚表,不同类型对象有各自独立的虚表,因此子类和父类有各自虚表。
2)子类由两部分构成,继承下来的父类和自己的成员。一般情况下,继承下来的父类中有虚函数表指针,自己就不会再生成虚函数表指针。
3)子类重写的父类的虚函数,子类的虚函数表中对应的虚函数就会被覆盖成子类重写的虚函数地址。
测试代码:
cpp
#include <iostream> using namespace std; class A { public: virtual void print() { cout << "A::print()" << endl; } virtual void funcA() { cout << "A::funcA()" << endl; } private: int _a; }; class B :public A { public: virtual void print() { cout << "B::print()" << endl; } virtual void funcB() { cout << "B::funcB()" << endl; } private: int _b; }; class C :public B { public: virtual void print() { cout << "C::print()" << endl; } virtual void funcC() { cout << "C::funcC()" << endl; } private: int _c; }; int main() { C c; c.funcB(); return 0; }
可以看到,在纯单继承体系中,只有一张虚函数表,存放所有继承体系的虚函数地址,其中被重写的虚函数只存放最终版(之前的都被覆盖了)。
4)子类的虚函数表中包含,父类的虚函数地址,子类重写(覆盖的虚函数地址),子类自己的虚函数地址。
测试代码:
cpp
#include <iostream> using namespace std; class A { public: virtual void func1() {} }; class B { public: virtual void func2() {} }; class C :public A, public B { public: virtual void func3() {} }; typedef void(C::* P)(); int main() { A a; B b; C c; P p = &C::func3; int n = 0; return 0; }
可以看到,多继承,C同时继承A和B,A和B各自独立有一个虚函数表指针,C自身的虚函数指针存放在第一个继承的类A的虚函数指针表中。
5)虚函数表本质是⼀个存虚函数指针的指针数组,一般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译器不会放)
6)虚函数和普通函数一样,存放在代码段的,编译后是一段指令,只是虚函数的地址又存到虚函数表中。
7)虚函数表存在哪里?C++没有规定,以下是一个测试程序,来测试虚函数表存在哪里。
测试代码:
cpp
#include <iostream> using namespace std; 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: // 重写基类的func1 virtual 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; }
可以看出Person虚表地址和Student虚表地址和常量区非常接近,可以推断出vs2022的虚表是存在常量区的。
可以看出Person和Student虚表地址和常量区最接近,可以推断出g++4.8.5的虚表存在常量区。