• C++父子类继承时的隐藏、覆盖、重载


      存在父子类继承关系时,若有同名成员函数同时存在,会发生隐藏、覆盖和重载这几种情况。对于初学者也比较容易混淆,为此,我整理了一下我的个人看法,仅供参考。希望对大家理解有帮助,也欢迎指正。

    1.父子类继承关系: 子类复制父类全部成员

      首先,理解父子类的继承关系是怎样发生的。在此基础上就很容易理解它们之间的关系和区别。  

      每一个类有它自己的成员变量和成员函数,是一个独立的空间整体。当子类继承父类时,会将父类的全部成员全部复制一份,作为子类的成员,但是,同时也会标记这些成员是从父类中继承的,与子类本身的成员,还是有区别的。这里认为将子类本身的成员存在子类域,从父类复制过来的存在父类域。

    如下图,Childer类中存在两个域,子类域和父类域,相互之间互不干扰。

     1 class Father
     2 {
     3     int f_a;
     4     int f_b;
     5 };
     6 
     7 class Childer:public Father
     8 {
     9     int c_a;
    10     int f_b;
    11 };
    12 
    13 int main()
    14 {
    15     cout<<"sizeof childer:"<<sizeof(Childer)<<endl;   //-> 16
    16     cout<<"sizeof father:"<<sizeof(Father)<<endl;     //-> 8
    17 }

     运行结果显示,子类大小为16,父类大小为8,也就是说子类的确有4个成员变量,就算是同名成员,也同样复制。

    2.隐藏:子类对象优先考虑子类域自身成员(成员变量和成员函数)

       隐藏发生的主要原因,就是当子类有父类的同名成员时,子类对象访问该成员时,会发生冲突。所以编译器的处理方式是,优先考虑子类域中的自身成员。

    即,子类对象访问某成员时,如ch.m_m 或者ch.f(),成员变量和成员函数都一样。编译器首先在子类域中检索,如果在子类域中找到该成员,则检索结束,返回该成员进行访问。如果在子类域中找不到该成员,则去父类域中检索。如果父类域中存在,则返回该成员进行访问,如果父类域中也不存在,则编译错误,该成员无效。

      当父子类域都存在同一成员时,编译器优先在子类中检索,就算父类域中也存在该同名成员,也不会被检索到。因此,父类域中的该成员被子类域中的该同名成员隐藏,即访问时完全以为该成员不存在,如果想访问父类域中的该成员,只能通过显示调用的方式,即:ch.Father::m_m;

            

     下面用代码说明,为了对问题有针对性说明,此处成员都采用public,也不涉及构造析构等问题。

     1 class Father
     2 {
     3 public:
     4     int f_a;
     5     int f_b;
     6 
     7     void ff1() {cout<<"father ff1"<<endl;}
     8 };
     9 
    10 class Childer:public Father
    11 {
    12 public:
    13     int c_a;
    14     int f_b;
    15 
    16     void cf1() {cout<<"childer cf1"<<endl;}
    17     void ff1() {cout<<"childer ff1"<<endl;}
    18 };
    19 
    20 int main()
    21 {
    22     Childer ch;
    23     
    24     cout<<ch.c_a<<endl; //只在子类域中的成员变量
    25     cout<<ch.f_b<<endl; //子类域和父类域都存在,优先访问子类域中的
    26     cout<<ch.Father::f_b<<endl; //显示访问被隐藏的成员变量
    27 
    28     cout<<"====================
    ";
    29     
    30     ch.cf1();
    31     ch.ff1();
    32     ch.Father::ff1();
    33 }

     

     运行结果可以看出,ch.f_b;  和 ch.Father::f_b;  两个同名成员同时存在。但访问时,子类成员将父类成员隐藏,想访问父类成员只能显示调用。

    通过成员函数的访问,这一效果更明显,ch.ff1();调用时,调用了子类域中的该同名成员函数。

      且此时编译器检索时,只根据名字,与函数的参数和返回类型无关。

    1 int ff1(int a ) {cout<<"childer ff1"<<endl;return 0;}

    若将Childer中的函数,改为上述类型。主函数中调用时,ch.ff1();编译错误。因为子类的int ff1(int a);会将父类的void ff1();隐藏。所以它们之间不存在重载。

    应该改为 ch.ff1(10); 这样会匹配子类域中的该成员。或者ch.Father::ff1();显示调用父类域中的成员。

    3.覆盖:虚函数,成员函数类型一摸一样,父类指针调用子类对象成员

     覆盖只发生在有虚函数的情况下,且父子类成员函数类型必须一摸一样,即参数和返回类型都必须一致。子类对象调用时,会直接调用子类域中的成员函数,父类域中的该同名成员就像不存在一样,(可以显示调用)即父类该成员被子类成员覆盖。这里很多人会感觉疑惑,认为是隐藏,因为父类的成员函数依然存在,依然可以调用,只是优先调用子类的,也就是“隐藏”了。而“覆盖”两个字的意思,应该是一个将另一个替代了,也就是另一个不存在了。

      举个小例子可以很明显的看出,覆盖的情况下,父子类的成员函数也是同时存在的。

    virtual void ff1() {cout<<"father ff1"<<endl; }

    将上面的例子Father类中的ff1函数加上virtual,其他不进行改变,运行结果也不变。

      下面解释一下,“覆盖”二字的由来。

    首先需明白一点,虚函数的提出,是为了实现多态。也就是说,虚函数的目的是为了,在用父类指针指向不同的子类对象时,调用虚函数,调用的是对应子类对象的成员函数,即可以自动识别具体子类对象。所以,上述例子中,直接用子类对象调用虚函数是没有意义的,一般情况也不会这样使用。

     1 class Father
     2 {
     3 public:
     4     virtual void ff1() {cout<<"father ff1"<<endl;}
     5 };
     6 
     7 class Childer_1:public Father
     8 {
     9 public:
    10     void ff1() {cout<<"childer_1 ff1 "<<endl;}
    11 };
    12 class Childer_2:public Father
    13 {
    14 public:
    15     void ff1() {cout<<"childer_2 ff1"<<endl; }
    16 };
    17 
    18 int main()
    19 {
    20     Father* fp;
    21 
    22     Childer_1 ch1;
    23     fp = &ch1;
    24     fp->ff1();
    25 
    26     Childer_2 ch2;
    27     fp = &ch2;
    28     fp->ff1();
    29     
    30     return 0;
    31 }

      使用虚函数,都是父类指针的形式,pf->f11() 。例子中的24行和28行,相同的代码,因为fp的指向不同对象,所以调用不同对象的虚函数。但从代码上看,fp是一个Father类的指针,但调用的是子类成员函数,就好像父类的成员被覆盖了一样。这就是覆盖一词的来源。

    覆盖的情况下,子类虚函数必须与父类虚函数有相同的参数列表,否则认为是一个新的函数,与父类的该同名函数没有关系。但不可以认为两个函数构成重载。因为两个函数在不同的域中。

     举例:

     1 class Father
     2 {
     3 public:
     4     virtual void ff1() {cout<<"father ff1"<<endl;}
     5 };
     6 
     7 class Childer_1:public Father
     8 {
     9 public:
    10     void ff1(int a) {cout<<"childer_1 ff1 "<<endl; }
    11 };
    12 
    13 int main()
    14 {
    15     Father* fp;
    16 
    17     Childer_1 ch1;
    18     fp = &ch1;
    19     fp->ff1();
    20    //ch1.ff1(); //没有匹配的成员
    21     ch1.ff1(2);
    22 
    23     return 0;
    24 }

    运行结果为:

    father ff1
    childer_1 ff1

    从19行 fp->ff1();的运行结果可以看出,fp虽然指向子类对象,并且调用的是虚函数。但是该虚函数,在子类中没有对应的实现,只好使用父类的该成员。

    即第10行的带参ff1 并没有覆盖从父类中继承的无参ff1. 而是认为是一个新函数。

    4.重载:相同域的同名不同参函数

      重载必须是发生在同一个域中的两个同名不同形参之间的。如果一个在父类域一个在子类域,是不会存在重载的,属于隐藏的情况。调用时,只会在子类域中搜索,如果形参不符合,会认为没有该函数,而不会去父类域中搜索。

    5.总结

      重载是在同一域下的函数关系,在父子类情况下时,一般不予考虑。

      隐藏,是子类改写、重写了父类的代码。而覆盖认为,子类实现了父类的虚函数。父类的虚函数可以没有实现体,成为纯虚函数,等着子类去实现。而隐藏时,父类的函数也必须有实现体的。隐藏还是覆盖,只是说法不同,只要明白编译器在调用时,如果检索、匹配相应的函数即可。

    综上所述,总结为以下几点:

    1.子类是将父类的所有成员都复制一份,并且保存在不同的域中。如果同名,子类中会有两份,分别在子类域和父类域。

    2.调用时,是从调用对象(或指针)的类型开始检索的,先从自己域中检索,如果找到,判断是否为虚函数,不为虚函数直接调用,若为虚函数,通过运行时类型识别,调用真正对象的函数。如果没找到,去其父类域中检索,重复刚刚的判断。直到调用函数或者没有匹配的成员。而不会去子类中检索,所以如果是父类指针,即使指向子类对象,但调用的函数也只能是父类中的函数,除非是虚函数,才会根据子类对象去检索函数。

    明白调用过程:

    2.1  一般情况下,哪种类型的,就调哪种类型对于自己域中的成员。

    Father f;   f.a; f.ff1(); 由于f是Father类型的,所以调用的都是Father自己域中的成员。

    Childer c; c.a; c.ff1(); 由于c是Chiler类型的,所以调用的都是Childer自己域中的成员。

    指针也一样。Father*fp;  fp->a;  fp->ff1();   由于fp是Father类型的指针,所以调用的都是Father自己域中的成员。

                就算fp = new Childer. fp->ff1(); 指向的是子类对象,依然调用父类自己的成员。因为fp是Father类型的。

                    Childer *cp; cp->a; cp->ff1();   由于cp是Childer类型的指针,所以调用的都是Childer自己域中的成员。

    2.2 .而有一种情况特殊,则是,当成员函数为虚函数时,虽然是父类类型的指针,但会根据指针指向的具体对象,调用该函数。
      即,如果ff1为虚函数,Father*fp; fp = new Childer; fp->ff1();   虽然fp是Father类型的指针,但由于ff1是虚函数,所以调用的是具体对象,Childer类的成员。

    对比2中的相同语句,这就是虚函数和多态的意义。

    路是一步一步走的
  • 相关阅读:
    《做衣服:破坏时尚》总结
    《程序员的思维修炼》总结
    纸玫瑰和鲜玫瑰,选择哪个?
    《古怪的身体:时尚是什么》总结
    《世界尽头的咖啡馆》总结
    《软技能:代码之外的生存指南》总结
    构造无限级树的框架套路,附上python/golang/php/js实现
    《Dior的时尚笔记》总结
    《编写可读代码的艺术》总结
    《费曼学习法》总结
  • 原文地址:https://www.cnblogs.com/Lalafengchui/p/3994340.html
Copyright © 2020-2023  润新知