【问题标题】:C++ : How does Virtual Function resolve "this" pointer scope issue?C++:虚函数如何解决“this”指针范围问题?
【发布时间】:2012-04-24 06:44:48
【问题描述】:

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

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

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
}

是一个错误并且很明显。为什么 不是错误?我认为此调用发生的情况是:参数中的aptr-&gt;disp(); --&gt; (*aptr-&gt;*(vtbl_B + offset to disp))(aptr) aptr 是指向成员函数的隐式this 指针。在disp() 内部,我们将有std::printf("%d",x); --&gt; std::printf("%d",aptr-&gt;x); SAME AS std::printf("%d",this-&gt;x); 那么为什么 没有给出错误而 给出了?

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

【问题讨论】:

    标签: c++ virtual-functions mingw32


    【解决方案1】:

    thisB::disp 内部的aptr 不同。 B::disp 实现将this 作为B*,就像B 的任何其他方法一样。当你通过A*指针调用虚方法时,它首先被转换为B*(它甚至可能改变它的值,所以它在调用过程中不一定等于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 中的某处。

    【讨论】:

    • 这个答案完全消除了我的疑虑(将 A* 转换为 B*,没有它我仍然看不到,由于我可能缺乏知识,this-&gt;x 如何在内部正确解析void B::disp(B* const this) {} 将 A* 传递为 this)..但不明白的是编译器在编译时不知道 methodPtr==&amp;B::dispmethodPtr==&amp;A::disp..我们可以有 void f(A* aptr) {aptr-&gt;disp();}if(something) f(&amp;bObj); else f(&amp;aObj);后者没有你展示的 staic_casting 的意义。 (我有一种感觉我搞砸了:))....
    • A 和 B 的进一步内存模型重合,这里的事情似乎更容易解释。如果 A 是 B 的第二个 Base 类怎么办。指针对齐会中断,但 B::disp() 仍然可以访问最左边的 Base 类的非私有成员。我认为this 静态解析为在遇到new B 并且在运行时不再触及this-&gt;aMemberVar 时找出/指向成员变量。
    • @ustulation 检查我更新的答案。例如,MSVC 为通过指向基址的指针调用B::disp 的所有可能方式生成“蹦床”,每个都适当地转换this,然后跳转到B::disp 本身。
    【解决方案2】:

    规则是:

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

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

    在情况 1 中,要调用的适当方法由 获取vpt获取适当方法的地址,然后 调用 适当的成员函数。
    因此,在静态绑定的情况下,动态调度本质上是fetch-fetch-call,而不是普通的call

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

    【讨论】:

    • 好的,但 this 被分割成只知道 的 A。在成员函数内部,我认为调用解析为this-&gt;whatEver。让我感到困惑的是,由于 this 隐式传递,如图所示与 B 对象不同,该机制是如何工作的?
    • @ustulation: 不,thisB 类型,B::disp() 是被调用的方法,它自然可以访问x,这是它自己的成员。这里没有切片。
    • 真正的规则是在C++中,动态调度只用于虚拟成员。您的规则由此而来,而且该语言不允许将变量定义为虚拟变量。
    • @ustulation 关于切片:this 是指针,不是类类型,所以不能切片。而this 只存在于非静态成员函数中。在实际调用该函数之前,您拥有的唯一指针是aptr。名称查找、重载解析、访问控制等都使用aptr 及其(静态)类型进行。只有选择函数后,编译器才会考虑虚拟,并通过必要的步骤解析为动态类型并创建this 指针。
    • @JamesKanze:是的,我的规则是一个组合的简化版本,我确实将两者结合在一起,因为它简化了细节并且在所有可能的情况下都适用。
    【解决方案3】:

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

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


    所以,倒退:

    &lt;2&gt; 编译失败,因为编译是关于静态信息的。 aptrA* 类型,因此A 的所有方法和属性都可以通过此指针访问。既然你声明了disp() 但没有x,那么对disp() 的调用会编译但没有x

    因此,&lt;2&gt; 的失败与语义有关,而这些是在 C++ 标准中定义的。


    到达&lt;1&gt;,因为A 中有disp()声明。这保证了函数的存在(我想说你实际上躺在这里,因为你没有在A定义它)。

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

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

    【讨论】:

      【解决方案4】:
      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-&gt;disp(); 将起作用并调用派生类disp(),因为派生类中的disp()覆盖基类函数。基类函数仍然必须存在,因为您通过指向基的指针调用它。 x 不是基类的成员,因此 aptr-&gt;x 不是有效的表达式。

      【讨论】:

      • which means that you must provide a definition for it. If you don't want to provide a definition for it you must make it a pure virtual function. 请尝试一下。它可以在没有定义 A::disp() 的情况下工作
      • @ustulation:仅仅因为它适用于您并不一定意味着您的代码是正确的。对于具有未定义行为的代码来说,看起来可以工作是一种可能的行为。
      • @ustulation 正如查尔斯·贝利所说,这是未定义的行为。它是否工作取决于编译器,可能取决于基类的构造函数和析构函数做什么,以及它们是否可见。
      • @CharlesBailey:好的!它从来没有让我觉得它是未定义的。由于行为与我想到的解释相匹配(我在上面的 sn-p 中作为评论写的那个)我认为不定义它没有任何问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-02-19
      • 2016-02-22
      • 1970-01-01
      • 1970-01-01
      • 2019-04-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多