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<<"====================\n";
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中的相同语句,这就是虚函数和多态的意义。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。