Java和C++里面的重写/隐藏/覆盖
首先,无关重载。
注:重载是同一个类的各个函数之间的。重写是父类子类之间的。Overload和Overwrite(也叫Override)的区别。
注意:Java里面区分重写(Override/Overwrite)与隐藏(Hide?)。而C++里面区分的是覆盖(Override)和隐藏/重写(Overwrite)。文字游戏,区分清楚就好了。
这里主要谈的是函数重写与隐藏
首先,我的理解:重写和隐藏是互斥的、相对的。父子中都存在的函数,不是重写就是隐藏。
重写和隐藏的本质区别是:重写是动态绑定的,根据运行时引用所指向对象的实际类型来决定调用相关类的成员。而隐藏是静态绑定的,根据编译时引用的静态类型来决定调用的相关成员。换句话说,如果子类重写了父类的方法,当父类的引用指向子类对象时,通过父类的引用调用的是子类方法。如果子类隐藏了父类的方法(成员变量),通过父类的引用调用的仍是父类的方法(成员变量)。
(注:这一句话非常绕,说的是子类隐藏了父类的方法,但调用的还是父类的方法,还不如说是父类隐藏了子类的方法。其实原义是,是针对子类引用说的隐藏,指的是子类引用调用子类,不调用父类;而父类引用仍然调用父类。)
Java的隐藏和C++的隐藏是有区别的。也不能说完全不同,但是重写的覆盖面和默认采用方式不同。
C++里面的重写,一般叫作覆盖。
C++里面的隐藏,子类会把父类中其他类型的方法都隐藏掉,使得不能调用。
Java里面的隐藏,只针对参数一样的static函数,参数不一样的static函数,照样不会隐藏,子类能够调用。
下面都有例子。
Java
先说Java的隐藏(参考 Link)
覆盖则指的是父类引用指向了子类对象,调用的时候会调用子类的具体方法;
隐藏指的是“子类把父类的属性或者方法隐藏了”,即将子类强制转换成父类后,调用的还是父类的属性和方法。(引号内的容易引起歧义,可以忽略)
(1) 变量只能被隐藏(包括静态和非静态),不能被覆盖
(2) 可以用子类的静态变量隐藏父类的静态变量,也可以用子类的非静态变量隐藏父类的静态变量,也可以用非最终变量(final)隐藏父类中的最终变量;
(3) 静态方法(static)只能被隐藏,不能被覆盖;
(4) 非静态方法可以被覆盖;
(5) 不能用子类的静态方法隐藏父类中的非静态方法,否则编译会报错;
(6) 不能用子类的非静态方法覆盖父类的静态方法,否则编译会报错;
(7) 不能重写父类中的最终方法(final);
(8) 抽象方法必须在具体类中被覆盖;
简单讲,父类和子类的方法的静态性必须一样。要么都有static,要么都没有,否则会编译报错,已实验。
注:Java,我认为的,对于“隐藏”,好的记忆方法是指向子类实例的父类指针(引用),看到的仍然是父类的方法,而把子类的方法给“隐藏”了。C++里面,因为涉及到参数不同的父类函数被隐藏,那才是叫作真的隐藏。
实例,我在Intellij上面实验了,如下:
package com.company;
class Solution {
}
class SuperClass {
public static int i = 1;
public int j = 2;
public final int k = 3;
public static void method1() {
System.out.println("SuperClass Method1");
}
public void method2() {
System.out.println("SuperClass Method2");
}
public final void method3() {
System.out.println("SuperClass Method3");
}
}
class SubClass extends SuperClass {
public static int i = 2;//无论是不是static,都能隐藏父类的变量i
public static int j = 1;
public final int k = 4;//无论是不是final,都能隐藏父类的变量k
public static void method1() {
System.out.println("SubClass Method1");
}
public void method2() {
System.out.println("SubClass Method2");
}
/*public final void method3() {
System.out.println("SuperClass Method3");
}*/
}
public class Main {
public static void main(String[] args) throws InterruptedException {
SuperClass sc = new SubClass();
System.out.println("i = " + sc.i); // 所有的成员变量,只能被隐藏
System.out.println("j = " + sc.j);
System.out.println("k = " + sc.k);
sc.method1();//静态方法只能被隐藏
sc.method2();
SubClass subc = new SubClass();
System.out.println("i = " + subc.i);
System.out.println("j = " + subc.j);
System.out.println("k = " + subc.k);
subc.method1();
subc.method2();
// Your Codec object will be instantiated and called as such:
//System.out.printf("ret:%d\n", ret);
System.out.println();
}
}
打印结果:
i = 1
j = 2
k = 3
SuperClass Method1
SubClass Method2
i = 2
j = 1
k = 4
SubClass Method1
SubClass Method2
把上面子类里面变量的static和final去掉:
public int i = 2;//无论是不是static,都能隐藏父类的变量i public static int j = 1; public int k = 4;//无论是不是final,都能隐藏父类的变量k
打印的结果和原来的一致:
i = 1
j = 2
k = 3
SuperClass Method1
SubClass Method2
i = 2
j = 1
k = 4
SubClass Method1
SubClass Method2
C++
而C++里面的隐藏,和Java里面的隐藏的语义,不太一样,参考 Link:
如果派生类的函数与基类的函数同名, 但是参数不同. 此时, 不论有无 virtual 关键字, 基类的函数将被隐藏(注意别与重载混淆).
如果派生类的函数与基类的函数同名, 并且参数也相同, 但是基类函数没有 virtual 关键字. 此时, 基类的函数被隐藏(注意别与覆盖混淆).
也就是说,C++的重写,只跟virtual关键字有关。如果没有这个关键字,那么父类中的方法和子类是没有关系的。即使用了virtual,如果方法参数不一样,也不重载,而是采用隐藏。(隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。)
而Java默认是重载,只有static方法和变量,是不重载,而采用隐藏的。
C++代码示例如下,在m42n03机器的 /home/work/data/code/overloadnhide 目录:
#include <iostream> using namespace std; class Base { public: virtual void f(float x){cout << "Base::f(float) " << x << endl;} virtual void f1(float x){cout << "Base::f1(float) " << x << endl;} void g(float x){cout << "Base::g(float) " << x << endl;} void h(float x){cout << "Base::h(float) " << x << endl;} }; class Derived : public Base { public: virtual void f(float x){cout << "Derived::f(float) " << x << endl;} virtual void f1(int x){cout << "Derived::f1(int) " << x << endl;} void g(int x) {cout << "Derived::g(int) " << x << endl;} void h(float x){cout << "Derived::h(float) " << x << endl;} }; int main() { Derived d; Base *pb = &d; Derived *pd = &d; // No hide , only overwrite pb->f(3.14f); pd->f(3.14f); //Derived::f(float) 3.14 pb->f1(3.14f); pd->f1(3.14f); // hide pb->g(3.14f); //Base::g(float) 3.14 pd->g(3.14f); //Derived::g(int) 3 (surprise!) // hide pb->h(3.14f); //Base::h(float) 3.14 (surprise!) pd->h(3.14f); //Derived::h(float) 3.14 return 0; }
编译命令及输出:
g++ -Wall -o test test.cpp test.cpp: In function `int main()': test.cpp:35: warning: passing `float' for converting 1 of `virtual void Derived::f1(int)' test.cpp:39: warning: passing `float' for converting 1 of `void Derived::g(int)' 有两个类型转换的warning
运行命令:
Derived::f(float) 3.14 Derived::f(float) 3.14 Base::f1(float) 3.14 Derived::f1(int) 3 Base::g(float) 3.14 Derived::g(int) 3 Base::h(float) 3.14 Derived::h(float) 3.14
可以看到,只有第一种情况(有virtual,并且父子类方法参数一样)才是Override覆盖,其他的情况全是隐藏。
java 的函数是没有 virtual 关键字的, 但是派生类和基类只要函数名和参数相同, 那么该函数就被覆盖了. 如果反过来想, 相对于 C++, 那不是 java 的每个函数都是虚函数吗? 可能C++ 在于效率上考虑, 不想所有的函数都使用动态联编.
g(float) 和 g(int) 是不同的函数, C++编译后在符号库中的名字分别是 _g_float 和 _g_int.即使他们都有 virtual 关键字, 但是因为是分别存在与派生类和基类中的不同函数, 所以在不存在覆盖的关系.
而且,C++里面一旦看到一个名称是g的函数,就不会再往上看父类中有没有其他参数类型的函数,如果子类中定义的g函数类型不对,直接编译报错。不管父类中的正确方法有没有加virtual函数,都是这样的。
但是,指向子类实例的父类指针,是可以正确调用这个其他类型的参数的方法的。
说明不同类型的同名函数,在C++子类中被隐藏了。
关于函数隐藏的更具体的例子
C++
这次先看C++的例子(感觉C++的例子更极端):
#include <iostream> using namespace std; class Base { public: virtual void f(float x){cout << "Base::f(float) " << x << endl;} virtual void f(int x, int y){cout << "Base::f(int, int) " << x << "," << y << endl;} virtual void f1(float x){cout << "Base::f1(float) " << x << endl;} void g(float x){cout << "Base::g(float) " << x << endl;} void g(int x, int y){cout << "Base::g(int, int) " << x << "," << y << endl;} void h(float x){cout << "Base::h(float) " << x << endl;} }; class Derived : public Base { public: virtual void f(float x){cout << "Derived::f(float) " << x << endl;} virtual void f1(int x){cout << "Derived::f1(int) " << x << endl;} void g(int x) {cout << "Derived::g(int) " << x << endl;} void h(float x){cout << "Derived::h(float) " << x << endl;} }; int main() { Derived d; Base *pb = &d; Derived *pd = &d; // No hide , only overwrite pb->f(3.14f); pd->f(3.14f); //Derived::f(float) 3.14 pb->f(1, 2); pd->f(1, 2); // to be removed pb->f1(3.14f); pd->f1(3.14f); // hide pb->g(3.14f); //Base::g(float) 3.14 pd->g(3.14f); //Derived::g(int) 3 (surprise!) // diffrent param pb->g(1, 2); pd->g(1, 2); // to be removed // hide pb->h(3.14f); //Base::h(float) 3.14 (surprise!) pd->h(3.14f); //Derived::h(float) 3.14 return 0; }
注意上面飘红的部分,是新加的。
编译,直接出错。错误在pd调用的两个地方:
g++ -Wall -o test test.cpp test.cpp: In function `int main()': test.cpp:37: error: no matching function for call to `Derived::f(int, int)' test.cpp:19: note: candidates are: virtual void Derived::f(float) test.cpp:40: warning: passing `float' for converting 1 of `virtual void Derived::f1(int)' test.cpp:44: warning: passing `float' for converting 1 of `void Derived::g(int)' test.cpp:48: error: no matching function for call to `Derived::g(int, int)' test.cpp:21: note: candidates are: void Derived::g(int)
去掉pd调用的两行,见上面代码注释(// to be removed)部分。编译通过:
$ g++ -Wall -o test test.cpp test.cpp: In function `int main()': test.cpp:40: warning: passing `float' for converting 1 of `virtual void Derived::f1(int)' test.cpp:44: warning: passing `float' for converting 1 of `void Derived::g(int)' $ ./test Derived::f(float) 3.14 Derived::f(float) 3.14 Base::f(int, int) 1,2 Base::f1(float) 3.14 Derived::f1(int) 3 Base::g(float) 3.14 Derived::g(int) 3 Base::g(int, int) 1,2 Base::h(float) 3.14 Derived::h(float) 3.14
可见,父类函数加不加virtual,都不影响它在被覆盖的子类里面,已经不可见了。
JAVA
关于这个例子,Java就完全不一样,上代码:
package com.company; class Solution { } class SuperClass { public static int i = 1; public int j = 2; public final int k = 3; public static void method1() { System.out.println("SuperClass Method1"); } public static void method1(int a) { System.out.println("SuperClass Method1 with int " + a); } public void method2() { System.out.println("SuperClass Method2"); } public void method2(int a) { System.out.println("SuperClass Method2 with int " + a); } public final void method3() { System.out.println("SuperClass Method3"); } } class SubClass extends SuperClass { public int i = 2;//无论是不是static,都能隐藏父类的变量i public static int j = 1; public int k = 4;//无论是不是final,都能隐藏父类的变量k public static void method1() { System.out.println("SubClass Method1"); } public void method2() { System.out.println("SubClass Method2"); } /*public final void method3() { System.out.println("SuperClass Method3"); }*/ } public class Main { public static void main(String[] args) throws InterruptedException { SuperClass sc = new SubClass(); System.out.println("i = " + sc.i); // 所有的成员变量,只能被隐藏 System.out.println("j = " + sc.j); System.out.println("k = " + sc.k); sc.method1();//静态方法只能被隐藏 sc.method1(3); sc.method2(); sc.method2(3); sc.method3(); SubClass subc = new SubClass(); System.out.println("i = " + subc.i); System.out.println("j = " + subc.j); System.out.println("k = " + subc.k); subc.method1(); subc.method1(3); subc.method2(); subc.method2(3); subc.method3(); // Your Codec object will be instantiated and called as such: //System.out.printf("ret:%d\n", ret); System.out.println(); } }
增加的内容,见以上飘红的部分。编译,通过!
i = 1 j = 2 k = 3 SuperClass Method1 SuperClass Method1 with int 3 SubClass Method2 SuperClass Method2 with int 3 SuperClass Method3 i = 2 j = 1 k = 4 SubClass Method1 SuperClass Method1 with int 3 SubClass Method2 SuperClass Method2 with int 3 SuperClass Method3
从上面可以看出。和C++不一样!
子类中没有定义的,父类中有的不同参数的函数,照样能够在父类和子类引用里面调用(实例都是子类的实例)。
总结:
Java里面,
默认Override,如果是子类的示例,那么不管引用是通过父类和子类,都会调用子类的方法;
如果是static方法,那么即使是子类的实例,父类引用和子类引用,会分别调用各自的方法;
如果子类中没有实现某种参数的方法,父类中有同名的,不管是不是static,都会调用父类的方法。
C++里面,
默认不是Override,除非加上virtual关键字并且父子函数参数完全一致,那么形成覆盖,如果是子类的示例,通过父子指针,都会调用子类的方法;
其他的情况,即使是子类的实例,父子指针,会分别调用各自的方法;
如果子类中没有实现某种参数的方法,父类中有同名的,子类指针不能调用,编译报错;父类指针(子类实例)能够调用父类的方法。
(完)