网站建设要托管服务器,设计师应该知道的网站,电子贺卡app,成都高标建设有限公司官方网站凡是面向对象的语言#xff0c;都有三大特性#xff0c;继承#xff0c;封装和多态#xff0c;但并不是只有这三个特性#xff0c;是因为者三个特性是最重要的特性#xff0c;那今天我们一起来看多态#xff01; 目录
1.多态的概念
1.1虚函数
1.2虚函数的重写
1.3虚… 凡是面向对象的语言都有三大特性继承封装和多态但并不是只有这三个特性是因为者三个特性是最重要的特性那今天我们一起来看多态 目录
1.多态的概念
1.1虚函数
1.2虚函数的重写
1.3虚函数重写的两个例外
1.子类的虚函数可以不加virtual
2. 协变(基类与派生类虚函数返回值类型不同)
1.4如何实现一个不能被继承的类
2. 多态的定义及实现
2.1多态调用
2.2普通调用
2.3析构函数建议加virtual吗
2.4抽象类
2.5接口继承和实现继承
3.多态的实现原理
3.1.虚表虚函数表
3.2多态的实现
3.3静态多态和动态多态
3.4单继承的多态实现
3.5多继承的多态实现
4.一些常考的多态的问题
总结 1.多态的概念
多态就是不同对象去完成某一种行为时产生的不同状态。
举例说明日常生活中我们去买票尤其买火车票时总会有不同的结果。当成人买的时候就是原价学生就是半价军人就是优先买票这都体现了多态。不同对象完成某一行为产生不同状态。
那实现多态前我们首先得清楚一些概念
1.1虚函数
虚函数即被virtual修饰的类成员函数称为虚函数。class A
{
public:virtual void func(){cout A endl;}
}; 1.2虚函数的重写 我们之前在继承的时候学习过重定义隐藏现在在多态这一节又出现了重写那到底是什么呢?我们来看一看 重定义隐藏首先在两个不同的类中父类子类构成继承只要函数名相同就会构成重定义隐藏。 重写覆盖在重定义的基础上除了函数名要相同还有返回值参数都得相同这才构成重写。 举个例子 class A
{
public:virtual void func(){cout A endl;}
};
class B:public A
{
public:virtual void func() //重写{cout B endl;}
}; 总结就是虚函数的重写条件子类和父类都是虚函数且函数名返回值参数都必须相同三同这才能构成虚函数的重写。 1.3虚函数重写的两个例外 1.子类的虚函数可以不加virtual 因为是继承关系父类的虚函数也被继承下来所以子类的可以不加virtual。建议还是都写上 class A
{
public:virtual void func(){cout A endl;}
};
class B:public A
{
public:void func(){cout B endl;}
}; 2. 协变(基类与派生类虚函数返回值类型不同) 意思是三同中返回值可以不同但要求返回值必须是父子类关系的指针或引用。其他父子类关系的指针或者引用也可以 class person
{
public:virtual person* func() //本父子类指针或引用{return this;}
};class student:public person
{
public:virtual student* func() //本父子类指针或引用{return this;}
}; 其他父子类关系的指针或者引用也可以 class A
{};
class B:public A
{};class person
{
public:virtual A* func(){return nullptr;}
};class student:public person
{
public:virtual A* func(){return nullptr;}
}; 但是父类的返回值不可以为子类的指针。 1.4如何实现一个不能被继承的类 方法一是需要把它的构造函数写为私有即可无法构造就不可能被继承 方法二类定义时加finalc11最终类不能被继承
class A final
{};
class B:public A
{}; 若final给虚函数虚函数则不能被重写 class A
{virtual void func()final{}
};
class B:public A
{virtual void func(){}
}; override是来判断是否已经重写检查重写 class A
{virtual void func(int){}
};
class B:public A
{virtual void func()override{}
}; 2. 多态的定义及实现 首先多态实现的前提必须是继承 多态实现的两个条件 1.必须使用父类基类的指针或者引用调用虚函数 2.被调用的函数必须是虚函数且子类派生类必须对虚函数进行重写 多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。比如Student继承了 Person。Person对象买票全价Student对象买票半价。 2.1多态调用 class Person
{
public:virtual void Buyticket(){cout Person:全价 endl;}
};class Student:public Person
{
public:virtual void Buyticket(){cout Student:半价 endl;}
};void func(Person p) //切片
{p.Buyticket();
}int main()
{Person p;Student s;func(p);func(s);
} 2.2普通调用 不符合多态条件即可 void func(Person p)//不是指针或者引用就是对象
{p.Buyticket();
}int main()
{Person p;Student s;func(p);func(s);
} 那么我们可以发现 普通调用跟调用对象的类型有关 多态调用必须是父类的指针或者引用无论是是哪个对象传他都会指向该对象中父类的那一部分切片进而调用该对象中的虚函数。一句话多态调用跟 指针/引用 指向的对象有关 2.3析构函数建议加virtual吗
我们看一个例子 class Person
{
public:~Person(){cout Person delete endl;delete _p;}
protected:int* _p new int[10];
};class Student :public Person
{
public:~Student(){cout Student delete endl;delete _s;}
protected:int* _s new int[20];
};int main()
{//Person p;//Student s;Person* ptr1 new Person;Person* ptr2 new Student;delete ptr1;delete ptr2;
} 我们都知道析构函数自动调用在继承中子类会先析构调用子类的析构函数以后自动再调用父类的析构函数。 但这用情况还适用吗 先看一下结果 我们发现居然调用了两次父类的析构函数 这种情况就会造成子类对象中的成员变量没有释放导致内存泄露 我们知道 delete有两种行为1.使用指针调用析构函数2.operator delete(ptr) 所以使用指针调用析构函数是普通调用不满足多态调用的条件普通调用是跟调用的对象类型有关类型都是Person所以只会调用person的析构函数 但此时我们更希望的是多态调用所以建议加virtual指针指向的对象是哪个就调用哪个的析构函数。但此时我们会想析构函数名字都不一样这能构成重写吗当然可以那是因为编译器会自动把父类子类的析构函数名字换成一样的ptr-destructor()。 那么就可以实现我们预期的效果 所以我们建议再写析构函数时可以无脑给父类的析构函数加virtual防止出现上面的情况导致内存泄露 。 普通调用时时普通调用父类的指针或者引用调用时时多态调用互不影响 2.4抽象类
在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口 类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。 class Car
{
public:virtual void Drive() 0;
};class BMW :public Car
{
public:virtual void Drive(){cout BMW-操控 endl;}
};
void Test()
{Car* pBMW new BMW;pBMW-Drive();
}int main()
{Test();
} 总结有些类不需要类的对象可以在写成纯虚函数。 2.5接口继承和实现继承 接口继承针对虚函数实现继承针对普通函数。 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实 现。 虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成 多态继承的是接口。 学了这么多来做一道题温习一下很坑
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: 以上都不正确若是ptr-func()就是B类对象直接调用就是普通调用普通调用跟对象类型有关。
普通调用在编译时就会静态绑定在编译时调用的函数以及函数的默认值就已经确定子类调用子类自己的函数跟父类没有任何关系函数都是子类编译时就已经静态绑定的所以缺省值依然是0。最终结果是B-0 答案选哪个 首先我们了解的第一点是继承父类的成员会原封不动的继承到子类 我们接下来看创建了一个B对象的指针指针来调用p-test()这时候会直接调用父类中的test再this-func()此时的this的类型是A*因为test处于A类中继承到B中也会原封不动的继承过去this依然是A*所以父类的指针调用虚函数满足多态的调用多态调用是看指针指向的对象又因为p调用的test所以指针指向B对象所以会调用B的重写的func虚函数所以最终答案是B-1.(其实多态调用一直是调的父类的接口再根据指向的对象去调用具体的实现后面会详细讲到 当B对象自己调用函数func时当不是多态调用时就会直接调用自己的func()缺省值还是自己的val0. 3.多态的实现原理 3.1.虚表虚函数表 来先看一道题 class Base1
{
public:virtual void Func1(){cout Func1() endl;}};class Base2
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};
sizeof(Base1),sizeof(Base2)它所占的字节数是多少 通过之前学习的内容我们可以了解到如果类中没有成员变量只有成员函数会留一个字节进行占位因为成员函数在代码段所以Base1的大小是1吗 原来不是我们想象的那样子是事实上来看 凡是有虚函数的都会有一个虚函数表指针来存虚函数简称虚表指针存虚函数的表叫做虚函数表简称虚表。 VFptr全程vftable是一个指针 指向虚表虚表中存的是虚函数的地址。 所以我们知道原来只要有虚函数就会有虚表指针所以Base1的字节大小是4字节 Base2的字节大小是加上内存对齐_b占四字节vtf占四字节8字节。 对于同一类实例化出的不同的对象他们的虚表是公用的 class A
{public:virtual void func(){}
}int main()
{A b;A c;
} 我们了解虚表和虚表指针以后那么多态到底如何实现呢 3.2多态的实现 来看一段代码 class Base
{
public:virtual void Func1(){cout Base::Func1() endl;}virtual void Func2(){cout Base::Func2() endl;}void Func3(){cout Base::Func3() endl;}private:int _b 1;char _ch;
};class Derive : public Base
{
public:virtual void Func1(){cout Derive::Func1() endl;}void Func3(){cout Derive::Func3() endl;}
private:int _d 2;
};int main()
{Base b;Derive d;//普通调用Base* ptr b;ptr-Func3();ptr d;ptr-Func3();
//多态调用ptr b;ptr-Func1();ptr d;ptr-Func1();
} 多态调用 ptr是父类的指针无论指向哪个对象都只能看到该对象父类的部分切片那么多态调用怎么调用呢通过虚表指针来调用虚函数完成重写的虚函数会在虚表对应的位置进行覆盖变成重写后的虚函数进而调用。一句话我也不知道我调用谁我指向谁就调用谁的虚函数进而完成动态绑定完成多态调用 静态绑定编译时通过类型就确定调用函数的地址然后直接call完成调用 通过反汇编可以看到 静态绑定一步完成动态绑定得很多步完成。 3.3静态多态和动态多态 1. 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态 比如函数重载 2. 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体 行为调用具体的函数也称为动态多态。 总结多态调用就是依靠虚表实现指向谁就调用谁的虚函数 虚表是存在代码段中的。 3.4单继承的多态实现 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; }void func4() { cout Derive::func4 endl; }
private:int b;
};int main()
{Base b;Derive d;} 我们知道Base对象中的虚表有func1和func2子类对虚函数进行重写func1重写func2不变 那么子类自己的虚函数func3在不在虚表里面呢 为了更方便观察我们可以实现一个打印虚表的函数 typedef void(*VFTptr)(); //函数指针重命名必须写到里面
void Print(VFTptr VFT[]) //函数指针数组
{int i 0;while (VFT[i]) //虚表中vs默认以空结束。{printf([%d]%p-, i, VFT[i]);VFT[i]();i;}cout endl;
}
int main()
{Base b;Derive d;Print((VFTptr*)(*(int*)b)); //先取地址再强转VFTptr的地址然后解引用取到地址再强转为VFT*类型进而传参调用Print((VFTptr*)(*(void**)d));//换为void**原因是因为机器若是32位指针大小就是4字节若是64位就是8字节//所以换为void**更普适先取地址再强转void**void*解引用那么这就根据机器的位数来决定指针的大小了
} 我们可以发现虚函数func3也会存在虚表中。 3.5多继承的多态实现 typedef void(*VFTptr)();
void Print(VFTptr VFT[])
{int i 0;while (VFT[i]){printf([%d]%p-, i, VFT[i]);VFT[i]();i;}cout endl;
}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;
};int main()
{Base1 b1;Base2 b2;Print((VFTptr*)(*(void**)b1));Print((VFTptr*)(*(void**)b2));Derive d;Print((VFTptr*)(*(void**)d));//Base1的虚表Print((VFTptr*)(*(void**)((char*)d sizeof(Base1))));//Base2的虚表//Base2* ptrd;//Print((VFTptr*)(*(void**)ptr)); //也可以这样找到虚表指针} 我们知道多继承下多态的实现子类继承多个父类只有当父类有虚函数多继承时才有虚表。 当子类也有虚函数时这时子类的虚函数放到第一个继承的父类的虚表中我们可以从上面代码结果看出。 再来练习题目 下列输出的结果是什么 class A{
public:A(char *s) { cout s endl; }~A(){}
};class B :public A
{
public:B(char *s1, char*s2) :A(s1) { cout s2 endl; }
};class C : public A
{
public:C(char *s1, char*s2) :A(s1) { cout s2 endl; }
};class D :public C, public B
{
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;
}
Aclass A class B class C class D
Bclass D class B class C class A
Cclass D class C class B class A
Dclass A class C class B class D 答案是哪个呢 首先D肯定是最后一个才被初始化的构造函数先走初始化列表BCA那肯定是A先被初始化因为B,C中都有AA不初始化BC没办法初始化其次要看继承的顺序D先继承C再继承B,所以先初始化C,再初始化B.最终答案就是D 第二题 class Base1 {public: int _b1;};
class Base2 { public: int _b2; };
class Derive : public Base2, public Base1
{ public: int _d; };int main(){Derive d;Base1* p1 d;Base2* p2 d;Derive* p3 d;return 0;
}
Ap1 p2 p3 Bp1 p2 p3 Cp1 p3 ! p2 Dp1 ! p2 ! p3 答案选哪个呢 子类首先继承了Base2再继承了Base1所以模型应该是这样的 所以没有答案答案应该是p3p2p1. 所以通过上面这两个例子我们可以看的出其实实现继承时继承的顺序是非常重要的有关谁先被创建。 4.一些常考的多态的问题 1. 什么是多态 多态分为静态多态和动态多态 静态多态是在编译时自动和所调用的类型所绑定从而确定函数地址直接调用 动态多态是在运行时根据父类指针所指向的对象指向父类调父类的虚函数指向子类调子类中父类那部分重写以后的虚函数。 2. 什么是重载、重写(覆盖)、重定义(隐藏) 重载同一作用域只有函数名相同参数不同的函数 重定义隐藏在两个不同的类中两个不同的作用域只要函数名相同就构成了重定义 重写在构成重定义隐藏的基础上函数得是虚函数且函数名参数返回值必须相同 3. 多态的实现原理 简而言之虚表的重要性离不开虚表和虚函数的重写指向谁就调用谁 4. inline函数可以是虚函数吗 可以语法角度上看不过编译器就忽略inline属性这个函数就不再是inline因为虚函数要放到虚表中去。如果inline函数不是虚函数就还会有inline这个属性 5. 静态成员可以是虚函数吗 不能因为静态成员函数没有this指针静态成员函数在类没有实例化对象之前就已经分配空间了不用实例化对象也可以调用但是对于virtual虚函数它的调用恰恰使用this指针。在有虚函数的类实例中this指针调用vptr指针指向的是vtable(虚函数列表)通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是this指针-vptr(4字节-vtable -virtual虚函数。所以说static静态函数没有this指针也就无法找到虚函数了。所以静态成员函数不能是虚函数。他们的关键区别就是this指针。 6. 构造函数可以是虚函数吗 不能因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的你连虚表指针都没有还怎么调用构造函数 7. 析构函数可以是虚函数吗什么场景下析构函数是虚函数 可以并且最好把基类的析构函数定义成虚函数。防止多态调用析构函数时重复调用一个对象的虚函数发生内存泄漏。 8. 对象访问普通函数快还是虚函数更快 我们得分具体情况 普通调用时当然是普通函数和虚函数都是一样的 多态调用时当然普通函数更快虚函数的调用会先去找虚表指针找到虚表再去调用虚函数 9. 虚函数表是在什么阶段生成的存在哪的 虚函数表是在编译阶段就生成的一般情况下存在代码段(常量区)的。 10. C菱形继承的问题虚继承的原理 注意这里不要把虚函数表和虚基表搞混了 菱形继承为了避免数据冗余会用虚基表来解决虚基表是用来存偏移量进而通过偏移量来找到虚基类 虚继承是虚函数的重写通过虚表指针找到虚表进而调用虚表中的虚函数 11. 什么是抽象类抽象类的作用 抽象类是在虚函数后面写上0它强制必须重写子类的虚函数不写就不可以实例化出对象另外抽象类体现出了接口继承关系。 总结 这一节我们完完整整把多态的全部内容都讲了一遍当然途中大家肯有会有不懂的地方因为这是难点我在编写这边文章的时候也是反反复复思考和学习所以大家需要反复思考观看不懂得可以在评论区回复或者私信我哦 大家加油