C++中动态分派的规则是什么?

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",因为编译器:

  • 查找类 A(因为 a 被声明为类型为 A 的变量)中最接近 op(int, int) 的方法,
  • 找不到 op(int, int) 方法,但找到了方法 op(int, double)(通过一个自动转型将 int 转换为 double),
  • 然后修复方法签名。

执行期间,JVM:

  • 在由编译器修复的签名 op(int, double) 中查找类 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) 将像 Java 一样打印 "c"。但是 b->op(2.0, 4.0) 再次输出 "c",我对此感到迷惑。

C++ 中在编译期和执行期间应用于动态分派的确切规则是什么? (请注意,如果在每个函数前面写上virtual,则 C++ 代码将具有相同的行为;在这里不会改变任何东西)

4个回答

3
对于C ++,当您执行b->op(2.0, 4.0)时,编译器会在B中查找可调用的方法(int x, double y)并使用它。如果子类中的任何方法可以处理调用,则不会查找超类。这被称为方法隐藏,即op(double,double)被隐藏了。
如果您想使其选择(double x, double y)版本,则需要在B内部使用以下声明使函数可见:
using A::op;

规则的进一步解释

这是关于C++中“隐藏规则”的解释。

我们正在讨论C++中的“基类”,而不是“超类”。 - thokra
我还是有些困惑。我现在理解了方法隐藏原则,但是通过 b->op(2.0, 4.0) 我们正在请求一个带有两个浮点数参数的函数。编译器怎么会认为选择一个带有 op(int, double) 签名的函数是个好主意呢?它不能(不应该?)从 double 自动转换为 int;这样会有精度损失的风险。 - Florian Richoux
1
@FlorianRichoux double 隐式可转换为 int,且 void (int, double)void (int, int) 更匹配。A 中的 void (double, double) 从未被看到,因为一旦在 B 中找到 op,名称查找就结束了。 - Simple
如果B没有定义op,那么名称查找将无法在B中找到它,因此它会在基类中查找。这种情况在任何范围内都会发生;类在这方面并不特殊。以全局函数foo和命名空间中的一个函数为例;如果您调用foo,它会在您的命名空间中找到该函数,然后停止,从未找到全局的foo函数(忽略ADL)。 - Simple
1
@FlorianRichoux C++ 中的隐式转换规则比 Java 宽松得多。可能出现精度损失并不会阻止编译器执行此操作;只是可能会导致它在正确的标志下发出警告。 - Sebastian Redl

1
通过在类 B 中声明一个新的重载版本给 op,你已经隐藏了基类版本。编译器只会根据类 B 进行分发,这就是为什么它选择了 op(int, double)

1
编译器会在转换时发出警告/错误,如果您告诉它的话。使用gcc编译器参数-Wconversion -Werror将防止您的代码编译通过,因为您是正确的,在这里存在潜在的精度损失。
考虑到您没有打开此编译器选项,编译器很高兴将您的调用解析为b->op(double, double)到B::op(int, double)。
请记住,这是一个编译时决策-而不是运行时/多态决策。
“b”指针的实际vtable在运行时将有一个可用的op(int, int)方法,但编译器在编译时不知道这个方法。它只能假定b指针是B*类型。

0

你从基类 A 中开始使用多态行为。然后,使用相同的签名,你不能在派生类中停止这个行为。

如果你声明相同的方法 virtual 或不声明,这并不是必要的。

你必须改变签名!

此外,你还有可见性问题。这一行

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

编译器在您的类 B 中查找方法。 op 方法隐藏了与类 A 相同名称的方法(重载决议)。如果他找到了有用的东西,他就会使用它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接