当前位置:首页 » 《关于电脑》 » 正文

C++:多态

16 人参与  2024年05月06日 13:50  分类 : 《关于电脑》  评论

点击全文阅读


目录

概念:

多态产生的条件:

 虚函数的重写:

虚函数:即被virtual修饰的类成员函数称为虚函数 

虚函数重写的两个例外:

 协变(基类与派生类虚函数返回值类型不同)

析构函数 

而为什么没有调用到子类呢?

注意子类不写virtual父类加上了virtual也可也进行虚函数的重写,但是不太建议

 重写、重定义、重载区别

继承体系区分:

原型的区分:

多继承中的指针偏移问题:

 虚表与虚表指针:

虚表指针概念: 

指向谁调用谁: 

指向谁调用谁,传父类调用父类,传子类调用子类: 

 指向谁调用谁:

 传父类调用父类,传子类调用子类:

单继承的虚表状态: 

不过这里可以解释为一种bug:



概念:

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。 再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的 活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5 毛....。其实这背后也是一个多态行为。

多态产生的条件:

 父子类之间完成虚函数的重写父类的指针或者引用去调用虚函数,且子类必须对父类的虚函数进行重写操作
 虚函数的重写:

父类要有虚函数,子类也需要有虚函数,且父子类虚函数是同一个函数名,统一的参数(参数的缺省值可以不同),父类和子类的虚函数返回值是要一致的。

而父类指针或者引用调用虚函数,就如下图所示:

虚函数:即被virtual修饰的类成员函数称为虚函数 
尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态同时静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名:.成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数

虚函数重写的两个例外:

 协变(基类与派生类虚函数返回值类型不同)

 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

 协变就是父子类的虚函数返回值不同,不过返回值必须是父子类关系的指针或者引用

如图所示,类A是类B的父类,而person中的虚函数使用了返回值类型是A*而他的子类student的虚函数使用的返回值是B* 刚好AB是父子关系,所以这就变成了协变

当然这个父子类指针是可以指其他类型的父子类,也可也是自己的父子类。

析构函数 

 父类和子类的虚函数的析构函数函数名并不相同,但是因为析构函数在多态中会被编译器在暗地里变成同一个名字。

为什么会变成同一个名字?因为析构函数会因为父子类关系,在子类调用析构后会自动调用父类,但是:

person是一个类指针,既可以指向自己也就是父类,又可以指向子类,这是因为切片的概念(这里的父子类没有变成多态关系) ,而在使用delete调用析构时出了问题!没有调用子类的析构,即使我们指向了子类。

而为什么没有调用到子类呢?
因为delete的内部构成是:析构函数和operator delete()而operator delete 有一个特点,那就是调用delete 的指针是什么类型的,就会调用什么类型的析构函数,而这里的两个指针都是父类person 所以就都调用了父类person的析构函数,所以没有调动子类的析构函数

所以这样子写就会有一种后果: 

在子类的析构函数还有东西,例如一个新的空间,所以如果只调用了父类,那么子类内部构建的东西就会变成内存泄露,内部有空间没有释放!

所以想要指向父类调用父类析构,指向子类调用子类析构,在希望出现这种情况时,就需要把析构函数的名字进行了多态暗地里的统一,变成了destructor,所以加上了虚函数加上destructor就变成了重写。

注意子类不写virtual父类加上了virtual也可也进行虚函数的重写,但是不太建议

 重写、重定义、重载区别

继承体系区分:
重写确实发生在继承体系中,子类重写父类的虚函数。重定义通常指的是在派生类中定义一个与基类同名的成员,这可以发生在继承体系中,但它与重写不同,因为重写特指虚函数的覆盖。 重载是发生在同一个类中的,它指的是在同一个作用域内使用相同的函数名但参数列表不同的函数。重载与继承体系无关,重载只能在一个范围内,不能在不同的类里
原型的区分:
重载要求函数名相同但参数列表不同。重写要求函数名和参数列表都与基类中被重写的虚函数相同。重定义的原型可能与基类中的成员不同,但这并不是关键区别点。

多继承中的指针偏移问题:

 答案选C,p1和p3指向的位置是相等的,但只是巧合,这里根据的是切片原理,p1只是指向第一个方形区域,p2指向的是第二个方形区域,而p3是指向两个方形区域外面的大方形区域!

 虚表与虚表指针:

