多态基础知识
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态,并且多态的前提是在继承的关系下!
多态的条件:虚函数重写 + 父类的指针 / 引用 去调用虚函数
- 虚函数重写
父子继承父子的两个虚函数,三同(函数名,参数类型,返回值类型),并且派生类(子类)重写的虚函数可以不加 virtual,‘重写’,重写的是实现的部分。
- 三同
函数名相同,参数类型,返回值类型相同,并且有协同的情况:返回值类型可以不同,但父子类虚函数的返回值必须是父子关系的类的指针或引用。
为什么派生类重写的虚函数可以不加 virtual ?
这一切还得从父子类的析构函数说起。编译器为了保证父子类的析构函数正确调用,于是特殊处理,将父子类的析构函数都统一处理成 destructor ,使其能构成多态,即父子类对象在 delete 时能自己调用自己的析构函数。首先规定,父类的析构函数必须定义为虚函数,但会出现子类在写析构函数的时候忘加 virtual 或者未写析构还想保证能正确调用析构函数的情况。编译器就规定多态的子类可以不加 virtual。
例如为了解决下面的场景
class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() { cout << "~Student()" << endl; } }; int main() { Person* p1 = new Person; delete p1; p1 = new Student; delete p1; return 0; }
多态调用中子类虚函数重写时,会用父类的接口,来实现,即 “重写”,即子类的虚函数的函数名,缺省参数是按父类的来,如果子类的缺省参数的值与父类不同,则编译器会无视子类缺省参数的值,采用父类的使用;但如果子类缺省参数的类型与父类不同,那就是不构成多态了!!
最直观的就是这个例子的输出结果:
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 t = 0) { std::cout << "B->" << t << std::endl; } }; int main(int argc, char* argv[]) { B* p = new B; p->test(); return 0; }
C++11 override 和 final 修饰虚函数
final 修饰父类的虚函数,表示该虚函数不能被重写,也只有类中的虚成员函数才能被 final 修饰(注:final 修饰一个类表示这个类不能被继承)
override 修饰派生类的虚函数,检查派生类虚函数是否重写了某个虚函数。如果重写了,就正常编译;如果没有重写,编译器就会报错!
重载、重写、重定义的区别
他们的关系如下:重载必须在同一定义域内,与重写与重定义不同 ;重写的要求相较于重定义较多,并且他们是非此即彼的关系。
抽象类
当一个类中含有一个或多个虚函数,并且在虚函数后面写上 = 0,那么这个函数就叫纯虚函数,包含这个纯虚函数的类就叫抽象类!(只要包含纯虚函数,就叫纯虚类,就不能实例化出对象)
抽象类的特点:无法实例化出对象,子类继承后也不能实例化出对象;只有在派生类重写这个纯虚函数后,子类才能实例化出对象。纯虚函数规范了派生类必须重写,纯虚函数更体现了接口继承!
普通函数继承:实现继承
纯虚函数继承:接口继承
多态原理
只要一个类中有虚函数,那么这个类中就会有一个指针叫 虚函数表指针,指针指向一个虚函数表,这个表中存储着各个虚函数的地址(指针),而虚函数本身和其他函数一样,是存在代码段中的,只是它的地址又被存在了虚表(虚表也是存在代码段中)中。(在计算类大小的时候必须牢记要加上这个虚函数表指针的大小和内存对齐)
当子类中重写了虚函数,那么在子类对象中就会重建一个虚函数表,会先将虚表里的内容拷贝过去,在内容为改变的情况下,表里的内容是一样的,但两个类中虚表指针是不一样的。如果使用相同的指针会很混乱。
如果定义一个子类对象和父类对象的话,虚表指针如下:
已知虚函数表中存储的是各个虚函数的指针,通过这些指针去调用对应的虚函数,那么多态能通过指针和引用去实现的原因是什么?
父类对象的指针指向子类对象和父类对象引用子类对象的原理分别是父类对象的指针指向子类对象对应的那一部分和引用子类对象某部分的别名,这些操作也包含子类对象的虚函数表指针;但如果是拷贝的话,只会将子类对象的成员拷贝至父类对象中,不会拷贝虚函数表指针。
为什么不能通过拷贝顺便将子类对象的虚函数表指针拷贝过去?如果能这样做,可能会达不到指向父类调用父类,指向子类调用子类的效果。因为如果能拷贝,那么父类中存储的就不一定是父类的虚函数表,就乱了!而析构函数(也是虚函数)也在虚函数表中,如果能拷贝过去,甚至会出现析构错误的情况!!
继承中的虚函数表
在单继承的情况下,子类虚表会将父类虚表中的地址拷贝过去,然后将子类中多余的虚函数地址再放进它自己的虚函数表里。
由下面的图片可以看出,继承多个父类的子类中有两个虚表,子类中的新的虚函数地址会存储在第一个类的虚函数表里
通过下面的代码来验证:
class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } virtual void func6() { cout << "Base1::func6" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } virtual void func7() { cout << "Base2::func7" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int d1; }; typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) { cout << " 虚表地址>" << vTable << endl; for (int i = 0; vTable[i] != nullptr; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i]; f(); } cout << endl; } int main() { Derive d; VFPTR* vTableb1 = (VFPTR*)(*(int*)&d); PrintVTable(vTableb1); VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1))); PrintVTable(vTableb2); return 0; }
通过函数指针来打印虚函数表里的虚函数地址
即使在菱形继承中,编译器也将子类中所有虚函数的地址,放在第一个父类的虚表中了。
即 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中