【问题标题】:C++ "this" doesn't match object method was called onC++“this”不匹配对象方法被调用
【发布时间】:2023-03-03 12:05:02
【问题描述】:

在 Microsoft Visual C++ 2003 下运行我的 C++ 程序时,我遇到了一个看起来很烦人的错误,但这可能只是我做错了,所以我想把它扔在这里看看是否有人有想法。

我有这样的类层次结构(完全一样 - 例如,真实代码中没有多重继承):

class CWaitable
{
public:
    void WakeWaiters() const
    {
        CDifferentClass::Get()->DoStuff(this);  // Breakpoint here
    }
};

class CMotion : public CWaitable
{
   virtual void NotUsedInThisExampleButPertinentBecauseItsVirtual() { }
};

class CMotionWalk : public CMotion
{ ... };

void AnnoyingFunctionThatBreaks(CMotion* pMotion)
{
    pMotion->WakeWaiters();
}

好的,所以我用“CMotionWalk”实例调用“AnnoyingFunctionThatBreaks”(例如,调试器说它是 0x06716fe0),一切看起来都很好。但是当我进入它时,在调用“DoStuff”的断点处,“this”指针与我调用该方法的 pMotion 指针具有不同的值(例如,现在调试器说高一个字 - 0x06716fe4)。

换个说法: pMotion 的值为 0x06716fe0,但是当我在其上调用方法时,该方法将“this”视为 0x06716fe4。

我不会疯了吧?这很奇怪,对吧?

【问题讨论】:

  • 切片发生在对象而不是指针上。您发布的代码经过一些修复后工作。发布一些真实的代码——我的水晶球今天不工作了。顺便说一句:你真的不打算传递“这个”,是吗?
  • 这几乎可以肯定是多重继承——代码示例可能被过度修剪了。
  • @Earwicker:你从哪里获得多重继承?
  • 绝对不涉及多重继承——继承与代码示例中描述的完全一样。另外,我很想了解更多关于传递“this”有什么问题?
  • 我猜测 MI 的原因是因为它必须更改 'this' 指针才能工作,而 SI 不必这样做。

标签: c++ pointers debugging


【解决方案1】:

我相信您只是看到了编译器构建 vtable 的方式的产物。我怀疑 CMotion 有它自己的虚函数,因此你最终会在派生对象中使用偏移量来到达基础对象。因此,不同的指针。

如果它正常工作(即,如果这不会产生崩溃,并且对象之外没有指针),那么我不会太担心它。

【讨论】:

  • 好吧,它不起作用,因为如果“this”与之前看到的不同,DoStuff 方法就会混淆。但是您的解释完全正确-我将其添加到问题中,即 CMotion 具有虚拟功能,因此将 CWaitable::WakeWaiters 声明为虚拟可以解决问题。
  • 我不明白偏移量会如何变化。派生类有一个基类,它们都从内存中的同一个位置开始。为什么在没有多重继承的情况下偏移量应该不同? msvc 做了什么奇怪的事情吗?
  • 我不知道 MSVC 是如何在内存中设置的,但如果你仔细想想,在 Derived 对象中一定有 Base 对象开始的地方。可以将派生元素放在基本元素之前或之后,这取决于编译器的突发奇想。
【解决方案2】:

CMotion 类是否派生了其他一些也包含虚函数的类?我发现 this 指针不会随着你发布的代码而改变,但是如果你有这样的层次结构它会改变:

class Test
{
public:
    virtual void f()
    {

    }
};

class CWaitable 
{
public:
    void WakeWaiters() const
    {
        const CWaitable* p = this;
    }
};

class CMotion : public CWaitable, Test
{ };


class CMotionWalk : public CMotion
{
public:
 };



void AnnoyingFunctionThatBreaks(CMotion* pMotion)
{
    pMotion->WakeWaiters();
}

我相信这是因为 CMotion 类的多重继承和 CMotion 中指向 Test::f() 的 vtable 指针

【讨论】:

  • 绝对没有多重继承。
【解决方案3】:

另见wikipedia article on thunking。如果您将调试器设置为单步执行汇编代码,您应该会看到它的发生。 (无论是 thunk 还是简单地更改偏移量取决于您从提供的代码中省略的细节)

【讨论】:

  • 我尝试单步执行汇编代码。我真的不知道我在寻找什么,但它看起来并没有那么不寻常
【解决方案4】:

