5

我对基类析构函数中的 this 指针有一个奇怪的问题。

问题描述:

我有 3 个课程:A1A2A3

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实例在程序终止时被销毁时,将调用A3析构函数,但在~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 实例被破坏时,析构函数中的this指针和我通过调用getPrimaryInstance()获得的地址由于某种原因根本不匹配!我不明白为什么this指针指向的地址总是不同于它(据我所知)应该是的地址。:(

在析构函数中这样做:

int test = (int)this - (int)&getPrimaryInstance();

还向我展示了差异不是恒定的(我曾短暂地认为存在一些恒定偏移),所以当它应该是同一个时,它就像是两个完全不同的对象。:(

我正在使用 VC++ Express (2008) 进行编码。在谷歌搜索了一下之后,我发现了以下 MS 文章:
修复:“this”指针在基类的析构函数中不正确

这与我遇到的问题不同(据说它在 C++.Net 2003 中也已修复)。但不管这些症状看起来很相似,而且它们确实提供了一个简单的解决方法,所以我决定尝试一下:
删除了 not-working- if -statement 并在A2的第二个继承之前添加了virtual,如下所示:

class A2:private A3, public A1 {...} // <-- old version
class A2:private A3, virtual public A1 {...} //new, with virtual!

它奏效了!this指针看起来仍然是错误的,但不再给出访问冲突。

所以我最大的问题是为什么?
为什么this指针不指向它应该指向的位置(?)?
为什么像上面那样向继承添加virtual可以解决它(尽管它仍然指向&getPrimaryInstance()以外的其他地方)?
这是一个错误吗?有人可以在非 MS 环境中尝试吗?
最重要的是:这安全吗?当然它不再抱怨了,但我仍然担心它没有做它应该做的事情。:S

如果有人对此有知识或经验并且可以帮助我理解它,我将非常感激,因为我仍在学习 C++,这完全超出了我目前的知识范围。

4

8 回答 8

4

你使用 C 演员正在杀死你。
它特别容易在多重继承的情况下中断。

您需要使用 dynamic_cast<> 来降低类层次结构。虽然您可以使用 static_cast<> 向上移动(就像我所做的那样),但有时我认为使用 dynamic_cast<> 向两个方向移动更清楚。

避免 C++ 代码中的 C 风格强制转换

C++ 有 4 种不同类型的转换,旨在取代 C 风格的转换。您正在使用等效的 reinterpret_cast<> 并且您使用不正确(任何优秀的 C++ 开发人员看到 reinterpret_cast<> 都会在这里稍等片刻)。

~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<> 可能是不安全的。

那么怎么样

更改 regInst() 以便它为第一个注册的对象返回 true。getPrimaryInstance() 将始终由要创建的第一个对象创建,因此它将始终是第一个注册自己的对象。

将此结果存储在本地成员变量中,并且仅在您不是第一个时才取消注册:

A3()
{
    m_IamFirst = getPrimaryInstance().regInst(this);
}

~A3()
{
    if (!m_IamFirst)
    {
         getPrimaryInstance().unregInst(this);
    }
}

问题:

为什么 this 指针不指向它应该指向的位置(?)?

确实如此。只需使用 C-Cast 就能搞定指针。

为什么像上面那样在继承中添加 virtual 可以解决它(尽管它仍然指向 &getPrimaryInstance() 之外的其他地方)?

因为它改变了内存中的布局,使得 C-Cast 不再搞砸你的指针。

这是一个错误吗?

否。 C-Cast 的错误使用

有人可以在非 MS 环境中尝试吗?

它会做类似的事情。

最重要的是:这安全吗?

不,这是由实现定义的虚拟成员的布局方式。你只是碰巧走运。

解决方案:停止使用 C 风格的演员表。使用适当的 C++ 强制转换。

于 2010-12-07T18:48:40.757 回答
3
class A2 : private A3, public A1 {...} // <-- old version
class A2 : private A3, virtual public A1 {...} //new, with virtual!  

为什么像上面那样在继承中添加 virtual 可以解决它(尽管它仍然指向 &getPrimaryInstance() 之外的其他地方)?

之所以会有所不同,是因为virtual继承会影响基类构造函数和基类析构函数的调用顺序。

指针的数值不同的原因this是完整对象的不同“基类子对象”A2 primary;可以而且必须具有不同的地址。在调用任何析构函数之前,您可以使用dynamic_castto get between A1*and A2*。当您确定一个A3对象确实是 a 的私有基础部分时A2,您可以使用 C 风格的强制转换从A3*to获取A2*

但是,一旦析构函数的主体~A2完成(在析构函数中就是这种情况~A3),dynamic_castfrom A1*toA2*将失败,C 风格的 from A3*to强制转换A2*将产生未定义的行为,因为不再有任何A2对象。

A2 primary;因此,除非您更改存储/访问方式,否则可能无法执行您正在尝试的操作。

于 2010-12-07T16:26:30.763 回答
2

虚拟基类只有在您具有菱形继承结构时才应发挥作用,即您正在多个继承类共享一个公共基类。您是否在实际代码中显示了 A1、A2 和 A3 的整个实际继承树?

于 2010-12-07T15:31:14.730 回答
1

问题可能是当为 A2 对象调用 A3::~A3() 时,A2 对象已经被销毁,因为在 A2 的销毁结束时调用了 ~A3()。您不能再次调用 getPrimary,因为它已经被破坏了。注意:这适用于静态变量主要的情况

于 2010-12-07T15:18:24.453 回答
1

OP @AnorZaken 评论道:

...这是我试图解决的原始问题之一:我希望 getPrimaryInstance() 直接返回 A2 引用,但我不能!A3没见过A2的定义!由于 getPrimaryInstance() 是在 A3 的基类中声明的(上面没有提到),你会得到:错误 C2555:'A3::getPrimaryInstance':覆盖虚函数返回类型不同,并且与 'A3Base::getPrimaryInstance' 不协变简单:即使我声明 A2 的存在,我也不知道有什么方法可以在声明 A2 之前告诉编译器 A2 有 A1 作为基础。:(如果我能解决这个问题那就太好了!

所以听起来你有类似的东西:

class A3Base {
public:
  virtual A1& getPrimaryInstance();
};

And since class A2 cannot be defined before class A3, I would just skip the covariant return type. If you need a way to get the A2& reference from an A3, add that as a different method.

// 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
}
于 2010-12-07T20:50:51.973 回答
0

当名为primary的静态A2实例在程序终止时被销毁时,将调用 A3 析构函数,但在 ~A3 内部,我尝试访问与我正在销毁的实例相同的函数。=> 运行时访问冲突!

当名为primary的静态A2实例被销毁时,指向primary的指针将指向内存中的“随机”位置。因此,您试图访问一个随机内存位置,并且您会遇到运行时违规。这一切都与您调用析构函数并在析构函数中进行调用的顺序有关。

尝试这样的事情:

delete a3;
delete a2-primary;

代替

delete a2-primary;
delete a3;

我还认为您可能会发现此类型转换教程很有帮助。

希望我能帮助你。

于 2010-12-07T16:02:00.230 回答
0

您应该问两个更大的问题:

  1. 为什么我需要使用私有继承?
  2. 为什么我需要对非抽象基类使用多重继承?

在大多数情况下,#1 的答案是你不知道。向另一个类声明一个包含数据成员的类通常以更简洁和更易于维护的代码库来处理相同的情况。

在大多数情况下,#2 的答案也是你不知道。这是您使用的语言的一项功能,后果自负。

我建议您阅读 Meyers 的书籍并重新评估您的设计模式。

于 2010-12-07T16:03:34.490 回答
-1

简短的回答。它发生了,因为不正确的析构函数正在调用。对于长答案,请检查这个这个 并检查 Scott Meyer 的 Effective C++。它涵盖了这些问题。

于 2010-12-07T15:20:47.307 回答