10

我想知道动态调度在 C++ 中是如何工作的。为了说明我的问题,我将从一些 Java 代码开始。

class A
{
  public void op(int x, double y) { System.out.println("a"); }
  public void op(double x, double y) { System.out.println("b"); }
}

class B extends A
{
  public void op(int x, double y) { System.out.println("c"); }
  public void op(int x, int y) { System.out.println("d"); }
}

class C extends B
{
  public void op(int x, int y) { System.out.println("e"); }
}

public class Pol
{
  public static void main(String[] args)
  {
    A a = new C();
    B b = new C();

    /* 1 */ a.op(2, 4);
    /* 2 */ b.op(2.0, 4.0);
  }
}

该调用a.op(2, 4)将打印“c”,因为确实是编译器:

  • 查看 Class A(因为a被声明为类型的变量A)哪个方法最接近op(int, int),
  • 找不到op(int, int)方法但找到了方法op(int, double)(使用单个自动转换int-> double),
  • 然后修复此签名。

在执行期间,JVM:

  • op(int, double)在 Class 中寻找具有编译器固定签名的方法,C但没有找到,
  • 查看 C 的超类内部,即B,
  • 最后找到一个方法op(int, double),然后调用它。

同样的原则也适用于b.op(2.0, 4.0)打印“b”的调用。

现在,考虑 C++ 中的等效代码

#include <iostream>

class A
{
public:
  virtual void op(int x, double y) { std::cout << "a" << std::endl; }
  virtual void op(double x, double y) { std::cout << "b" << std::endl; }
};

class B : public A
{
public:
  void op(int x, double y) { std::cout << "c" << std::endl; }
  virtual void op(int x, int y) { std::cout << "d" << std::endl; }
};

class C : public B
{
public:
  void op(int x, int y) { std::cout << "e" << std::endl; }
};

int main()
{
  A *a = new C;
  B *b = new C;

  /* 1 */ a->op(2,  4);
  /* 2 */ b->op(2.0, 4.0);

  delete a;
  delete b;
}

a->op(2, 4)将打印“c”,如 Java。但是b->op(2.0, 4.0)再次输出“c”,我迷路了。

在编译和在 C++ 中执行期间用于动态调度的规则到底是什么?(请注意,如果您virtual在每个函数前面编写代码,C++ 代码将具有相同的行为;这里没有任何改变)

4

4 回答 4

3

对于 C++,当您执行b->op(2.0, 4.0);编译器查找时B,会找到它可以调用的方法(int x, double y)并使用它。如果子类中的任何方法可以处理调用,它不会查看超类。这称为方法隐藏,即。op(double, double)是隐藏的。

如果要使其选择(double x, double y)版本,则需要在内部B使用以下声明使函数可见B

using A::op;

规则的进一步解释

于 2013-09-20T09:32:35.887 回答
1

通过在 in 中声明一个新的重载opB您已经隐藏了基本版本。编译器只会根据“B”分派,这就是它选择op(int,double).

于 2013-09-20T09:34:05.193 回答
1

如果您告诉编译器,编译器将在转换时发出警告/错误。使用 gcc,编译器参数-Wconversion -Werror将阻止您的代码编译,您是对的,这里可能会降低精度。

鉴于您没有打开此编译器选项,编译器很乐意将您对 b->op(double, double) 的调用解析为 B::op(int, double)。

请记住,这是一个编译时决定 - 而不是运行时/多态决定。

“b”指针的实际 vtable 将在运行时有一个方法 op(int, int) 可用,但编译器在编译时不知道这个方法。它只能假设 b 指针的类型为 B*。

于 2013-09-20T10:22:16.437 回答
0

您从基类中的多态行为开始A。然后,使用相同的签名,您无法在派生类中停止此操作。

如果您声明相同的方法,则没有必要virtual

你必须改变签名!

此外,您还有可见性问题。这条线

B *b = new C;
b->op(2.0, 4.0);

编译器在您的类中查找方法Bop-methods 隐藏类名相同的方法(A重载解决方案)。如果他发现了一些有用的东西,他只是在使用它。

于 2013-09-20T09:32:22.923 回答