我对基类析构函数中的this-pointer有一个奇怪的问题。
问题描述:
我有3个班级: A1 , A2 , A3
A2 从 A1 公开继承,并从 A3
私下继承class A2:private A3, public A1 {...}
A3 有一个函数 getPrimaryInstance() ...返回 A1 类型的引用 一个 A2 实例:
A1& A3::getPrimaryInstance()const{
static A2 primary;
return primary;
}
A3 构造函数如下所示:
A3(){
getPrimaryInstance().regInst(this);
}
(其中 regInst(...)是 A1 中定义的函数,用于存储指向所有 A3 实例的指针)
类似于 A3 析构函数:
~A3(){
getPrimaryInstance().unregInst(this);
}
^这是问题发生的地方!
当在程序终止时销毁名为 primary 的静态 A2 -instance时,将调用 A3 -destructor,但在内部~A3 我试图访问我破坏的同一个实例上的一个函数。 => 运行时访问冲突!
所以我认为可以用一个简单的if语句修复:
~A3(){
if(this != (A3*)(A2*)&getPrimaryInstance()) //Original verison
//if (this != dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance())))
//Not working. Problem with seeing definitions, see comments below this post
getPrimaryInstance().unregInst(this);
}
(双重演员的原因是继承:)
A1 A3
。 \ /
。 A2
(但这并不重要,可能只有(int) -casted或其他什么)
踢球者是它仍然崩溃。 使用调试器逐步执行代码可以发现,当我的 A2 主要 -instance在析构函数中获取 this -pointer并获取地址时来自 getPrimaryInstance()的调用由于某种原因根本不匹配!我无法理解为什么 this -pointer指向的地址总是与它(对于我有限的知识)应该是的地址不同。 :(
在析构函数中执行此操作:
int test = (int)this - (int)&getPrimaryInstance();
同时向我展示了差异并不是恒定的(我简单地得到了一个理论,即存在一些常数偏移),所以当它应该是同一个时,它就像它是两个完全不同的对象。 :(
我在VC ++ Express(2008)中编码。
谷歌搜索后,我发现了以下MS文章:
FIX: The "this" Pointer Is Incorrect in Destructor of Base Class
这与我所遇到的问题不同(并且它在C ++中也被认为是固定的.Net 2003)。但无论症状看起来是多么相似和,他们确实提出了一个简单的解决方法,所以我决定尝试一下:
删除了not-working- if -statement,并在第二次继承之前的虚拟中添加到 A2 ,如下所示:
class A2:private A3, public A1 {...} // <-- old version
class A2:private A3, virtual public A1 {...} //new, with virtual!
和它一起工作! 此 -pointer仍然看似错误,但不再提供访问冲突。
所以我的大问题是为什么?
为什么 this -pointer没有指向应该的位置(?)?
为什么在上面的继承中添加虚拟来解决它(尽管这个仍然指向除&amp; getPrimaryInstance()之外的其他地方)?
这是一个错误吗?有人可以在非MS环境中试用它吗?
最重要的是:这样安全吗?当然它不再抱怨了,但我仍然担心它不会做它应该做的事情。 :S
如果有人对此有所了解或有经验并且可以帮助我理解它,我会非常感激,因为我还在学习C ++,这完全超出了我目前的知识。
答案 0 :(得分:4)
你使用C演员阵容会杀了你 在多重继承的情况下,它尤其容易中断。
您需要使用dynamic_cast&lt;&gt;抛弃一个类层次结构。虽然你可以使用static_cast&lt;&gt;向上移动(正如我所做的那样)但有时我认为使用dynamic_cast&lt;&gt;更清晰向两个方向移动。
C ++有4种不同类型的演员,旨在取代C风格演员。您使用的是等效的reinterpret_cast&lt;&gt;并且您使用不正确(任何优秀的C ++开发人员在看到reinterpret_cast&lt;&gt;将会在这里停留一段时间)。
~A3(){
if(this != (A3*)(A2*)&getPrimaryInstance())
getPrimaryInstance().unregInst(this);
}
Should be:
~A3()
{
if (this != dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance())))
{ getPrimaryInstance().unregInst(this);
}
}
A2对象的布局:(可能是这样的)。
Offset Data
A2 0x00 |------------------
0x10 * A3 Stuff
*------------------
0x20 * A1 Stuff
*------------------
0x30 * A2 Stuff
在getPrimaryInstance()
中 // Lets assume:
std::cout << primary; // has an address of 0xFF00
返回的引用将指向对象的A1部分:
std::cout << &getPrimaryInstancce();
// Should now print out 0xFF20
如果使用C样式转换,则不会检查任何内容只是更改类型:
std::cout << (A2*)&getPrimaryInstancce();
// Should now print out 0xFF20
std::cout << (A3*)(A2*)&getPrimaryInstancce();
// Should now print out 0xFF20
虽然如果使用C ++强制转换,它应该正确补偿:
std::cout << static_cast<A2*>(&getPrimaryInstance());
// Should now print out 0xFF00
std::cout << dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance()));
// Should now print out 0xFF10
当然,实际值都非常依赖于编译器,并且取决于实现布局。以上只是可能发生的事情的一个例子。
虽然正如所指出的那样调用dynamic_cast&lt;&gt;可能并不安全。在一个目前处于被摧毁过程中的物体上。
那怎么样?
更改regInst(),使其对注册的第一个对象返回true。 getPrimaryInstance()将始终由第一个要创建的对象创建,因此它始终是第一个自我注册的对象。
将此结果存储在本地成员变量中,如果不是第一个,则仅取消注册:
A3()
{
m_IamFirst = getPrimaryInstance().regInst(this);
}
~A3()
{
if (!m_IamFirst)
{
getPrimaryInstance().unregInst(this);
}
}
问题:
为什么this-pointer不指向它应该的位置?(?)?
确实如此。只需使用C-Cast螺丝钉上指针。
为什么像上面那样为继承添加虚拟解决它(尽管这仍然指向除&amp; getPrimaryInstance()之外的其他地方?)
因为它改变了内存中的布局,使得C-Cast不再搞砸你的指针。
这是一个错误吗?
没有。 C-Cast
的使用不正确有人可以在非MS环境中试用吗?
它会做类似的事情。
最重要的是:这样安全吗?
没有。实现定义了虚拟成员的布局方式。你碰巧很幸运。
解决方案:停止使用C样式转换。使用适当的C ++强制转换。
答案 1 :(得分:3)
class A2 : private A3, public A1 {...} // <-- old version
class A2 : private A3, virtual public A1 {...} //new, with virtual!
为什么像上面那样为继承添加虚拟解决它(尽管这仍然指向除&amp; getPrimaryInstance()之外的其他地方?)
这会产生影响的原因是virtual
继承会影响基类构造函数和基类析构函数的调用顺序。
this
指针的数值不相同的原因是完整对象A2 primary;
的不同“基类子对象”可以且必须具有不同的地址。在调用任何析构函数之前,您可以使用dynamic_cast
来介于A1*
和A2*
之间。当您某些 A3
对象确实是A2
的私有基础部分时,您可以使用C风格的转换从A3*
转到A2*
~A2
。
但是,一旦析构函数~A3
的主体完成,dynamic_cast
析构函数就是这种情况,A1*
从A2*
到A3*
的{{1}}将会失败,从A2*
到A2
的C样式转换将产生未定义的行为,因为不再有任何A2 primary;
对象。
因此,除非您更改{{1}}的存储/访问方式,否则可能无法执行您正在尝试的操作。
答案 2 :(得分:2)
如果你有一个钻石继承结构,那么虚拟基类应该只能起作用,即你要多次继承共享一个公共基类的类。您是否在实际代码中显示了A1,A2和A3的整个实际继承树?
答案 3 :(得分:1)
问题可能是当为A2对象调用A3 :: ~A3()时,A2对象已被破坏,因为在A2的销毁结束时调用~A3()。你不能再次调用getPrimary,因为它已经被破坏了。 注意:这适用于静态变量primary
的情况答案 4 :(得分:1)
OP @AnorZaken评论说:
...这是我试图解决的原始问题之一:我希望getPrimaryInstance()直接返回A2引用,但我不能! A3还没见过A2的定义!由于getPrimaryInstance()是在A3的基类(未在上面提到)中声明的,因此您得到:错误C2555:'A3 :: getPrimaryInstance':覆盖虚函数返回类型不同且与'A3Base :: getPrimaryInstance'不协变简单:即使我声明A2的存在我也不知道有什么方法告诉编译器A2在我声明A2之前将A1作为基础。 :(如果我能解决这个问题,那就太棒了!
所以听起来你有类似的东西:
class A3Base {
public:
virtual A1& getPrimaryInstance();
};
由于class A2
之前无法定义class A3
,我只想跳过协变返回类型。如果您需要从A2&
获取A3
引用的方法,请将其添加为不同的方法。
// A3.hpp
class A2;
class A3 : public A3Base {
public:
virtual A1& getPrimaryInstance();
A2& getPrimaryInstanceAsA2();
};
// A3.cpp
#include "A3.hpp"
#include "A2.hpp"
A1& A3::getPrimaryInstance() {
return getPrimaryInstanceAsA2(); // no cast needed for "upward" public conversion
}
答案 5 :(得分:0)
当在程序终止时销毁名为 primary 的静态 A2 -instance时,将调用A3析构函数,但在~3A内我尝试访问一个函数我破坏的同一个例子。 =&GT;在运行时访问冲突!
当静态 A2 - 名为主的实例 已销毁 指向主的指针时>将指向内存中的“随机”位置。因此,您尝试访问随机内存位置,并获得运行时违规。这一切都与您调用析构函数的顺序有关,并在析构函数中进行调用。
尝试这样的事情:
delete a3;
delete a2-primary;
而不是
delete a2-primary;
delete a3;
我也认为您可能会发现 typecasting tutorial 很有帮助。
希望我能帮助你。
答案 6 :(得分:0)
你应该问两个更大的问题:
在大多数情况下,#1的答案是你没有。将包含数据成员的类声明为另一个类通常会在更清晰,更易于维护的代码库中处理相同的情况。
在大多数情况下,#2的答案也是如此。这是您使用该语言的一项功能。
我建议您阅读Meyers书籍并重新评估您的设计模式。
答案 7 :(得分:-1)