网站开发工程师岗位职责,东莞长安人才市场招聘信息,wordpress好玩插件,住建网查询资质一键查询前言 作者#xff1a;小蜗牛向前冲 名言#xff1a;我可以接受失败#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话#xff0c;还请点赞#xff0c;收藏#xff0c;关注#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。 本期学习目标小蜗牛向前冲 名言我可以接受失败但我不能接受放弃 如果觉的博主的文章还不错的话还请点赞收藏关注支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。 本期学习目标认识什么是多态 认识抽象类理解多态的原理理解多承和多态常见的面试问题。
目录
一、认识多态
1、什么是多态
2、虚函数
3、多态的定义
4、多态中虚函数的二种特殊情况
二、抽象类
1、概念
2、 接口继承和实现继承
三、多态的原理
1、虚函数表
2、多态原理剖析
3、单继承和多继承关系的虚函数表
四、多态的其他知识
1、C11 override 和 final
2、重载、覆盖(重写)、隐藏(重定义)的对比
3、动态绑定与静态绑定
五、分享继承和多态常见的面试问题 一、认识多态
1、什么是多态
多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会 产生出不同的状态。
举个例子来说某宝常年到大学的开学季就会举办扫描领红包活动但是我们发现每个领到的金额都不同而且相对来是非活跃用户红包金额更大这种我们不同角色分到吧同金额的红包我们就可以称为多态。
2、虚函数
在认识多态前我们首先要认识一一下什么是虚函数
在类中被关键字virtual修饰的函数我们就称为虚函数就如red_packet函数在继承中我用关键字virtual解决的菱形继承问题这里要注意区分二则的用法一个是在继承这作用于类一个是在多态中作用于函数。
class Peson
{virtual void red_packet(){cout 小额红包 endl;}
protected:string _name;int _age;
};
3、多态的定义
那么在继承中要构成多态还有两个条件 1. 必须通过基类的指针或者引用调用虚函数 2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。
下面我们来看一段代码
class Person
{
public://虚函数virtual void red_packet(){cout 普通红包 endl;}
protected:string _name;int _age;
};class active_people:public Person
{
public:virtual void red_packet(){cout 小额红包 endl;}
};void fun(Peson p)
{p.red_packet();
}
int main()
{Person p;active_people ap;fun(p);fun(ap);return 0;
} 这里我们通过父类的引用形成了多态。
特别注意子类函数可以不加victual,但父类必须加victual才构成虚函数。
4、多态中虚函数的二种特殊情况
我们都知道要构成多态就用就要满足构成多态的二个条件其中被调用的函数必须是虚函数也就是说必须加是virtual。
但是存在二种特殊情况也构成多态:
析构函数(函数名字不同构成的重写)
class Person
{
public:~Person(){cout Person delete endl;delete[]_p;}
protected:int* _p new int[10];};class active_people:public Person
{
public:~active_people(){cout active_people delete endl;delete[]_d;}
protected:int* _d new int[15];};int main()
{Person p;active_people ap;return 0;
} 通过这段代码我们可以看出父子类在调用析构函数的时候是先调用子类的析构函数子类的析构函数调用完成后子类会自动调用父类的析构函数。
但是如果我对代码进行一下更改
int main()
{Person* ptr1 new Person;Person* ptr2 new active_people;delete ptr1;delete ptr2;return 0;
} 这里我们发现子类并没有被析构掉。为什么呢
这就得提一下delete的特性 1使用指针调用析构 2operator delete(ptr) 也就是说这时候是一个普通调用 (是什么类型就调用什么用的析构函数)所以才只会调用父类的析构但这样的行为不是我们期望的存在内存泄露其实我们期望应该是一个多态调用指向父类调用父类的析构指向子类调用子类的析构函数只要我们在父类的析构函数上加上virtual就可以解决这个问题了。
virtual ~Person()
{cout Person delete endl;delete[]_p;
} 就是为了让父类的析构函数为虚函数
如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字 都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同 看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处 理编译后析构函数的名称统一处理成destructor。
协变(基类与派生类虚函数返回值类型不同
派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。
举例
class A
{};class B : public A
{};
class Person
{
public:virtual A* red_packet(){cout 普通红包 endl;return nullptr;}
};class active_people :public Person
{
public:virtual B* red_packet(){cout 小额红包 endl;return nullptr;}
};void fun(Person p)
{p.red_packet();
}
int main()
{Person p;active_people ap;fun(p);fun(ap);return 0;
} 这里我们只要简单知道在协变这要求三同中返回值可以不同但是要求返回值必须是一个父子类关系的指针或者引用
二、抽象类
1、概念
在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口 类。
抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生 类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。 那这有什么用呢
其实就是强制我们重写子类的虚函数 2、 接口继承和实现继承
普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实 现。
虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成 多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。
三、多态的原理
1、虚函数表
在了解虚函数表前我们先来看一道笔试题
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;
};
我想不少同学会认为是8,可能认为 成员函数存放在公共的代码段我们就只要计算成员变量大小就可以了根据内存对齐就可以得到大小为8个字节。
但真的是这样吗
我们打印出类的大小
cout sizeof(Base) endl; 为什么是16个字节呢 int main()
{Base b;cout sizeof(b) endl;
}
这时我们进行调试打开监视窗口 发现b对象中存放了_vfptr的东西这又是什么呢
其实是一个指针 对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数表也简称虚表。
所以说根据内存对其很容易得到类的大小为16。
那我们在思考一个问题虚函数表到底存放在哪里呢
下面我们通过一份代码大致推断一下
int main()
{int a 0;printf(栈:%08lxp \n, a);const char* str hello world;printf(代码段 / 常量区:%08lxp \n, str);static int b 0;printf(静态区/数据段: %08lxp\n, b);//取类这前4个字节的地址也就是虚表地址Base be;printf(虚表: %08lxp\n, *((int*)be));} 通过打印我们发现虚表位于代码段/常量区中。
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;
};虚表当中存储的就是虚函数的地址因为父类当中的Func1和Func2都是虚函数所以父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址。 而子类虽然继承了父类的虚函数Func1和Func2但是子类对父类的虚函数Func1进行了重写因此子类对象d的虚表当中存储的是父类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖覆盖就是指虚表中虚函数地址的覆盖重写是语法的叫法覆盖是原理层的叫法。
其次需要注意的是Func2是虚函数所以继承下来后放进了子类的虚表而Func3是普通成员函数继承下来后不会放进子类的虚表。此外虚函数表本质是一个存虚函数指针的指针数组一般情况下会在这个数组最后放一个nullptr。
那我们不由的思考到底虚表是那个阶段就开始初始化? 其实虚表指针是在构造函数初始化列表的时候填入对象的虚表是在编译的时候就生成了。虚表里面存放是虚函数地址同普通函数一样编译完成就放在代码段这一个类这所有的虚函数都会放在虚表这。子类会将父类的虚表拷贝一份然后用重写的虚函数地址覆盖到原来虚表中的函数地址因此虚函数的重写也叫虚函数的覆盖。 2、多态原理剖析
前面我们说了那么多但是虚表是如何实现多态的呢 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 Mike;Student Johnson;Person* p1 Mike;Person* p2 Johnson;Func(p1);Func(p2);return 0;
}p1指针指向Mike对象当我们调用Func函数且将p1传给Func时p1-BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。p2指针指向johnson对象时p2-BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket. 这样就实现出了不同对象去完成同一行为时展现出不同的形态 我们都知道达到多态有两个条件一个是虚函数覆盖一个是对象的指针或引用调 用虚函数但这是为什么呢下面我们通过反汇编了解一下。
void Func(Person* p)
{
p-BuyTicket();
}
int main()
{
Person mike;
Func(mike);
mike.BuyTicket();
return 0;
}
// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
...
p-BuyTicket();
// p中存的是mike对象的指针将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用不是在编译时确定的是运行起来
以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket虽然是虚函数但是mike是对象不满足多态的条件所以这里是普通函数的调
用转换成地址时是在编译时已经从符号表确认了函数的地址直接call 地址
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
} 看出满足多态以后的函数调用不是在编译时确定的是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的 . 3、单继承和多继承关系的虚函数表
单继承的虚函数表
这里没什么可以过多分析上面我们都是多单继承虚表的分析但是我们要注意一个现象。
class Base
{
public://父类虚函数virtual void fun1(){cout Base::fun1 endl;}virtual void fun2(){cout Base::fun2 endl;}
private:int a;
};class Derive :public Base
{//重写的虚函数virtual void fun1(){cout Derive::fun1 endl;}//虚函数virtual void fun3(){cout Derive::fun3 endl;}//普通函数void fun4(){cout DERIVE::fun4 endl;}
private:int b;
};int main()
{Base b;Derive d;return 0;
} 这里虽然从监视窗口我们并没有从虚表中看到fun3虚函数的地址但是我们通过查询虚表指针的地址因为虚表本质是一个函数指针数组我们可以发现fun3函数的地址是存在的也就说明只要是虚函数就会进虚表但是vs编译器可能会对非重写的虚函数优化从而在监视窗口中我们不能发现他。
为了更好的验证我们对多继承关系的虚函数表的讨论我们写一个函数来打印虚函数的地址
//为函数指针数组重新取个名字
// 写法1函数指针数组
//void PrintVFTbale(VFPtr vft[], int n)
//{
// for (int i 0; i n; i)
// {
// printf([%d]:%p\n, i, vft[i]);
// }
//}
typedef void(*VFPtr)();
//写法2
void PrintVFTbale(VFPtr vft[])
{for (int i 0; vft[i] ! nullptr; i){printf([%d]:%p-, i, vft[i]);vft[i]();}cout endl; 这里我们要注意要想打印虚表就要取类这前4个字节(32位平台)/前8个字节(64位平台),
我们上面的取法是通用的(void*)在32位平台4个字节,64位平台8个字节。而我们*void**)就自然的找到了void*从而在不同平台上形成自适应。
多继承虚表函数(在vs2013上测试)
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://父类重写了func1virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }
private:int d1;
};// 函数指针数组
typedef void(*VFPtr)();
void PrintVFTbale(VFPtr vft[])
{for (int i 0; vft[i] ! nullptr; i){printf([%d]:%p-, i, vft[i]);vft[i]();}cout endl;
}int main()
{Base1 b1;Base2 b2;PrintVFTbale((VFPtr*)(*(void**)b1));PrintVFTbale((VFPtr*)(*(void**)b2));Derive d;PrintVFTbale((VFPtr*)(*(void**)d));//打印父类的第二张虚表 写法1//PrintVFTbale((VFPtr*)(*(void**)((char*)dsizeof(Base1))));//写法2Base2* ptr2 d;PrintVFTbale((VFPtr*)(*(void**)ptr2));return 0;
} base1h和base2都有一张虚表都被Derive继承(有二张虚表)但是没有被重写的func3通常是放在第一张虚表中的。我们在找父类的第二张虚表的时候可以通过字节偏远的方法找到也可以直接用 Base2* ptr2 d;切片的方式自动偏移。这里我们打印虚函数地址结束调条件是最后一个元素为nullptr,但是这种情况在大部分情况下是适应的要注意的是虚表的具体实现方式可能因编译器而异不同的编译器可能会有不同的实现细节。因此在特定的编译器和环境中虚表的最后一个元素是否为nullptr可能会有所不同。但根据常见的编译器实现将最后一个元素设为nullptr是一种常见的做法。 四、多态的其他知识
1、C11 override 和 final
在学习override和final时我们先思考一个问题如何实现一个不能被继承的类
方法1将构造函数私有(c98)
class A
{
private:A(){}
};class B : public A
{};
这时候因为对象对无法建立自然就无法被继承
方法2 类定义的时候加final
这时候我们称类为最终类类不能被继承。
class A final
{};其实final还有一个功能修饰函数这该函数就不能被重写 override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。 2、重载、覆盖(重写)、隐藏(重定义)的对比 重载 二个函数在同一个作用域 函数名相同/参数不同 重定义(隐藏) 二个函数分别在父类和子类的作用域 函数名相同 二个父类和子类的同名函数不构成重写就是重定义 重写(覆盖) 二个函数分别在父类和子类的作用域 函数名/参数/返回值必须相同(协变除外) 二个函数必须是虚函数
3、动态绑定与静态绑定 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态比如函数重载.动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。 五、分享继承和多态常见的面试问题 1. 什么是多态 它指的是同一种操作或接口可以被不同类型的对象以不同的方式实现和处理的能力。具体来说多态性使得我们可以使用基类父类的指针或引用来引用子类派生类的对象并且根据具体的对象类型执行对应的方法或操作。
多态有两种表现形式静态多态编译时多态和动态多态运行时多态。 静态多态通过函数重载和运算符重载实现编译器在编译阶段根据参数的静态类型决定调用哪个函数或操作符。 动态多态通过虚函数和基类指针/引用实现运行时根据对象的动态类型来确定调用哪个函数。即使使用基类指针或引用也能够在运行时确定实际调用的是子类的方法。 2. 什么是重载、重写(覆盖)、重定义(隐藏) 重载同一作用域内函数名相同参数不同 重写子类和父类的虚函数名称、返回值、参数都相同称子类重写了父类的虚函数 重定义子类和父类的函数名相同称子类隐藏了父类的某个函数。 3. 多态的实现原理 父类和子类之中保存的虚表指针是不一样的通过传入指针或者引用(本质也是指针)确定去子类还是父类之中去寻找虚表指针最后达到调用不同虚函数的目的。 4. inline函数可以是虚函数吗 可以不过编译器就忽略inline属性这个函数就不再是inline因为虚函数要放到虚表中去如果不构成多态直接调用则内联展开。在类里面定义的函数默认内联。 5. 静态成员可以是虚函数吗 不能因为静态成员函数没有this指针使用类型::成员函数的调用方式无法访问虚函数表所以静态成员函数无法放进虚函数表。 6. 构造函数可以是虚函数吗 不能因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 7. 析构函数可以是虚函数吗什么场景下析构函数是虚函数 可以并且最好把基类的析构函数定义成虚函数。
通过基类指针或引用来管理派生类对象。如果基类中的析构函数不是虚函数当通过基类指针或引用删除派生类对象时可能只会调用到基类的析构函数而不会调用派生类的析构函数从而导致派生类可能存在资源泄漏或未被正确清理的问题 8. 对象访问普通函数快还是虚函数更快 先如果是普通对象是一样快的。如果是指针对象或者是引用对象则调用的普通函数快因为构成多态运行时调用虚函数需要到虚函数表中去查找。 9. 虚函数表是在什么阶段生成的存在哪的 虚函数表是在编译阶段就生成的一般情况下存在代码段(常量区)的。 10. C菱形继承的问题虚继承的原理 菱形虚拟继承因为子类对象当中会有两份父类的成员因此会导致数据冗余和二义性的问题。虚继承对于相同的虚基类在对象当中只会存储一份若要访问虚基类的成员需要通过虚基表获取到偏移量进而找到对应的虚基类成员从而解决了数据冗余和二义性的问题。 11.什么是抽象类抽象类的作用 在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类 。
抽象类强制重写了虚函数另外抽象类体现出了接口继承关系。