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