为什么我们说在编译时无法确定虚函数的调用?

时间:2013-01-02 13:00:07

标签: c++

  

可能重复:
  If virtual table is created in compile time, then why do we call this as a run time polymorphism?

我们说C ++中的虚函数调用是在运行时确定的,而不是在编译时确定的。所以我认为我不清楚编译时和运行时之间的区别。在我粗略的想法中,一切都应该在编译时确定......谁可以帮助解决这个问题?谢谢!

8 个答案:

答案 0 :(得分:7)

看看这个简化的例子:

struct Base
{
  virtual void func()
  { std::cout << "Base::func()" << std::endl; }
};

struct Derived : Base
{
  virtual void func()
  { std::cout << "Derived::func()" << std::endl; }
};

有一个带有虚函数的基类,以及一个覆盖它的派生类。现在这是我们的主要计划:

int main()
{
  Base *bp = 0;

  std::string input;
  std::cin >> input;

  if (input == "base")
    bp = new Base;
  else
    bp = new Derived;

  /* The compiler cannot decide which
     function is called here: */
  bp->func();

  return 0;
}

编译器无法决定是否在bp->func()中调用基类的函数或派生类的函数,因为它取决于来自用户的输入。

这说明了编译时和运行时之间的区别:编译器在编译时将代码转换为机器代码,但用户输入仅在运行时可用。


(我的代码示例不是完美的代码。例如,我声明了具有虚函数的类而没有声明虚拟析构函数。还有其他问题。这只是为了说明编译时和运行时之间的区别,并且每次都展示什么是什么,什么不可能。)

答案 1 :(得分:2)

class C
{
public:
    virtual void f() {}
};

class D : public C
{
public:
    void f() {}
};

void fn(C * c)
{
    // Is C::f or D::f called here?
    c->f();
}

答案 2 :(得分:2)

在编译时确定存在呼叫的事实。只有在知道对象时才能知道调用的成员函数。例如

Base* ptr = (rnd() % 2 ? new D1() : new D2());
ptr->vf();

如果vf()是虚函数,则无法知道在编译时调用哪个vf()(假设D1和D2各自拥有它们)。

答案 3 :(得分:2)

在任何给定的函数中,在编译时,您只能推导出在任何可能的函数运行中都是真的,无论其输入参数的值如何。只要输入到函数的数据可能会改变其行为,就无法在编译时推断出结果。

例如:

class A
{ 
   virtual void virt() = 0;
};

class B : public A 
{
   virtual void virt() { /*some computation */};
};


class C : public A 
{
   virtual void virt() { /*some other computation */};
};


void f(A* a)
{
    a->virt();
}

在这里,当我们编译f时,无法知道a指向的对象是B还是C类型,还是其他一些我们甚至不知道的派生类型(在这个编译单元中)。实际上,这可能取决于用户输入,并且可能因不同的运行而异

因此,在编译时中,我们不知道在键入a->virt()时实际调用哪个函数。

然而,在运行时中,我们确实知道a实际指向的内容,因此我们可以确定将调用哪个函数。

实际上,使用 vtable 解决虚函数调用, vtable 是指向类的所有虚函数的指针数组。

答案 4 :(得分:1)

#include <iostream>

struct Base { virtual void foo() { std::cout << "base\n"; } };
struct Derived : Base { void foo() { std::cout << "derived\n"; } };

int main() {
    Base b;
    Derived d;
    bool flag;
    if (std::cin >> flag) {
        Base *ptr = flag ? &b : &d;
        ptr->foo();
    } else {
        std::cout << "error\n";
    }
}

我不知道您目前认为的“编译时间”,但程序编译和链接都发生在程序运行之前,因此在用户提供输入之前。因此,在编译时无法确定调用foo的目标,因为它取决于运行时的用户输入。

如果foo是非虚函数,则无论Base::foo()的值如何都将调用flag,因此在这种情况下,目标将在编译时被识别。 / p>

答案 5 :(得分:0)

通常,您不知道在调用虚函数时将执行哪些代码:

struct Base
{
    virtual void method() = 0;
};

void foo(Base* p)
{
    p->method();   // What code will be execute here?
}

如果有多个派生自Base的类,将执行哪些代码?

答案 6 :(得分:0)

总而言之,与上述相同 编译时间是源代码编译的时间运行时是编译代码执行的时间,这可能取决于您对程序的输入....,因此根据您的输入,在运行时决定哪个对象引用将访问虚函数。这里

答案 7 :(得分:0)

在更多的技术层面(希望我直截了当地说:S),有一种叫做 vtable 的东西,它是为了实现多态而构建的。

基本上每个只能有一个 vtable ,因此任何类的实例都会共享相同的 vtable vtable 对于程序员来说是不可见的,它包含指针来实现虚函数。

构建 vtables 的编译器只有在需要时才构建它们(即如果类或它的基类包含virtual function。所以值得注意的是不是所有课程都建立了 vtable

示例时间:

class Base {
public:
    virtual void helloWorld();
}

class Derived : public Base {
public:
    void helloWorld();
}

int main(void) {
    Derived d;
    Base *b = &d;

    b->helloWorld(); // here is the magic...

    /* This call is actually translated to something like the line below,
        lets assume we know that the virtual pointer pointing to the viable 
        for Derived is called Derived_vpointer (but it's only a name and 
        probably not what it would be called):

        *(b -> Derived_vpointer -> helloWorld() )
    */

所以,这意味着当调用b->helloWorld()时,它实际上使用vpointer来查找一个替代的vtable来引导调用虚函数的正确版本。所以此处的 Derived有一个 vtable 和一个指向该表的虚拟指针。因此,当 b 指向派生实例时,它将使用派生中的 vpointer ,最终调用正确的实现

这是运行时,或者更确切地说,查找是运行时,因为我们可以轻松地让另一个类扩展 Base 并生成{{1}指向这个(我们称之为b)类。当我们再次使用AnotherDerived时会发生什么,b->helloWorld() vpointer 将用于评估对AnotherDerived的调用。

所以让我们在代码中获取它..

helloWorld()