除调用全局删除运算符外,如何删除空指针?

时间:2018-08-01 00:05:30

标签: c++ undefined-behavior delete-operator void-pointers

C ++标准非常清楚明确地指出,在delete指针上使用delete[]void是未定义的行为,如this answer所述:

  

这意味着无法使用类型为void*的指针删除对象,因为没有类型为void的对象。

但是,据我了解,deletedelete[]仅做两件事:

  • 调用适当的析构函数
  • 调用适当的operator delete函数,通常是全局函数

只有一个参数operator delete(还有operator delete[])和that single argument is void* ptr

因此,当编译器遇到带有void*操作数的delete-expression时,它当然可以 maliciously做一些完全不相关的操作,或者仅不输出任何代码那个表情。更好的是,尽管我测试过的MSVS,Clang和GCC版本没有这样做,但它可能会发出诊断消息并拒绝编译。 (后两个使用-Wall发出警告;使用/W3的MSVS不会。)

但是在删除操作中,实际上只有一种明智的方法来处理上述每个步骤:

  • void*未指定任何析构函数,因此不会调用任何析构函数。
  • void不是类型,因此不能具有特定的对应operator delete,因此必须调用全局operator delete(或[]版本)。由于该函数的参数为​​void*,因此不需要类型转换,并且运算符必须正确运行。

因此,在遇到常见的编译器实现(大概不是恶意的,否则我们甚至无法相信它们仍然遵守标准)时,可以依靠它们执行上述步骤(在不调用析构函数的情况下释放内存)吗?这样的删除表达式?如果没有,为什么不呢?如果是这样,当数据的实际类型没有析构函数(例如,它是诸如delete之类的基元数组)时,以这种方式使用long[64]是否安全?

是否可以安全地直接为void operator delete(void* ptr)数据调用全局删除运算符void*(以及相应的数组版本)(再次假设不应调用析构函数)?

5 个答案:

答案 0 :(得分:3)

void*是指向未知类型对象的指针。如果您不知道某物的类型,则可能无法知道该如何破坏某物。因此,我认为,不,没有“真的只有一种明智的方式来处理这种删除操作”。处理此类删除操作的唯一明智方法是不处理它。因为根本无法正确处理它。

因此,正如您链接到的原始答案所说:删除void*是未定义的行为([expr.delete] §2)。该答案remains essentially unchanged to this day中提到的脚注。老实说,我只是将其指定为未定义的行为而不是使其变得不正确,这使我有些惊讶,因为我无法想到在编译时无法检测到任何情况。

请注意,从C ++ 14开始,new表达式不一定意味着调用分配函数。 delete表达式也不一定意味着要调用释放函数。编译器may调用分配函数来获取使用new表达式创建的对象的存储。 In some cases,允许编译器省略此类调用并使用以其他方式分配的存储。例如,这使编译器有时可以将用new创建的多个对象打包到一个分配中。

void*上调用全局释放函数而不是使用delete表达式是否安全?仅当使用相应的全局分配功能分配存储时。通常,除非您自己调用分配函数,否则您肯定无法确定。如果从new表达式中获得了指针,则通常不知道该指针是否甚至是释放函数的有效参数,因为它甚至可能不指向通过调用分配函数获得的存储。请注意,知道new表达式必须使用哪个分配函数基本上等同于知道void*所指向内容的动态类型。如果您知道,也可以static_cast<>到实际类型,然后delete

使用琐碎的析构函数来释放对象的存储空间而不先显式调用析构函数是否安全?基于[basic.life] §1.4,我会说是。请注意,如果该对象是一个数组,则可能仍然必须首先调用任何数组元素的析构函数。除非它们也无关紧要。

您可以依靠常见的编译器实现来产生您认为合理的行为吗?不。对您 可以确切依赖的内容进行正式定义,实际上就是从头开始制定标准。假设您具有符合标准的实现,则可以依靠该标准为您提供的保证。只要您使用特定编译器的特定版本来编译代码,您还可以依靠特定编译器文档可能提供的任何其他保证。除此之外,所有赌注都关闭了……

答案 1 :(得分:1)

如果要调用释放函数,则只需调用释放函数。

这很好:

void* p = ::operator new(size);

::operator delete(p);  // only requires that p was returned by ::operator new()

这不是:

void* p = new long(42);

delete p;  // forbidden: static and dynamic type of *p do not match, and static type is not polymorphic

但是请注意,这也不安全:

void* p = new long[42];

::operator delete(p); // p was not obtained from allocator ::operator new()

答案 2 :(得分:1)

尽管标准将允许实现使用传递给delete的类型来决定如何清理有问题的对象,但是并不需要实现这样做。该标准还将允许基于内存分配new在即将返回的地址之前的空间中存储清除信息并将delete实现为对某事物的调用的一种替代方法(可能是更好的方法)喜欢:

typedef void(*__cleanup_function)(void*);
void __delete(void*p)
{
  *(((__cleanup_function*)p)[-1])(p);
}

在大多数情况下,以这种方式实现new / delete的成本相对来说是微不足道的,并且该方法将提供一些语义上的好处。这种方法的唯一重大缺点是,它要求实现记录其new / delete实现的内部工作情况,并且其实现不能支持类型无关的{{1} },则必须破坏任何依赖于其记录在案的内部工作方式的代码。

请注意,如果将delete传递到void*是一个约束违例,那将禁止实现提供类型无关的delete,即使它们很容易做到这一点,即使为他们编写的某些代码也要依靠这种能力。代码依赖于这种能力的事实将使它仅可移植到可以提供该能力的实现中,但是,如果实现者选择这样做,则允许实现支持这种能力比使它成为约束违禁更为有用。

我个人希望看到Standard提供的实现有两个特定选择:

  1. 允许将delete传递到void*并使用传递给delete的任何类型删除对象,并定义一个指示支持这种构造的宏。

  2. p>
  3. 如果将new传递给void*,请进行诊断,并定义一个宏,指示其不支持这种构造。

其实现支持类型不可知的delete的程序员然后可以决定他们从这种功能中获得的好处是否可以证明使用它带来的可移植性限制是合理的,而实现者可以决定支持更大范围的支持所带来的好处。程序足以证明支持该功能所需的少量费用。

答案 3 :(得分:0)

  

void *不指定析构函数,因此不会调用析构函数。

这很可能是不允许的原因之一。取消分配支持类实例的内存而没有为所述类调用析构函数的方法,实际上是一个非常糟糕的主意。

例如,假设该类包含一个std::map,其中包含数十万个元素。这代表了大量的内存。做您打算做的事会泄漏所有的内存。

答案 4 :(得分:0)

void没有大小,因此编译器无法知道要释放多少内存。

编译器应如何处理以下内容?

struct s
{
    int arr[100];
};

void* p1 = new int;
void* p2 = new s;
delete p1;
delete p2;