为什么基类指针和引用可以指向派生类对象,但是反过来不行?

来源:互联网 发布:华三基于端口nat配置 编辑:程序博客网 时间:2024/05/16 10:57

基类指针和引用


  1. BaseClass *pbase = NULL;
  2. DerivedClass dclass;
  3. pbase = & dclass;

基类指针和引用可以指向派生类对象,但是无法使用不存在于基类只存在于派生类的元素。(所以我们需要虚函数和纯虚函数)

原因是这样的:

  1. 在内存中,一个基类类型的指针是覆盖N个单位长度的内存空间。 
    当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。
  2. 于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素.

类型一致并不是死板地说类型一定要完全一样,类型是一种约束,帮助你验证程序的正确性。比如说你女朋友说:“我要吃水果!”,这时候你送上去一个“苹果”,也应该是满足条件的,而送上去一个馒头可能就孤独一生了,这就是类型系统的作用.

代码中pb的静态类型是Base*,这个是不可改变的,在其定义时就已经决定了。 
但pb的动态类型是DerivedClass*,这个可以在运行时改变(这样才实现了多态

C++在面向对象编程中,存在着静态绑定动态绑定的定义,本节即是主要讲述这两点区分。 
是在一个类的继承体系中分析的,因此下面所说的对象一般就是指一个类的实例。 
首先我们需要明确几个名词定义:

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;必须搞清楚的一点是:动态绑定只有当我们指针或引用调用虚函数的时候才会发生。

从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。 
先看代码和运行结果:

  1. 1 class A
  2. 2 {
  3. 3 public:
  4. 4 /*virtual*/ void func(){ std::cout << "A::func()\n"; }
  5. 5 };
  6. 6 class B : public A
  7. 7 {
  8. 8 public:
  9. 9 void func(){ std::cout << "B::func()\n"; }
  10. 10 };
  11. 11 class C : public A
  12. 12 {
  13. 13 public:
  14. 14 void func(){ std::cout << "C::func()\n"; }
  15. 15 };

下面逐步分析测试代码及结果,

  1. 1 C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;
  2. 2 B* pb = new B(); //pb的静态类型和动态类型也都是B*;
  3. 3 A* pa = pc; //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
  4. 4 pa = pb; //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
  5. 5 C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;

如果明白上面代码的意思,请继续,

  1. 1 pa->func(); //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();
  2. 2 pc->func(); //C::func() pc的动、静态类型都是C*,因此调用C::func();
  3. 3 pnull->func(); //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;

如果注释掉类C中的func函数定义,其他不变,即

  1. 1 class C : public A
  2. 2 {
  3. 3 };
  4. 4
  5. 5 pa->func(); //A::func() 理由同上;
  6. 6 pc->func(); //A::func() pc在类C中找不到func的定义,因此到其**基类**中寻找;
  7. 7 pnull->func(); //A::func() 原因也解释过了;

如果为A中的void func()函数添加virtual特性,其他不变,即

  1. 1 class A
  2. 2 {
  3. 3 public:
  4. 4 virtual void func(){ std::cout << "A::func()\n"; }
  5. 5 };
  6. 6
  7. 7 pa->func(); //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用;
  8. 8 pc->func(); //C::func() pc的动、静态类型都是C*,因此也是先在C中查找;
  9. 9 pnull->func(); //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;

引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。当我们使用基类的指针或者引用调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能死派生类的一个对象。如果该函数时虚函数,则知道运行时才能知道到底执行哪一个版本,判断的依据是引用或者指针所绑定的对象的真实类型。

另一方面,对非虚函数的调用和通过对象进行的函数(虚函数或非虚函数)调用 
在编译期绑定。对象的类型是不变的,我们无论如何不能令对象的静态类型和动态类型不同(指针和引用可以不同)。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。

分析:

  1. 如果基类A中的func不是virtual函数,那么不论pa、pb、pc指向哪个子类对象,对func的调用都是在定义pa、pb、pc时的静态类型决定,早已在编译期确定了。

同样的空指针也能够直接调用no-virtual函数而不报错(这也说明一定要做空指针检查啊!),因此静态绑定不能实现多态

  1. 如果func是虚函数,那所有的调用都要等到运行时根据其指向对象的类型才能确定,比起静态绑定自然是要有性能损失的,但是却能实现多态特性;

本文代码里都是针对指针的情况来分析的,但是对于引用的情况同样适用。

至此总结一下静态绑定和动态绑定的区别:

  1. 静态绑定发生在编译期,动态绑定发生在运行期;

  2. 对象的动态类型可以更改,但是静态类型无法更改;

  3. 要想实现动态,必须使用动态绑定

  4. 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

建议:

绝对不要重新定义继承而来的非虚(non-virtual)函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的BUG

另外,在动态绑定也即在virtual函数中,要注意默认参数的使用。当缺省参数和virtual函数一起使用的时候一定要谨慎,不然出了问题怕是很难排查。 
看下面的代码:

  1. 1 class E
  2. 2 {
  3. 3 public:
  4. 4 virtual void func(int i = 0)
  5. 5 {
  6. 6 std::cout << "E::func()\t"<< i <<"\n";
  7. 7 }
  8. 8 };
  9. 9 class F : public E
  10. 10 {
  11. 11 public:
  12. 12 virtual void func(int i = 1)
  13. 13 {
  14. 14 std::cout << "F::func()\t" << i <<"\n";
  15. 15 }
  16. 16 };
  17. 17
  18. 18 void test2()
  19. 19 {
  20. 20 F* pf = new F();
  21. 21 E* pe = pf;
  22. 22 pf->func(); //F::func() 1 正常,就该如此;
  23. 23 pe->func(); //F::func() 0 哇哦,这是什么情况,调用了子类的函数,却使用了基类中参数的默认值!
  24. 24 }

为什么会有这种情况,请看《Effective C++ 第三版》 条款37。 
这里只给出建议: 
绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。

override关键字(C++ 11)

基类中的虚函数在派生类中隐含的也是一个虚函数。当派生类覆盖了虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。如果,函数名字相同但是形参列表不同,这是合法的,但是新定义的函数与基类中的函数时相互独立的。并没有发生覆盖,通常情况下,我们把这当做一种错误,因为我们希望它发生覆盖,但是不小心形参列表弄错了。调试并发现这样的错误很困难,因此新标准中引入了override关键字。在没有发生覆盖虚函数的情况下(比如:参数列表不同、基类中的这个函数不是虚函数或者是基类中没有该函数)不能通过编译。

相应的,我们还可以使用final将函数声明为不允许覆盖(只有虚函数才存在覆盖)的。

finaloverride出现在形参列表和尾置返回类型之后。

虚函数与默认实参

虚函数也可以拥有默认实参,如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。也就是说,如果我们通过基类的指针或者引用调用参数,则使用基类中定义的默认实参,即使实际运行的是派生类总的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不同。

回避虚函数机制

如果希望对虚函数的调用不要进行动态绑定,而是希望执行它的特定版本,使用作用域运算符可以实现这一目标。

baseP->Base::func();

通常情况下,只有成员函数和友元函数使用作用域运算符回避虚函数。如果一个派生类的虚函数需要调用它的基类版本,如果没有使用作用域运算符,则在运行时该调用将被解析成派生类版本自身的调用,从而导致无限循环。

http://m.blog.csdn.net/blog/yapian8/42460915#      ////原文

上一篇下一
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 三星手机版本低下载不了微信怎么办 选了动漫制作技术但不会画画怎么办 做主播高薪可是心累不愿做了怎么办 pr导出的avi无压缩太大怎么办 捡了个小米max被绑定了怎么办 二十岁时头发开始掉了怎么办 在酒店换衣服忘记关窗帘了怎么办 淘宝店铺装修更改图片要收费怎么办 惠阳市教育考试考证号忘记怎么办 高考完被被骗去读自考以后怎么办 孩子学习遇到瓶颈期了老师该怎么办 微信家长群有不好的言论出现怎么办 铃木汽车后备箱电动锁没有电怎么办 坐飞机没有连号座位带孩子怎么办 白沙的衣服洗衣服时染上颜色怎么办 网购商家少发了货怎么办 我想成为安利的员工怎么办会员 安利皇后锅锅盖吸在桌子上怎么办 淘宝客服退款返佣金诈骗后怎么办 第一试用网的钱提现出现问题怎么办 一个手机号注册两个京东账号怎么办 白色衣服被洗衣粉泡白了怎么办 白色衣服染成一块块荧光色了怎么办 中脉远红镇痛护腰不会发热了怎么办 用完悦诗风吟脸变黑不均匀怎么办 护肤品开封后一年还没用完怎么办 兰蔻化妆品套装正品和假怎么办 月经期间卫生巾搞得屁股疼怎么办 大姨妈特别多用卫生巾老是漏怎么办 夏天用卫生巾不透气摩擦红了怎么办 在日本的洗手间用完的姨妈巾怎么办 想穿短裙但是膝盖怕凉怎么办 裤子被卫生巾粘住扯不下来怎么办 医生说来姨妈不可以用卫生巾怎么办 隆胸以后摸起来感觉假体会动怎么办 产后15个月说恶露没排干净怎么办 母猪产后两天肚子里还有小猪怎么办 背心式无痕运动文胸显得胸小怎么办 卫生巾过敏起疙瘩反复挠不好怎么办 去健身房办卡老板跑了怎么办 买货我已经拒收商家不退款怎么办