8

我有以下代码:

class Pet {
public:
  virtual string speak() const { return ""; }
};

class Dog : public Pet {
public:
  string speak() const { return "Bark!"; }
};

int main() {
  Dog ralph;
  Pet* p1 = &ralph;
  Pet& p2 = ralph;
  Pet p3;

  // Late binding for both:
  cout << "p1->speak() = " << p1->speak() <<endl;
  cout << "p2.speak() = " << p2.speak() << endl;

  // Early binding (probably):
  cout << "p3.speak() = " << p3.speak() << endl;
}

我被要求确定编译器是对最终函数调用使用早期绑定还是后期绑定。我在网上搜索过,但没有找到任何可以帮助我的东西。有人能告诉我如何执行这项任务吗?

4

6 回答 6

5

您可以查看反汇编,看看它是否似乎正在通过 vtable 进行重定向。

线索是它是直接调用函数的地址(早期绑定)还是调用计算地址(后期绑定)。另一种可能性是该函数是内联的,您可以将其视为早期绑定。

当然,标准并没有规定实现细节,可能还有其他可能性,但这涵盖了“正常”实现。

于 2011-09-30T13:42:12.560 回答
3

你总是可以使用 hack :D

//...
Pet p3;
memset(&p3, 0, sizeof(p3));
//...

如果编译器确实使用 vtbl 指针,猜猜会发生什么:>

p3.speak()  // here
于 2011-09-30T14:34:07.270 回答
2

查看生成的代码。例如,在 Visual Studio 中,您可以设置一个断点,然后右键单击并选择“Go To Disassembly”。

于 2011-09-30T13:39:13.700 回答
2

它使用早期绑定。您有一个 P3 类型的对象。虽然它是具有虚函数定义的基类,但类型是具体的并且在编译时已知,因此不必考虑到派生类的虚函数映射。

这与在 Pet 构造函数中调用 speak() 非常相似 - 即使在生成派生对象时,当基类构造函数执行时,对象的类型是基类的类型,因此函数不会使用 v-table ,它将调用基类型的版本。

基本上,早期绑定是编译时绑定,后期绑定是运行时绑定。运行时绑定仅用于编译器在编译时没有足够的类型信息来解析调用的情况。

于 2011-09-30T13:39:23.750 回答
1

事实上,编译器没有义务特别使用其中任何一个,只是为了确保调用了正确的函数。在这种情况下,您的对象是具体类型Pet,因此只要Pet::speak调用编译器就是“做正确的事”。

现在,鉴于编译器可以静态查看对象的类型,我怀疑大多数编译器会优化虚拟调用,但并不要求他们这样做。

如果您想知道您的特定编译器在做什么,唯一的方法是查阅其文档、源代码或生成的反汇编。

于 2011-09-30T13:57:01.313 回答
0

我只是想到了一种在运行时告诉的方法,无需猜测。您可以简单地用 0 覆盖多态类的 vptr 并查看是否调用了该方法或是否遇到分段错误。这就是我的例子:

Concrete: Base
Concrete: Derived
Pointer: Base
Pointer: Derived
DELETING VPTR!
Concrete: Base
Concrete: Derived
Segmentation fault

whereConcrete: T表示T通过具体类型调用虚成员函数成功。类似地,Pointer: T表示T通过Base指针调用成员函数是成功的。


作为参考,这是我的测试程序:

#include <iostream>
#include <string.h>

struct Base {
  unsigned x;
  Base() : x(0xEFBEADDEu) {
  }
  virtual void foo() const {
    std::cout << "Base" << std::endl;
  }
};

struct Derived : Base {
  unsigned y;
  Derived() : Base(), y(0xEFCDAB89u) {
  }
  void foo() const {
    std::cout << "Derived" << std::endl;
  }
};

template <typename T>
void dump(T* p) {
  for (unsigned i = 0; i < sizeof(T); i++) {
    std::cout << std::hex << (unsigned)(reinterpret_cast<unsigned char*>(p)[i]);
  }
  std::cout << std::endl;
}

void callfoo(Base* b) {
  b->foo();
}

int main() {
  Base b;
  Derived d;
  dump(&b);
  dump(&d);
  std::cout << "Concrete: ";
  b.foo();
  std::cout << "Concrete: ";
  d.foo();
  std::cout << "Pointer: ";
  callfoo(&b);
  std::cout << "Pointer: ";
  callfoo(&d);
  std::cout << "DELETING VPTR!" << std::endl;
  memset(&b,0,6);
  memset(&d,0,6);
  std::cout << "Concrete: ";
  b.foo();
  std::cout << "Concrete: ";
  d.foo();
  std::cout << "Pointer: ";
  callfoo(&b);
  std::cout << "Pointer: ";
  callfoo(&d);
  return 0;
}
于 2011-09-30T14:34:16.557 回答