2

(C++,MinGW 4.4.0,Windows 操作系统)

代码中的所有注释,除了标签 <1> 和 <2>,都是我的猜测。如果您认为我在某处错了,请纠正我:

class A {
public:
   virtual void disp(); //not necessary to define as placeholder in vtable entry will be
                        //overwritten when derived class's vtable entry is prepared after
                        //invoking Base ctor (unless we do new A instead of new B in main() below)
};

class B :public A {
public:
   B() : x(100) {}
   void disp() {std::printf("%d",x);}
   int x;
};

int main() {
   A* aptr=new B;             //memory model and vtable of B (say vtbl_B) is assigned to aptr
   aptr->disp();              //<1> no error
   std::printf("%d",aptr->x); //<2> error -> A knows nothing about x
}

<2> 是一个错误并且很明显。为什么 <1> 不是错误?我认为这个调用发生的是:参数是指向成员函数aptr->disp(); --> (*aptr->*(vtbl_B + offset to disp))(aptr) aptr的隐式指针。this在里面disp()我们会有std::printf("%d",x); --> std::printf("%d",aptr->x); SAME AS std::printf("%d",this->x);那么为什么 <1> 没有给出错误而 <2> 呢?

(我知道 vtables 是特定于实现的东西,但我仍然认为值得提出这个问题)

4

4 回答 4

3

thisaptr里面不一样B::disp。该B::disp实现采用thisas B*,就像 的任何其他方法一样B。当您通过A*指针调用虚方法时,它会被转换为B*first (甚至可能会更改其值,因此在调用期间不一定等于aptr)。

即真正发生的事情是这样的

typedef void (A::*disp_fn_t)();
disp_fn_t methodPtr = aptr->vtable[index_of_disp]; // methodPtr == &B::disp

B* b = static_cast<B*>(aptr);
(b->*methodPtr)(); // same as b->disp()

有关更复杂的示例,请查看此帖子http://blogs.msdn.com/b/oldnewthing/archive/2004/02/06/68695.aspx。在这里,如果有多个A基可以调用相同的B::disp,MSVC 会生成不同的入口点,每个入口点将A*指针移动不同的偏移量。当然,这是特定于实现的;例如,其他编译器可能会选择将偏移量存储在 vtable 中的某处。

于 2012-04-24T07:02:29.543 回答
3

规则是:

在 C++ 中,动态分派仅适用于成员函数函数,不适用于成员变量。

对于成员变量,编译器仅在该特定类或其基类中查找符号名称。

在情况 1 中,要调用的适当方法是通过获取 、获取适当方法的地址然后调用适当的成员函数来决定的。因此,在静态绑定的情况下, 动态调度本质上是一种而不是正常的。vpt
fetch-fetch-callcall

情况2:编译器只x在范围内查找,this显然找不到,报错。

于 2012-04-24T06:47:55.570 回答
1

你很困惑,在我看来你来自更动态的语言。

在 C++ 中,编译和运行时显然是隔离的。必须首先编译程序然后才能运行(并且这些步骤中的任何一个都可能失败)。


所以,倒退:

<2>编译失败,因为编译是关于静态信息的。aptr是 type A*,因此 的所有方法和属性A都可以通过这个指针访问。既然你声明了disp()但没有x,那么调用disp()编译但没有x

因此,<2>' 的失败与语义有关,并且在 C++ 标准中定义。


到达<1>,它可以工作,因为有in的声明。这保证了函数的存在(我想说你实际上躺在这里,因为你没有在 中定义它)。disp()AA

运行时发生的事情由 C++ 标准在语义上定义,但该标准没有提供实现指南。大多数(如果不是全部)C++ 编译器将使用每个类的虚拟表 + 每个实例的虚拟指针策略,在这种情况下,您的描述看起来是正确的。

然而,这是纯粹的运行时实现,它运行的事实不会追溯影响程序编译的事实。

于 2012-04-24T07:38:33.483 回答
0
virtual void disp(); //not necessary to define as placeholder in vtable entry will be
                     //overwritten when derived class's vtable entry is prepared after
                     //invoking Base ctor (unless we do new A instead of new B in main() below)

您的评论并不完全正确。一个虚函数是odr-used 的,除非它是纯的(反过来不一定成立),这意味着你必须为它提供一个定义。如果您不想为其提供定义,则必须将其设为纯虚函数。

如果您进行这些修改之一,则aptr->disp();可以工作并调用派生类disp(),因为disp()在派生类中会覆盖基类函数。基类函数仍然必须存在,因为您通过指向基的指针调用它。x不是基类的成员,因此aptr->x不是有效的表达式。

于 2012-04-24T06:57:21.340 回答