https://blog.csdn.net/m0_63488627/article/details/130106690?spm=1001.2014.3001.5501https://blog.csdn.net/m0_63488627/article/details/130106690?spm=1001.2014.3001.5501
目录
1. 多态的概念
2.多态的实现
1.虚函数
2.多态条件
得到的多态条件
特殊条件
3.虚函数析构函数
4.override和final
3.抽象类
1.概念
2.接口继承和实现继承
4.多态的原理
1.虚函数表
2.父子类虚表关系
3.多态的实现原理
1.静态绑定和态绑定
2.父子在多态调用的原理
3.虚表重要记忆点
5.单继承和多继承的多态
单继承
多继承
菱形继承
菱形虚拟继承
1. 多态的概念
1.概念
多态:就是多种形态,传不同的对象处理,会出现不同的状态
例子如,学生买票半价,普通人买票全价,军人优先买票
辨析:函数重载是通过传入不同的参数,随后能调用不同的同名函数;而多态是面对不同的对象出现不同的行为进行处理
2.多态的实现
1.虚函数
class Person { public: //虚函数 virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student :public Person { public: //虚函数 virtual void BuyTicket() { cout << "买票-半价" << endl; } };
虚函数构成重写,也叫做覆盖;
三同:函数名相同,参数相同,返回值相同。
如果不是虚函数,则两个函数构成隐藏;
2.多态条件
得到的多态条件
1.类继承
2.虚函数重写
3.父类指针或者引用调用虚函数
void Func(Person& p) { p.BuyTicket(); } void Func(Person* p) { p->BuyTicket(); }
比较
普通调用:跟调用对象类型有关,传入对象,运行的是对象中的函数
多态调用:与指向对象有关,指向父类调用父类虚函数,指向子类调用子类虚函数
void Func(Person p) { p.BuyTicket(); }
特殊条件
1.子类可以不加virtual关键字,只要父类有virtual那依然是虚函数
2.协变:三同中,返回值可以不同,都是要求返回值为一个父子类关系的指针或者引用
此外,传出其他类的父子关系的指针或者引用也可以。
破坏多态条件:
1.父类没有virtual那就破坏了虚函数,调用为普通调用
2.取消引用和指针,就算是虚函数也会变为普通调用
3.虚函数析构函数
推荐:在继承中,将析构函数作为虚函数
原因:
class Person { public: ~Person() { cout << "Person delete:" << _p << endl; delete _p; } protected: int* _p = new int[10]; }; class Student :public Person { public: ~Student() { cout << "Student delete:" << _s << endl; delete _s; } protected: int* _s = new int[10]; };
1.如果是传统的,对象的作用域结束调用析构,父子类都没什么问题。
2.但是如果我们用到了指针,那么会出现问题,其调用就是普通调用,不会指定全部删除;因为析构函数父子同名重定义,那么当传入指针进行析构,那自然是普通调用,父类普通调用还好,可能能清除;但是子类使用父类的指针或者引用则一定只调用了父类的析构。
3.所以,析构函数作为虚函数,那么指针析构,指向的是父类就父类析构,子类就子类析构
修改:
class Person { public: virtual ~Person() { cout << "Person delete:" << _p << endl; delete _p; } protected: int* _p = new int[10]; }; class Student :public Person { public: virtual ~Student() { cout << "Student delete:" << _s << endl; delete _s; } protected: int* _s = new int[10]; };
满足三同原则,为重写,指向父类调父类,指向子类调子类
结论:写继承,无脑给父类的析构写virtual关键字
4.override和final
如何实现出一个不被继承的类
1.构造函数设为私有;原因是子类需要父类的构造但是父类的构造设为私有无法访问;那么就无法被继承了 -- 此外析构私有不太行,虽然不能直接构造,但是可以new出来指针,绕过了判断
2.类定义时加final关键字,该类称为最终类
class A final { };
此外,final也可以修饰虚函数,这样函数就不能被重写。
class A final { virtual func final(); };
override:检查函数是否完成了重写
class A { virtual func final(int); }; class B : public A { virtual func final() override; };
3.抽象类
1.概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数;包含纯虚函数的类叫做抽象类。
1.抽象类不能实例化出对象
2.派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
在我看来,抽象类算是一种模板,如果想继承该模板,必须重写虚函数后可以实例化。那么现实中一个不存在的概念性的对象,可以用抽象类定义。
2.接口继承和实现继承
1.普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
2.虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
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; } A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
p调用的test()是B从A中继承的,那么test中调用func函数,是this指针调用的,this是test的指针即A的指针,但是A指针是p切割后得到的,那么func是满足虚函数要求,调用func时是B的函数是多态情况,但是要知道虚函数是接口继承,可认为A中func函数被替换为B中的,但是val=1没有变,所以答案选B
4.多态的原理
1.虚函数表
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; }; int main() { cout << sizeof(Base) << endl; return 0; }
虚表指针指向虚表,虚表存放虚函数的地址,有几个虚函数就有几个虚函数地址存储
2.父子类虚表关系
1.父类结构其实跟上面说的一样,一个虚表指针指向虚表,虚表里有虚函数地址
2.子类继承了父类,那子类中的父类有虚指针,但是虚表的内容发生变化;拷贝父类,将完成重写的虚函数进行覆盖,覆盖原来指向
3.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
3.多态的实现原理
1.静态绑定和态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载,普通调用
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。静态调用:就是编译器能确定调用的函数,以至于它call的是函数的地址
动态调用:编译器看不出来,需要算出函数的地址,再调用之
2.父子在多态调用的原理
1.对于一个指针指向无论是父类还是子类,指针能读到的都是父类的那一部分
2.父类的虚表就是上面说过的那样的,子类是覆盖了虚表
3.调用方式都是取虚表指针,在虚表中找到虚函数的地址取实现调用
4.也就是说,多态的实现取决于虚表
3.虚表重要记忆点
1.虚表放在常量区/代码段
2.同一个类型的虚表是一样,不同类型的虚表是不一样的
5.单继承和多继承的多态
单继承
class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int _a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int _b; }; typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[],int num) { cout << " 虚表地址>" << vTable << endl; for (int i = 0; i<num; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i]; f(); } cout << endl; } int main() { Base b; Derive d; VFPTR * vTableb = (VFPTR*)(*(void**)&b); PrintVTable(vTableb,2); VFPTR* vTabled = (VFPTR*)(*(void**)&d); PrintVTable(vTabled,3); return 0; }
此时我们发现:子类没有构成虚函数的函数也在虚表中
多继承
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; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << 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; }
1.多继承,只有有虚函数的父类,才会生成虚表
2.子类自己的函数,存在第一个虚表中
菱形继承
其模型就是菱形继承的虚函数,其实就是D中有BC;而BC还要A的虚表,所以BC自带两个虚表
菱形虚拟继承
1.如果是菱形虚拟继承,则D一定要写虚函数;因为如果不写,A虚函数不知道写在B表还是C表,其存在二义性编译器看不懂。
2.D类会多一个虚表
3.B的第一个表是虚表,第二个是虚基表,C亦是如此
4.虚基表的开头找的是虚函数的偏移量。
class A{
public:
A(char *s) { cout<<s<<endl; }
~A(){}
};
class B:virtual public A
{
public:
B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A
{
public:
C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C
{
public:
D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
{ cout<<s4<<endl;}
};
int main() {
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}
A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D
1.A只有一个,所以A只构造一次
2.优先A构造
3.初始化列表优先于括号内,所以D最后
4.构造按继承顺序,即使先C(s1,s3),B(s1,s2) 也是B先输出,C再输出