4

考虑这个程序:

int main()
{
    struct test
    {
        test() { cout << "Hello\n"; }
        ~test() { cout << "Goodbye\n"; }

        void Speak() { cout << "I say!\n"; }
    };

    test* MyTest = new test;
    delete MyTest;

    MyTest->Speak();

    system("pause");
}

我原以为会发生崩溃,但结果却发生了:

你好
再见
我说!

我猜这是因为当内存被标记为已释放时,它并没有被物理擦除,并且由于代码直接引用它,所以仍然可以在那里找到对象,完全完好无损。在调用之前进行的分配Speak()越多,崩溃的可能性就越大。

不管是什么原因,这对于我的实际线程代码来说都是一个问题。鉴于上述情况,我如何可靠地判断另一个线程是否删除了当前线程想要访问的对象?

4

10 回答 10

8

没有独立于平台的方法来检测这一点,没有其他线程在删除对象后将指针设置为 NULL,最好是在关键部分或等效部分内。

简单的解决方案是:设计您的代码,使这种情况不会发生。不要删除其他线程可能需要的对象。仅在安全时才清除共享资源。

于 2010-10-21T17:05:27.547 回答
4

我原以为会发生崩溃,但结果却发生了:

那是因为 Speak() 没有访问该类的任何成员。编译器不会为您验证指针,因此它像任何其他函数调用一样调用 Speak(),将(删除的)指针作为隐藏的“this”参数传递。由于 Speak() 不会为任何事情访问该参数,因此它没有理由崩溃。

于 2010-10-21T18:42:04.390 回答
3

我原以为会发生崩溃,但结果却发生了:

未定义的行为意味着任何事情都可能发生。

于 2010-10-21T17:04:22.917 回答
2

鉴于上述情况,我如何可靠地判断另一个线程是否删除了当前线程想要访问的对象?

如何将MyTest指针设置为零(或 NULL)。这将使其他线程清楚它不再有效。(当然,如果您的其他线程有自己的指针指向相同的内存,那么您设计的东西是错误的。不要去删除其他线程可能使用的内存。)

此外,您绝对不能指望它以它的方式工作。那是幸运的。某些系统会在删除后立即损坏内存。

于 2010-10-21T17:06:05.470 回答
2

尽管最好改进设计以避免访问已删除对象,但您可以添加调试功能来查找访问已删除对象的位置。

  • 将所有方法和析构函数设为虚拟。
  • 检查您的编译器是否创建了一个对象布局,其中指向 vtable 的指针位于对象前面
  • 使指向 vtable 的指针在析构函数中无效

这种肮脏的技巧导致所有函数调用都读取指针指向的地址,并在大多数系统上导致 NULL 指针异常。在调试器中捕获异常。

如果您犹豫是否将所有方法设为虚拟,您也可以创建一个抽象基类并从该类继承。这使您可以毫不费力地删除虚拟功能。只有析构函数需要在类中是虚拟的。

例子

struct Itest
{
    virtual void Speak() = 0;
    virtual void Listen() = 0;
};

struct test : public Itest
{
    test() { cout << "Hello\n"; }
    virtual ~test() { 
        cout << "Goodbye\n";

        // as the last statement!
        *(DWORD*)this = 0;  // invalidate vtbl pointer
    }

    void Speak() { cout << "I say!\n"; }
    void Listen() { cout << "I heard\n"; }
};
于 2010-10-21T17:36:11.643 回答
1

在这种情况下,您可能会使用引用计数。任何取消引用指向已分配对象的指针的代码都会增加计数器。完成后,它会递减。那时,如果计数为零,则会发生删除。只要对象的所有用户都遵守规则,没有人可以访问被释放的对象。

出于多线程的目的,我同意其他答案,即最好遵循不会导致代码“希望”条件为真的设计原则。从您最初的示例中,您是否要捕获异常以判断对象是否已被释放?这有点依赖副作用,即使它不是可靠的副作用,我只喜欢将其用作最后的手段。

于 2010-10-21T17:13:10.233 回答
1

如果由于您正在调用未定义的行为而在其他地方删除了某些内容,这不是“测试”的可靠方法 - 也就是说,它可能不会引发异常让您捕获。

相反,使用std::shared_ptrorboost::shared_ptr和 count 引用。您可以使用 强制 ashared_ptr删除其内容shared_ptr::reset()。然后您可以检查它是否稍后使用shared_ptr::use_count() == 0.

于 2010-10-21T18:17:46.527 回答
0

您可以使用一些静态和运行时分析器(如 valgrind)来帮助您查看这些内容,但它更多地与您的代码结构以及您如何使用语言有关。

于 2010-10-21T17:07:02.697 回答
0
// Lock on MyTest Here.
test* tmp = MyTest;
MyTest = NULL;
delete tmp;
// Unlock MyTest Here.

if (MyTest != NULL)
    MyTest->Speak();
于 2010-10-21T17:08:09.477 回答
0

一种解决方案,不是最优雅的......

在您的对象列表周围放置互斥锁;删除对象时,将其标记为空。使用对象时,请检查 null。由于访问是序列化的,因此您将拥有一致的操作。

于 2010-10-21T17:08:32.057 回答