我遇到了在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)。 / p>
用不同的方式表达: pMotion的值为0x06716fe0,但是当我调用一个方法时,该方法将'this'视为0x06716fe4。
我不是只是生气了吗?这很奇怪,对吧?
答案 0 :(得分:10)
我相信你只是看到编译器构建vtable的方式的工件。我怀疑CMotion具有它自己的虚函数,因此你最终得到派生对象中的偏移量以获得基础对象。因此,不同的指针。
如果它正在工作(即如果这不会导致崩溃,并且对象外没有指针)那么我就不会太担心了。
答案 1 :(得分:6)
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中的vtable指针指向了Test :: f()
答案 2 :(得分:2)
另见wikipedia article on thunking。如果将调试器设置为逐步执行汇编代码,则应该看到它正在发生。 (无论是thunk还是简单地改变偏移量取决于你从你提供的代码中省略的细节)
答案 3 :(得分:1)
我认为我可以解释这一个......在Meyer's或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
有趣的是,bObj
和cObj
都会在A
和B
或C
上的调用方法之间显示地址更改。区别在于B
包含虚拟方法。这允许编译器插入实现功能虚拟化所需的附加表。该程序显示的另一个有趣的事情是typeid(T)
和typeid(*ptr)
在B::x
虚拟调用时有所不同。一旦插入虚拟表,您还可以使用sizeof
查看大小增加。
在您的情况下,只要您CWaitable::WakeWaiters
虚拟,就会插入vtable,它实际上会关注对象的实际类型以及插入必要的簿记结构。这会导致对象基础的偏移量不同。我真的希望我能找到描述神秘内存布局的引用,以及为什么对象的地址依赖于它被解释为继承被混合到乐趣中的类型。
一般规则:(之前您已经听过)基类总是有虚拟析构函数。这将有助于消除这样的小惊喜。
答案 4 :(得分:0)
您需要发布一些实际代码。以下指针的值与预期的一样 - 即它们是相同的:
#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 );
}
答案 5 :(得分:0)
我无法解释为什么会这样,但宣布CWaitable :: WakeWaiters为虚拟修复问题