删除句柄 class 中的对象可能会导致未定义的行为

Delete object in handle class potentially causes undefined behavior

我有以下一段代码(来自 Koening & Moo Accelerated C++ 第 255 页),它定义了一个通用句柄 class HandleHandle用于管理对象的内存:

#include <iostream>
#include <stdexcept>

///Handle
template <class T>
class Handle
{
  public:
    Handle() : p(0) {}
    Handle &operator=(const Handle &);
    T *operator->() const;
    ~Handle() { delete p; }
    Handle(T *t) : p(t) {}

  private:
    T *p;
};

template <class T>
Handle<T> &Handle<T>::operator=(const Handle &rhs)
{
    if (&rhs != this)
    {
        delete p;
        p = rhs.p ? rhs.p->clone() : 0;
    }
    return *this;
};

template <class T>
T *Handle<T>::operator->() const
{
    if (p)
        return p;
    throw std::runtime_error("error");
};

class test_classA
{
    friend class Handle<test_classA>;

  private:
    virtual test_classA *clone() const { return new test_classA; }

  public:
    virtual void run() { std::cout << "HiA"; }
};

class test_classB : public test_classA
{
  private:
    virtual test_classB *clone() const { return new test_classB; }

  public:
    virtual void run() { std::cout << "HiB"; }
};

int main()
{

    Handle<test_classA> h;
    h = new test_classA;
    h->run();

    return 0;
}

当我使用 g++ -o main main.cpp -Wall 编译它时,我收到警告:

warning: deleting object of polymorphic class type ‘test_classA’ which has non-virtual destructor might cause undefined behaviour [-Wdelete-non-virtual-dtor]
     ~Handle() { delete p; }

我不太明白这个警告。句柄class在析构函数中自动删除了指针*p而不管其类型,那么潜在的陷阱在哪里呢?

你的警告说明了一切。您的 class A 是多态的,但析构函数是非虚拟的。当基 class 没有虚析构函数时,通过指向基 class 的指针删除派生 class 的对象是未定义的行为。

在你的具体例子中没有未定义的行为,因为你没有派生的对象class,但编译器可能无法确定它,所以无论如何它都会警告你。

问题是如果处理的对象是模板实例化类型的子类,会发生错误删除。

在您的情况下,如果您的 Handle<test_classA> h; 将处理类型为 test_classB 的对象...

句柄 h 的类型为 Handle<test_classA>,因此它将存储指向 test_classA 的指针并调用 test_classA 的析构函数。但是,您可以在句柄中存储指向 test_classB 的指针,在这种情况下,不会调用 test_classB 析构函数,因为 test_classA 析构函数不是虚拟的:

h = static_cast< test_classA * >(new test_classB);

另请注意,此 Handle class 名称选择不当,它本质上是一种 class.

的智能指针

在 C++ 中,如果您有一个基础 class(此处为 test_classA),而其他 class 派生自它(此处为 test_classB),您如果这些指针实际上可能指向类型 test_classB 的对象,则必须小心删除类型 test_classA 的指针。请注意,您在此处编写的代码中正是这样做的。

如果你做这样的事情,你需要给你的基础 class (test_classA) 一个虚拟析构函数,像这样:

class test_classA {
public:
    virtual ~test_classA() = default;
    // ...
};

这样,当 C++ 尝试删除 test_classA 类型的指针时,它知道所讨论的指针可能实际上并不指向 test_classA 对象,并且会正确调用正确的析构函数。

顺便说一句,这个问题完全独立于您的包装器类型。你可以通过写

得到同样的问题
test_classA* ptr = new test_classB;
delete ptr; // <--- Warning! Not good unless you have a virtual dtor.

警告是因为您的基 class' 析构函数不是虚拟的。如果你想多态地使用你的class(例如创建一个带有基本class指针的向量,其中指向的对象是派生的class)你会有未定义的行为。

另一件需要提及的事情是,您将句柄 class 声明为 class test_classA 的好友,以便获得对克隆功能的访问权限。请注意,friend 关键字不可传递,因此在派生的 class 中,句柄 class 无法访问克隆函数。

最后你的克隆函数我不是很清楚。先看主要功能:

Handle<test_classA> h;
h = new test_classA
  1. 你用它的默认构造函数实例化一个 class 句柄,将 p 初始化为空指针(顺便说一句,如果你使用 C++11,最好用 nullptr 而不是 0 或空)
  2. 在下一行中调用实例 h 的运算符=。在这里,您的 operator= 等待句柄 class。所以表达式 new test_classA 将隐式调用你的 Handle class 构造函数 Handle(T *t) : p(t) {}。然后这将在您的 operator= 函数中使用,您可以在其中检查 rhs.p 是否为 NULL。因为它不是空的,所以你将调用克隆函数(你写的等同于 (rhs.p)->clone 所以它不是调用的 Handle class' operator-> 而是指针 p)这将在 HEAP 中再次创建内存。

我想这不是你想要的。