虚表指针概念: 

虚表指针(vptr)并不是每个虚函数一个虚表指针。实际上,每个包含虚函数的类     的   对象   都有一个虚表指针(vptr)而不是每个虚函数一个。这个虚表指针指向一个虚函数表(vtable),虚函数表中存放了该类所有虚函数的地址。当子类继承父类并覆盖父类的虚函数时,子类的虚函教表会包含父类的虚函数地址(如果子类没有覆盖这些函数)以及子类新增或覆盖的虚函数地址。
指向谁调用谁: 
父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象,这也是虚函数中,指向谁调用谁的概念本质。通过虚表指针和虚函数表,就可以实现多态性,即可以在运行时确定应该调用哪个类的虚函数。虚表指针是类级别的,而不是函数级别的。每个类只有一个虚表指针,指向其对应的虚函数表,但是多继承的时候,就会可能有多张虚表。每一个类中的虚函数在虚表中都要有地址。

指向谁调用谁,传父类调用父类,传子类调用子类: 
 指向谁调用谁:

1.父类和子类的func构成了重写,即使子类的func没有加上virtual,但是还是具有重写,别看参数的缺省值不一样,但只要参数一样就行,所以父类和子类的func构成了重写

2.给了一个test加上了virtual,然后子类指针遍历p调用了父类的test,这是因为继承所以可以调用父类,同时,这里面test内部的指针是A*this,而不是B*this,因为子类是继承不是拷贝,所以是调用父类的成员,所以这里面隐藏的this是父类的类型,所以这里将p传给了A*this,而this调用了func(),因为this是A所以这里是多态调用

3.换句话说,这里面的test就可以当作一个多态调用的函数了,就例如上面迈火车票的函数调用一样,可以变换成 void test(A*this){this->func()}

4.而这里传值传的p 是 指向的是 一个new B 的地址,所以只能调用相对应类型的方法,也就是B的方法,从而调用了B的func()

答案是B::f(),

虽然 B 的 f 函数被声明为 private,但这并不影响多态性的工作。当您使用基类的指针或引用来调用虚函数时,C++ 运行时系统会检查对象的实际类型,并调用正确的函数。访问权限(如 publicprotectedprivate)仅影响代码在何处可以访问该函数,而不影响多态性的行为。

因此,即使 B 的 f 函数是 private 的,当通过基类指针 pa 调用 f 函数时,由于 pa 实际上指向一个 B 对象,所以仍然会调用 B 的 f 函数。这就是为什么输出是 B::f()

 传父类调用父类,传子类调用子类:

如图所示, 图中父子类各自的虚表指针指向的虚表以及虚表内部的地址并不相同,所以这里证明了父类的虚表指针和子类的虚表指针指向的虚表并不是同一个。

并且,对于编译器而言在指向谁调用谁 在使用这个指令之前,编译器会自行的判断这个父子类是否是产生了多态,如果是则编译器会如上图所示,如果不是编译器则会如下图所示:

单继承的虚表状态: 

如图所示,只有f1进行了多态的重写,func2没有重写因为子类没有func2 ,func3没有重写因为父类么有func3。

父类虚表  

子类虚表 

可以看到 子类的虚表不正常,因为每一个类中的虚函数在虚表中都要出现,所以这里少了两个虚函数的地址,func1是重写的,func2是继承的没有重写,而func3和func4不见了!作为子类自己的虚函数消失了。

不过这里可以解释为一种bug:

因为虚函数都需要进入虚表内部,但这里并没有放入,这里是VS监视窗口的bug

也可也理解为,子类的虚表实际上是拷贝了父类的虚表,重写的部分进行覆盖,没有重写的部分不变动的拷贝,也可也说是子类的虚表被隐藏了。

 多继承虚表问题:

多继承的对象模型:

 

Derive由两个部分构成,第一个是继承了Base1, 一个类中只有一个虚表指针,所以base1内部有一个虚表指针和一个int类型的对象,按照内存对齐需要最大的 变量的对齐数的倍数,所以是8,而base2也是如此,

而Derive继承了这两个所以就有了十六字节,然后他自己又有一个四字节