我认为我可以解释这一点……在 Meyer 或 Sutter 的书中某处有更好的解释,但我不想搜索。我相信您所看到的是如何实现虚函数 (vtables) 以及 C++ 的“在使用之前无需付费”的性质。

如果没有使用虚拟方法,则指向对象的指针指向对象的数据。一旦引入了虚拟方法,编译器就会插入一个虚拟查找表(vtable),而指针则指向它。我可能遗漏了一些东西(而且我的大脑还没有工作),因为直到我在基类中插入一个数据成员之前我无法做到这一点。如果基类有一个数据成员并且第一个子类有一个虚拟的,那么偏移量会因 vtable 的大小而不同(在我的编译器上为 4)。这是一个清楚地表明这一点的示例:

template <typename T>
void displayAddress(char const* meth, T const* ptr) {
    std::printf("%s - this = %08lx\n", static_cast<unsigned long>(ptr));
    std::printf("%s - typeid(T).name() %s\n", typeid(T).name());
    std::printf("%s - typeid(*ptr).name() %s\n", typeid(*ptr).name());
}

struct A {
    char byte;
    void f() { displayAddress("A::f", this); }
};
struct B: A {
    virtual void v() { displayAddress("B::v", this); }
    virtual void x() { displayAddress("B::x", this); }
};
struct C: B {
    virtual void v() { displayAddress("C::v", this); }
};

int main() {
   A aObj;
   B bObj;
   C cObj;

   std::printf("aObj:\n");
   aObj.f();

   std::printf("\nbObj:\n");
   bObj.f();
   bObj.v();
   bObj.x();

   std::printf("\ncObj:\n");
   cObj.f();
   cObj.v();
   cObj.x();

   return 0;
}

在我的机器 (MacBook Pro) 上运行它会打印以下内容:

aObj:
A::f - this = bffff93f
A::f - typeid(T)::name() = 1A
A::f - typeid(*ptr)::name() = 1A

bObj:
A::f - this = bffff938
A::f - typeid(T)::name() = 1A
A::f - typeid(*ptr)::name() = 1A
B::v - this = bffff934
B::v - typeid(T)::name() = 1B
B::v - typeid(*ptr)::name() = 1B
B::x - this = bffff934
B::x - typeid(T)::name() = 1B
B::x - typeid(*ptr)::name() = 1B

cObj:
A::f - this = bffff930
A::f - typeid(T)::name() = 1A
A::f - typeid(*ptr)::name() = 1A
C::v - this = bffff92c
C::v - typeid(T)::name() = 1C
C::v - typeid(*ptr)::name() = 1C
B::x - this = bffff92c
B::x - typeid(T)::name() = 1B
B::x - typeid(*ptr)::name() = 1C

有趣的是bObjcObjABC 上的调用方法之间都表现出地址变化。不同之处在于B 包含一个虚方法。这允许编译器插入实现功能虚拟化所需的附加表。该程序显示的另一个有趣的事情是typeid(T)typeid(*ptr) 在虚拟调用时在B::x 中是不同的。插入虚拟表后,您还可以使用sizeof 看到大小增加。

在您的情况下,一旦您将CWaitable::WakeWaiters 设为虚拟,vtable 就会被插入,它实际上会注意对象的真实类型以及插入必要的簿记结构。这会导致对象底部的偏移量不同。我真的希望我能找到描述一个神话般的内存布局的参考资料,以及为什么一个对象的地址取决于它被解释为当继承融入乐趣时的类型。

一般规则:(你以前听说过)基类总是有虚析构函数。这将有助于消除这样的小意外。

【讨论】:

  • 这个想法是每个类都将其数据存储在一个连续的内存块中,并生成其方法以尽可能快地访问该数据。因此,这些方法的“this”指针不必在同一上下文中更改。
【解决方案5】:

您需要发布一些实际代码。以下指针的值与预期一致 - 即它们是相同的:

#include <iostream>
using namespace std;

struct A {
    char x[100];
    void pt() {
        cout << "In A::pt this = " << this << endl;
    }
};

struct B : public A { 
    char z[100];
};

void f( A * a ) {
    cout << "In f ptr = " << a << endl;
    a->pt();
}

int main() {
    B b;
    f( &b );
}

【讨论】:

    【解决方案6】:

    我无法解释为什么会这样,但是将 CWaitable::WakeWaiters 声明为虚拟可以解决问题

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-11-30
      • 1970-01-01
      • 2016-06-19
      • 2016-05-08
      • 2017-06-17
      • 2023-03-04
      相关资源
      最近更新 更多