我尝试从父类的构造函数中调用重写方法,并注意到跨语言的不同行为。
C++
- 回应A.foo()
class A{
public:
A(){foo();}
virtual void foo(){cout<<"A.foo()";}
};
class B : public A{
public:
B(){}
void foo(){cout<<"B.foo()";}
};
int main(){
B *b = new B();
}
Java
- 回应B.foo()
class A{
public A(){foo();}
public void foo(){System.out.println("A.foo()");}
}
class B extends A{
public void foo(){System.out.println("B.foo()");}
}
class Demo{
public static void main(String args[]){
B b = new B();
}
}
C#
- 回应B.foo()
class A{
public A(){foo();}
public virtual void foo(){Console.WriteLine("A.foo()");}
}
class B : A{
public override void foo(){Console.WriteLine("B.foo()");}
}
class MainClass
{
public static void Main (string[] args)
{
B b = new B();
}
}
我意识到C ++对象是从层次结构中最顶层的父进程创建的,所以当构造函数调用重写方法时,B甚至不存在,因此它调用方法的A'版本。但是,我不确定为什么我在Java和C#(来自C ++)中获得不同的行为
答案 0 :(得分:26)
在C ++中,正如您所正确指出的那样,该对象的类型为A
,直到A
构造函数完成。该对象实际上在构造期间改变了类型。这就是使用A
类vtable的原因,因此调用A::foo()
而不是B::foo()
。
在Java和C#中,即使在构建基类期间,也始终使用最派生类型的vtable(或等效机制)。因此,在这些语言中,B.foo()
被调用。
请注意,通常不建议从构造函数中调用虚方法。如果你不是很小心,虚拟方法可能会认为对象是完全构造的,即使不是这种情况。在Java中,每个方法都是隐式虚拟的,你别无选择。
答案 1 :(得分:9)
虽然我知道您正在为实验做这件事,但重要的是要注意 Effective Java 2nd Edition,第17项:设计和继承文档中的以下引用,否则禁止它:< / p>
为了允许继承,类必须遵守一些限制。 构造函数不得直接或间接调用可覆盖的方法。如果违反此规则,将导致程序失败。超类构造函数在子类构造函数之前运行,因此在子类构造函数运行之前将调用子类中的重写方法。如果重写方法依赖于子类构造函数执行的任何初始化,则该方法将不会按预期运行。
以下是一个例子来说明:
public class ConstructorCallsOverride {
public static void main(String[] args) {
abstract class Base {
Base() { overrideMe(); }
abstract void overrideMe();
}
class Child extends Base {
final int x;
Child(int x) { this.x = x; }
@Override void overrideMe() {
System.out.println(x);
}
}
new Child(42); // prints "0"
}
}
此处,当Base
构造函数调用overrideMe
时,Child
尚未完成初始化final int x
,并且该方法的值不正确。这几乎肯定会导致错误和错误。