同时虚函数func3放到了继承的第一个类的虚表内部,而这里Derive就有两个虚表指针,同时这里的两个虚表指针不能合二为一,这关系于切片问题!

所以多继承内部可能会有多个虚表指针,并且继承的类需要算入继承的那个类的大小,和自己成员变量的大小即可,哪怕自己也有虚函数,也不需要加上虚表指针,因为会继承 那个类的虚表指针

抽象类:  

 如图就是一个抽象类,这个函数只需要声明,不需要示例化对象,同时后面加上=0的虚函数是纯虚函数,而包含纯虚函数的的类就是抽象类,如下图的类car一样

 抽象类的意义是?抽象类的意义是要强制子类进行虚函数进行重写。

 

 

因为子类继承父类的抽象类导致也不能实例化出对象,所以如果要子类实例化对象,那么子类就必须重写虚函数,也就是子类必须重写。

和override的区别是,override是已经重写了,是进行查看重写有无错误,进行检查作用,类似assert断言的功能,这种是子类不重写就会报错,强制子类重写

同时因为纯虚函数不能实例化对象,但是可以具有指针,可以使用父类 类型的指针,进行调用子类。

同时因为纯虚函数不能实例化对象,但是可以具有指针,可以使用父类 类型的指针,进行调用子类。

如下图所示,Drive在类Car内部是一个纯虚函数,但是我们定义了了一个类Car类型的指向ptr ,并用这个指向指向了纯虚函数Drive进行调用。

 final

如果不想要一个类被继承就加上final,如图外面不想要car被继承就加上了final,加上final后类不能被继承,虚函数不能被重现,被final修饰的类被叫做最终类。

 override:

是写在子类的重写虚函数后边的,是可以帮忙检查是否完成重写,如果没有完成重写会报错,或者在重写时会出现一些问题时,overrdie会发出报错!

多态的原理: 

本质是父类对象里面的虚函数放到了父类的虚表,而子类对象继承了父类,同时继承了父类的虚表

而如果有虚函数进行重写,那么父类的虚表内部是父类的虚函数,而子类的虚表内是子类的虚函数是子类重写的虚函数,而指向谁调用谁,父类指针指向父类对象,指向父类则从父类的虚表中找到父类的虚函数,指向子类则从子类的虚表中找子类的虚函数,所以就是指向父类调用父类,指向子类调用子类

不是多态则是在编译时就是已经指向了某个地方,而不是因为指向谁调用了谁

满足多态的本质在底层看来就是在虚表内寻找虚函数。

多态的调用:

 运行时,到窒息对象的虚表内部寻找虚函数调用,指向父类调用父类的虚函数,指向子类调用子类的虚函数,同时虚函数和普通函数一样,都是存在于代码段中的,而不是存在虚表内部的,虚表内部仅仅只是村粗虚函数的地址,而虚表村粗在常量区的!

菱形继承:

 

B内部继承A,C内部继承A ,所以B,C一样都是12字节,因为A是8字节,而D继承了B,C 加上D自己的成员变量 占据4个字节,所以一共是 12+12+4 = 28字节,所以菱形继承和多继承没区别,同时func4放入了B的虚表内部。

 菱形虚拟继承:

 菱形虚拟继承对象模型:一共三十六个字节

因为是虚拟继承,所以A要放到最下面,同时B、C内部有两个指针,一个是虚表指针表示自己的虚函数,另一个是虚基表指针,指向A的虚函数,再加上BC内部的成员函数,所以B、C都是12字节

且B和C的虚函数不能将虚函数丢到A中,这和菱形继承有关系,因为A的虚表是共享的了,同时A还是虚基类

同时因为A是被虚拟继承,所以A的虚函数被放入了公共区域(不计算在内),然而A内部还有一个虚表指针,是指向自己的虚函数,所以A是虚表指针加上A的内部成员变量 等于 8字节

而D因为继承所以没有自己的虚函数指针,所以只需要算上D的成员变量即可所以D是4字节

最后12+12+4+8 =36

父子类虚表问题:

 父类虚表和子类虚表是不一样的,子类虚表一部分是继承父类的!且继承了父类虚表后,子类就不需要在建立虚表,也就是说子类不需要再单独建立虚表。

 



点击全文阅读


本文链接:http://zhangshiyu.com/post/104163.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1