分类信息网站平台的推广,wordpress登陆404,网站开发工程师专业,设计制作散发寄递个人主页#xff1a;#x1f35d;在肯德基吃麻辣烫 我的gitee#xff1a;gitee仓库 分享一句喜欢的话#xff1a;热烈的火焰#xff0c;冰封在最沉默的火山深处。 文章目录 前言一、什么是多态#xff1f;二、多态的构成条件2.1什么是虚函数#xff1f;2.2虚函数的重写2… 个人主页在肯德基吃麻辣烫 我的giteegitee仓库 分享一句喜欢的话热烈的火焰冰封在最沉默的火山深处。 文章目录 前言一、什么是多态二、多态的构成条件2.1什么是虚函数2.2虚函数的重写2.3 什么是虚函数表有虚函数的对象的大小 2.4普通对象调用和实现多态后的对象调用 三、多态的原理经典题回到多态的两个条件 多态条件的两个特例这一点是C的大坑3.1动态绑定和静态绑定 四、默认成员函数和虚函数的关系4.1构造函数可以设置成虚函数吗4.2 析构函数可以设置成虚函数吗一道经典面试题 五、单继承和多继承关系中的虚函数表5.1单继承关系中的虚函数表5.2多继承关系中的虚函数表 六、抽象类写在最后的面试题总结 前言
本文继C继承之后讲解C多态。 一、什么是多态
单单从概念入手不好理解应该深入理解多态的实现后再回过头来讲解。 现在简单举个例子我们在购买高铁票时往往会有成人票全价学生票半价的优惠针对不同的人群给予不同的优惠这个就是多态多种形态。
二、多态的构成条件
多态的两个构成条件为 1.基类的指针或引用 2.满足虚函数的重写 2.1什么是虚函数
被virtual关键字修饰的类成员函数就是虚函数。
在继承中子类要想重写父类父类的成员函数必须是虚函数而子类的成员函数可以不加virtual但一般建议加上比较合适。
2.2虚函数的重写
虚函数的重写是对父类的虚函数的实现进行覆盖覆盖的内容是子类虚函数的实现。
满足重写的条件三同。 函数名参数类型返回值相同的虚函数就能满足重写的条件。 2.3 什么是虚函数表
虚函数表是继承体系中如果一个函数是虚函数则该函数的地址会存储在一张虚函数表中而不是存储在对象中该虚函数表的地址才存储在对象中。通过虚函数表可以找到对应的虚函数从而能够进一步实现多态。
有虚函数的对象的大小
class A
{
public:virtual void func1(){}virtual void func2(){}protected:int _a;
};int main()
{cout sizeof(A) endl;return 0;
}请计算上面的代码中A这个类的大小。 A有两个虚函数一个成员实际上大小为8 原因 虚函数在内存中是存储在虚函数表中而不是存在类对象中类对象在内存中存储的是一个虚函数表指针和成员变量。 指针大小是4字节成员是int类型则共为8字节。 虚函数表本质上是一个函数指针数组。 一般建议如果不实现多态就不要设置成虚函数
2.4普通对象调用和实现多态后的对象调用
普通对象调用成员函数是在编译期间就确定了地址而实现多态后的函数是在运行期间才确定地址。因为子类继承父类子类先拷贝父类的虚函数表的地址如果某个函数是虚函数则会在子类的虚函数表中重写改函数的地址。
所以编译器在遇到父类的指针或引用调用子类的函数时编译器在编译期间无法确定到底调用谁的函数只能运行起来去子类的虚函数表中查找是谁的地址就调用谁。
三、多态的原理
我们通过以下例子来看待多态。
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;Func(Mike);Student Johnson;Func(Johnson);return 0;
}首先子类继承了父类父类在内存中有一张虚表存的是虚函数的地址。当子类继承了父类后子类中也同样有一张虚表只不过是这张虚表上的虚函数的地址是拷贝父类的如果子类中有虚函数且满足重写的条件则子类的虚函数表的虚函数的地址会被重写覆盖成子类的虚函数的地址。 当我们用父类的指针或引用调用子类的虚函数时会去访问子类的虚函数表当我们调用父类的虚函数时会访问父类的虚函数表这样就实现了父类指针指向父类对象就调用父类的虚函数表指向子类对象就调用子类的虚函数表。
所以多态就是我们想调用父类的函数就传递父类对象给父类指针/引用想调用子类的函数就传递子类对象给父类指针/引用。
经典题
这里有一道非常经典的坑人题目
class A
{
public:virtual void func(int val 1){ std::coutA- val std::endl;}virtual void test(){ func();}};class B : public A
{
public:void func(int val0){ std::coutB- val std::endl;}};int main(int argc ,char* argv[]){B*p new B;p-test();return 0;}问结果输出什么 对于一个子类指针p指向子类对象通过这个指针调用函数test因为子类继承了父类父类就成了子类的成员通过父类的虚表找到test函数的地址调用在test函数中又调用func函数因为这是B的指针调用func函数时会去B对象的虚表中找到func的地址这个地址不再是从父类那里拷贝的func的地址这个地址已经被重写成子类的func的地址所以调用的是子类的func函数。 然而虚函数的重写只是重写函数的实现函数的接口仍然继承父类的这个叫做接口继承所以参数的缺省值仍然是父类的。 结果输出B-1 回到多态的两个条件
多态的两个条件 1.父类的指针或引用 2.虚函数的重写 (1)为什么不能是子类的对象赋值给父类的对象而是子类对象赋值给父类的指针/引用 因为子类对象赋值给父类对象切片过程中不会拷贝虚表。所以父类对象只能调用父类的虚表子类对象才能调用子类的虚表满足不了多态。
(2)为什么子类的对象赋值给父类的对象不会拷贝虚表 因为如果拷贝虚表使用子类对象会调用子类的虚函数使用父类对象也会拷贝子类的虚函数就乱套了。
(3)为什么不能是子类的指针或引用因为父类是子类的一部分父类赋值给子类指针不会切片就不能获取父类的虚函数表。
需要注意的以下几点 1虚函数存在哪虚函数表存在哪 注意虚函数和普通函数一样都是存在代码段的虚函数表也是存在代码段的。 是虚函数地址存在虚表中虚函数表的地址存在对象中。 我们可以通过打印地址的方式验证一下虚函数表存在代码段 void test_where()
{Person p;Student s;//栈int a;printf(栈-[%p]\n, a);//堆区int* ptr new int;printf(堆-[%p]\n, ptr);//数据段静态区static int b;printf(静态-[%p]\n, b);//代码段常量区const char* str Hello World;printf(常量区-[%p]\n, str);printf(虚函数表1-[%p]\n, *((int*)p));printf(虚函数表2-[%p]\n, *((int*)s));}int main()
{test_where();return 0;
}思路获取父类或子类对象的虚函数表指针也就是取出父类/子类在内存中的前4个字节将该地址与内存中的栈区堆区静态区数据段常量区代码段的地址进行对比跟谁的地址比较近就大致在哪个区域。 2子类继承父类后如果子类也有自己的虚函数则这些虚函数的地址是按照他们声明的顺序依次存在虚表的最后因为前面先存父类的虚函数的地址。 多态条件的两个特例这一点是C的大坑
1协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。 这一点是C的大坑
2析构函数的重写父类和子类的析构函数名不同 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor。
3.1动态绑定和静态绑定
多态可以分为动态多态和静态多态动态多态也叫做动态绑定静态多态也叫做静态绑定。 静态绑定有函数重载 函数重载在编译时进行匹配cout的自动识别类型的底层也是使用函数重载实现的。 函数重载原理函数名修饰规则 动态绑定多态。 编译阶段编译器不知道到底调用子类的虚函数还是父类的虚函数所以编译器只能通过给定的对象在运行期间通过调用虚函数表的地址来进行查看。这个过程叫做动态绑定。 多态原理虚函数表 四、默认成员函数和虚函数的关系
4.1构造函数可以设置成虚函数吗 构造函数不能是虚函数因为虚函数表是在构造函数的初始化列表初始化之后才会生成的。如果构造函数是虚函数那么调用构造函数的时候要去虚函数表里面找到构造函数的地址来进行调用。那么如何找到虚表呢这就是一个典型的现有鸡还是先有蛋的问题了。 4.2 析构函数可以设置成虚函数吗 析构函数可以设置成虚函数并且强烈建议设置成虚函数。 1析构函数加virtual是不是虚函数 是因为在编译器编译阶段会通过函数名修饰规则统一将子类的析构函数和父类的析构函数改成destructor。 2为什么要这么处理 因为在子类析构函数调用完成后必须要调用父类的析构函数对父类的资源释放。设置成虚函数可以完成子类对父类的析构函数的重写。 3为什么要让他们构成重写 因为在下面的场景中构成重写可以实现父类指针调用子类对象会调用子类的析构子类析构调用结束会自动调用父类的析构父类指针调用父类对象会调用父类的析构。从而实现不同对象传递给父类指针/引用会调用不同对象的函数实现多态。 如果不设置成虚函数假如有以下的场景会出问题
class Person
{
public:~Person(){cout father:~Person endl;}
};class Student : public Person
{
public:~Student(){cout son:~Student endl;delete [] ptr;}protected:int* ptr new int[10];//new 构造 operator new()
};int main()
{Person* p new Person;delete p;p new Student;delete p;return 0;
}delete p 会处理成以下方式 p-destructor() operator delete( p )
普通对象看当前者的类型。 多态对象看指向对象的类型。
p是父类对象不管指向的对象是父类还是子类都会调用父类的析构函数在子类申请的空间就得不到释放会造成内存泄露问题。 如果设置成虚函数就能够实现虚函数的重写从而实现多态。
一道经典面试题
设计一个不想被继承的类如何创建 1构造函数设置成私有
class A
{
private:A(){}};class B : public A
{
public:B(){}};原理在继承体系中子类的构造函数必须先去调用父类的构造函数这里父类构造设置成私有子类就无法调用了。
不过这里出现一个问题子类无法调用父类的构造父类也无法调用自己的构造了。 我们可以写一个函数在函数里面创建一个父类。
class A
{
private:A Createobj(){return A();}A(){}};但是又有一个问题如何调用这个函数呢因为调用该函数创建对象而创建对象又需要在函数里面创建。 这里我们可以加一个static解决
class A
{
private:static A Createobj(){return A();}A(){}};int main()
{A a A::Createobj();
}这样就可以通过指定类域访问该函数解决。
五、单继承和多继承关系中的虚函数表
5.1单继承关系中的虚函数表
单继承关系中子类会拷贝父类的虚函数表如果子类还有自己的虚函数则该虚函数的地址会放在虚函数表的最后。
我们通过调试窗口无法看到子类的虚函数。 不过我们可以通过获取虚表的地址来打印虚表的各个虚函数的地址。
typedef void(*VFPTR)();class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a 1;
};class Derive :public Base {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::func4 endl; }
private:int b 2;
};void PrintVfptr(VFPTR* arr)
{printf(虚表地址是[%p]\n, arr);for (int i 0; arr[i] ! nullptr; i){printf(第%d个虚函数地址是[%p]\n, i, arr[i]);}printf(\n);}int main()
{Base b;Derive d;int vfptrb *((int*)b);int vfptrd *((int*)d);PrintVfptr((VFPTR*)vfptrb);PrintVfptr((VFPTR*)vfptrd);return 0;
}
思路1.先获取虚表的地址取子类对象的地址强转成int*再进行解引用就取到了子类对象的前4个字节也就是虚表指针。2.再强转成VFPTR*通过打印该指针指向的内容即可打印虚表的内容。
5.2多继承关系中的虚函数表
这里有几种猜测子类的未重写的虚函数会放在第一个继承的父类的虚表中或者放在其他的父类的虚表中。
我们也可以通过打印地址的方式确定。
typedef void(*VFPTR)();class Base1
{
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }private:int b1 1;
};class Base2
{
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Derive::func2 endl; }
private:int b2 2;
};class Derive1 :public Base1, public Base2
{
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }
private:int d 2;
};void PrintVfptr(VFPTR* arr)
{printf(虚表地址是[%p]\n, arr);for (int i 0; arr[i] ! nullptr; i){printf(第%d个虚函数地址是[%p]\n, i, arr[i]);}printf(\n);}int main()
{Derive1 d;int vfptrb1 *((int*)d);Base2* p2 d;//自动切片p2就指向Base2对象的首地址int vfptrb2 *(int*)p2;//int vfptrb2 *((int*)((char*)d sizeof(Base1)));PrintVfptr((VFPTR*)vfptrb1);PrintVfptr((VFPTR*)vfptrb2);return 0;
}通过打印可以看到子类中未重写的虚函数放在第一个继承的父类的虚表中。
但是这里有一个问题为什么重写了func1在Base1的func1的地址和Base2的func1的地址不一样
在ptr1和ptr2调用func1的过程中调用的是Derive的func1函数因为func1已经被重写了。
而在内存中ptr1和ptr2指向的地址如下 因为ptr1指向的地址刚好是d对象的首地址ptr1和this指针是重叠的无需偏移而ptr2需要偏移Base1字节才与this指针重叠。 这就导致在调用func1函数前ptr2需要先偏移才能调用。我们看到的是在偏移之前的ptr2的地址这就是为什么看到的func1的地址不同调用的确实同一个函数的原因实际上ptr2会偏移。
六、抽象类 在虚函数的后面写上 0则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。 class
{
public:virtual void func1() 0{}
};比如上面这个就是抽象类。 抽象类的特性
1.不能实例化出对象子类继承后也不能实例化出对象只有重写虚函数才能实例化出对象。2.可以定义指针或引用.
普通的对象是实现继承而实现多态的对象是接口继承。
抽象类的作用间接强制子类虚函数必须重写否则无法实例化对象。
写在最后的面试题 什么是多态什么是重载、重写(覆盖)、重定义(隐藏)多态的实现原理inline函数可以是虚函数吗静态成员可以是虚函数吗构造函数可以是虚函数吗析构函数可以是虚函数吗对象访问普通函数快还是虚函数更快虚函数表是在什么阶段生成的存在哪的C菱形继承的问题虚继承的原理什么是抽象类抽象类的作用 1.多态分为静态多态和动态多态。 静态多态是函数重载本质上是函数名修饰规则来完成。 动态多态继承中的虚函数重写 父类指针或引用两个条件完成。 动态多态的本质上是由虚函数表的实现来完成。 2.重载两个函数必须在同一作用域函数名和参数类型必须相同。 重写覆盖两个函数在父类和子类的作用域且要求满足函数名参数类型返回值必须相同协变例外两个函数必须是虚函数。 重定义隐藏两个函数在父类和子类的作用域且要求满足函数名相同两个父类和子类的重名函数不构成重写就一定是重定义。 3.多态实现的原理 静态多态函数名修饰规则 动态多态虚函数表 4.不可以因为一个函数如果设置成内联就是一段代码没有产生地址无法将地址放进虚表就不能是虚函数了。声明如果一个内联函数被virtual修饰那么该函数就会自动忽略inline的属性而成为一个虚函数。 5. 静态成员函数不能是虚函数因为静态成员函数没有this指针使用类型::成员函数的方式来调用该函数但这种方式无法访问虚函数表所以静态成员函数无法放进虚表里无法实现多态就没有意义。 6.构造函数不能是虚函数构造函数不能是虚函数因为虚函数表是在构造函数的初始化列表中生成的如果构造函数是虚函数又得取到虚函数表里面找到构造函数的地址所以是现有构造函数还是先有虚表呢 7.析构函数可以是虚函数并且强烈建议析构函数设置成虚函数。因为父类和子类的析构函数会被编译器统一编译成destructor有些特殊场景必须实现多态才能解决请参考本文内容。 8.对象访问普通函数和虚函数是一样快的因为它们都是在编译期间就确定。但如果是指针对象或引用对象调用是普通函数快因为调用虚函数会去到虚表中查找地址速度会慢一些。 9.虚函数表在编译阶段生成的存在代码段具体参考本文章的案例。 10.菱形继承的问题数据冗余和二义性。菱形虚拟继承原理生成一张虚基表存放的是该对象相对于父类对象的偏移量通过偏移量可以访问父类的成员不再需要在每个子类中存一份父类的成员。 11.抽象类具有纯虚函数的类叫做抽象类。而在虚函数后面加上 0就是纯虚函数。抽象类的意义强制子类进行虚函数的重写并且抽象类体现了接口继承关系。 总结
多态内容就讲到这里。 文章转载自: http://www.morning.nrddx.com.gov.cn.nrddx.com http://www.morning.jfsbs.cn.gov.cn.jfsbs.cn http://www.morning.ctbr.cn.gov.cn.ctbr.cn http://www.morning.qyjqj.cn.gov.cn.qyjqj.cn http://www.morning.bnygf.cn.gov.cn.bnygf.cn http://www.morning.redhoma.com.gov.cn.redhoma.com http://www.morning.nqlcj.cn.gov.cn.nqlcj.cn http://www.morning.xbmwm.cn.gov.cn.xbmwm.cn http://www.morning.gnyhc.cn.gov.cn.gnyhc.cn http://www.morning.ynrzf.cn.gov.cn.ynrzf.cn http://www.morning.zxcny.cn.gov.cn.zxcny.cn http://www.morning.rwhlf.cn.gov.cn.rwhlf.cn http://www.morning.yxwcj.cn.gov.cn.yxwcj.cn http://www.morning.symgk.cn.gov.cn.symgk.cn http://www.morning.lnnc.cn.gov.cn.lnnc.cn http://www.morning.rwjfs.cn.gov.cn.rwjfs.cn http://www.morning.yrbq.cn.gov.cn.yrbq.cn http://www.morning.jmlgk.cn.gov.cn.jmlgk.cn http://www.morning.baguiwei.com.gov.cn.baguiwei.com http://www.morning.yodajy.cn.gov.cn.yodajy.cn http://www.morning.thlzt.cn.gov.cn.thlzt.cn http://www.morning.tqfnf.cn.gov.cn.tqfnf.cn http://www.morning.dbrnl.cn.gov.cn.dbrnl.cn http://www.morning.wzwyz.cn.gov.cn.wzwyz.cn http://www.morning.lgmgn.cn.gov.cn.lgmgn.cn http://www.morning.tmbtm.cn.gov.cn.tmbtm.cn http://www.morning.frcxx.cn.gov.cn.frcxx.cn http://www.morning.pxlsh.cn.gov.cn.pxlsh.cn http://www.morning.xtrzh.cn.gov.cn.xtrzh.cn http://www.morning.rbbzn.cn.gov.cn.rbbzn.cn http://www.morning.ydfr.cn.gov.cn.ydfr.cn http://www.morning.srkqs.cn.gov.cn.srkqs.cn http://www.morning.lhrxq.cn.gov.cn.lhrxq.cn http://www.morning.kdlzz.cn.gov.cn.kdlzz.cn http://www.morning.dgknl.cn.gov.cn.dgknl.cn http://www.morning.xlxmy.cn.gov.cn.xlxmy.cn http://www.morning.yongkangyiyuan-pfk.com.gov.cn.yongkangyiyuan-pfk.com http://www.morning.kztts.cn.gov.cn.kztts.cn http://www.morning.fxxmj.cn.gov.cn.fxxmj.cn http://www.morning.ntcmrn.cn.gov.cn.ntcmrn.cn http://www.morning.qhmql.cn.gov.cn.qhmql.cn http://www.morning.yrck.cn.gov.cn.yrck.cn http://www.morning.khcpx.cn.gov.cn.khcpx.cn http://www.morning.rbbzn.cn.gov.cn.rbbzn.cn http://www.morning.tbnpn.cn.gov.cn.tbnpn.cn http://www.morning.mphfn.cn.gov.cn.mphfn.cn http://www.morning.drytb.cn.gov.cn.drytb.cn http://www.morning.mxnfh.cn.gov.cn.mxnfh.cn http://www.morning.dwdjj.cn.gov.cn.dwdjj.cn http://www.morning.rxhs.cn.gov.cn.rxhs.cn http://www.morning.qichetc.com.gov.cn.qichetc.com http://www.morning.xcjbk.cn.gov.cn.xcjbk.cn http://www.morning.ydwnc.cn.gov.cn.ydwnc.cn http://www.morning.bpmmq.cn.gov.cn.bpmmq.cn http://www.morning.ctqlq.cn.gov.cn.ctqlq.cn http://www.morning.mwbqk.cn.gov.cn.mwbqk.cn http://www.morning.nhpgm.cn.gov.cn.nhpgm.cn http://www.morning.fhxrb.cn.gov.cn.fhxrb.cn http://www.morning.lgnbr.cn.gov.cn.lgnbr.cn http://www.morning.hbywj.cn.gov.cn.hbywj.cn http://www.morning.dbrpl.cn.gov.cn.dbrpl.cn http://www.morning.lzsxp.cn.gov.cn.lzsxp.cn http://www.morning.snktp.cn.gov.cn.snktp.cn http://www.morning.wrtbx.cn.gov.cn.wrtbx.cn http://www.morning.mfbcs.cn.gov.cn.mfbcs.cn http://www.morning.mnclk.cn.gov.cn.mnclk.cn http://www.morning.lpsjs.com.gov.cn.lpsjs.com http://www.morning.qmbpy.cn.gov.cn.qmbpy.cn http://www.morning.yqndr.cn.gov.cn.yqndr.cn http://www.morning.mnbcj.cn.gov.cn.mnbcj.cn http://www.morning.fy974.cn.gov.cn.fy974.cn http://www.morning.bhwll.cn.gov.cn.bhwll.cn http://www.morning.ailvturv.com.gov.cn.ailvturv.com http://www.morning.smrty.cn.gov.cn.smrty.cn http://www.morning.tpssx.cn.gov.cn.tpssx.cn http://www.morning.bsqkt.cn.gov.cn.bsqkt.cn http://www.morning.qfdmh.cn.gov.cn.qfdmh.cn http://www.morning.xdpjf.cn.gov.cn.xdpjf.cn http://www.morning.lwzpp.cn.gov.cn.lwzpp.cn http://www.morning.rmjxp.cn.gov.cn.rmjxp.cn