最近写c++代码,发现一个和java不一样的地方,就是在构造函数中,调用虚函数,似乎C++的虚函数表没生效:
参考如下代码:
c++
#include <iostream>
class Base {
public:
Base() {
A(); // 调用虚函数
}
virtual void A() {
std::cout << "Base::A()" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {}
void A() override {
std::cout << "Derived::A()" << std::endl;
}
};
int main() {
Derived d;
}
//输出 Base::A()
而java:
java
class Base {
Base() {
print(); // 调用子类重写的方法
}
void print() {
System.out.println("Base");
}
}
class Derived extends Base {
private int value = 10;
@Override
void print() {
System.out.println("Derived: " + value);
}
}
public class Main {
public static void main(String[] args) {
Derived d = new Derived(); // 输出:Derived: 0
}
}
深入研究发现区别在于:
-
Java :对象在构造前就已分配内存,包括所有成员变量(已设置默认值 ,但是未初始化)
Java 的对象模型是统一的:Java 的对象从创建的第一刻起就知道它们的最终类型。在 Java 中,当你创建一个派生类对象时,虚表(vtable)已经绑定到派生类的实现。因此,哪怕对象还在构造中,调用的方法都会是派生类重写的方法。
-
C++:对象逐步构造,未构造部分包含未初始化的内存
在 C++ 中,构造过程从基类到派生类逐步进行。在基类的构造阶段,派生类的部分成员可能还未初始化,因此动态多态机制(虚表)还没有完全就绪。此时,调用虚函数会调用基类版本,而不是派生类的重写版本。
C++ vptr的初始化时机
class MyClass : public Base1, public Base2 {
// 构造函数调用顺序:
// 1. Base1构造函数(vptr = Base1的vtable)
// 2. Base2构造函数(vptr = Base2的vtable)
// 3. MyClass构造函数(vptr = MyClass的vtable)
};
构造过程中,对象处于"半构造"状态:
- 基类部分已构造完成
- 派生类部分尚未构造
- vptr需要逐步更新到正确的vtable
所以C++ 在构造函数中调用虚函数无法生效
java机制的合理吗?
可以看到上面的例子里面,输出是 :Derived: 0 而不 Derived: 10 就说明问题了。
java对象在构造前就已分配内存,包括所有成员变量(已设置默认值 ,但是未初始化)
子类方法可能依赖未初始化的字段可能导致不可预测的行为,导致调试困难,所以最好也不要这样使用。
《Effective Java》第19条也明确明确建议:
"在构造函数中不要调用可被覆盖的方法,无论是直接还是间接调用。"