删除void指针是安全的吗?

103

假设我有以下代码:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

这样安全吗?或者在删除之前必须将ptr转换为char*吗?


6
为了让读者理解,我在win c++中使用void*变量作为线程的参数(参见_beginthreadex)。通常它们实际上指向类。 - ryansstack
4
在这种情况下,它是一个通用的包装器,用于new/delete操作,可以包含分配跟踪统计信息或优化的内存池。在其他情况下,我曾经看到过将对象指针错误地存储为void*成员变量,并在析构函数中错误地删除而没有进行适当的对象类型转换。因此,我对安全性/陷阱很感兴趣。 - An̲̳̳drew
2
对于一个通用的new/delete包装器,你可以重载new/delete运算符。根据你使用的环境,你可能会得到内存管理的钩子来跟踪分配情况。 如果你陷入了一个不知道你在删除什么的情况中,那就是你的设计不够优化,需要进行重构的强烈暗示。 - Hans
51
我认为有太多人在质疑问题本身而不是回答它。(不只是在这里,在所有的SO上都是这样) - Petruza
但是说真的,@Andrew直接使用mallocfree即可。这隐含了构造函数/析构函数不会在void*指针下面的任何东西上被调用(如下面的答案所述)。 - bobobobo
显示剩余5条评论
13个回答

153

通过void指针删除是C++标准未定义的 - 请参见第5.3.5/3节:

在第一种情况(删除对象)中,如果操作数的静态类型与其动态类型不同,则静态类型应为操作数的动态类型的基类,并且静态类型应具有虚拟析构函数,否则行为未定义。在第二种情况(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。

以及它的脚注:

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

.


不,当然不是。它仍然说它是未定义行为。更甚的是,现在它规范地声明删除void*是未定义行为 :) - Johannes Schaub - litb
将一个空指针的指向地址填充为 NULL,对应用程序内存管理有什么影响吗? - artu-hnrq
这个问题在2009年已经回答了,截至C++17/20,它仍然有效吗? - jcarpenter2

28

这取决于"安全"。通常情况下,它会起作用,因为指针与分配本身的信息一起存储,所以释放器可以将其返回到正确的位置。在这个意义上,只要您的分配器使用内部边界标记,它就是"安全"的。(许多分配器都是如此。)

然而,正如其他答案中提到的那样,删除void指针不会调用析构函数,这可能是一个问题。在这个意义上,它不是"安全"的。

没有好的理由按照你现在的方式去做你正在做的事情。如果你想编写自己的释放函数,你可以使用函数模板来生成具有正确类型的函数。这样做的好处是可以生成池分配器,对于特定类型来说非常高效。

正如其他答案中提到的那样,在C++中这是未定义行为。一般来说,避免未定义行为是好的,尽管这个主题本身很复杂,充满了相互冲突的观点。


9
这怎么能成为被接受的答案呢?“删除空指针”没有任何意义——安全性是无关的。 - Kerrek SB
7
“按照你现在的方式做事情没有充分的理由。”这只是你的观点,不是事实。 - rxantos
8
请提供一个反例,证明在C++中做问题作者想要做的事情是一个好主意。 - Christopher
我认为这个答案实际上大部分是合理的,但我也认为对于这个问题的任何答案都需要至少提到这是未定义行为。 - Kyle Strand
“这个主题本身就很复杂,充满了相互矛盾的观点。” 实际上并非如此。未定义行为是不可接受的。当它发生时,它会使所有东西无效。 “这取决于‘安全’。” 再次,实际上并非如此。这是未定义的行为。没有合理的“安全”代码定义允许未定义的行为。 - François Andrieux
显示剩余3条评论

26

在C++中这不是一个好主意,也不是你会做的事情。你没有理由失去类型信息。

当你对非基本类型调用它时,你删除数组中的对象时,你的析构函数不会被调用。

相反你应该重载new/delete。

删除void*可能会正确释放内存,但是这是错误的,因为结果是未定义的。

如果由于某种我不知道的原因需要将指针存储在void*中然后释放它,那么应该使用malloc和free。


5
您对析构函数不被调用是正确的,但是关于大小未知是错误的。如果您给delete一个从new获取的指针,它实际上确实知道要删除的对象的大小,完全与类型无关。C++标准没有规定它的实现方式,但我见过的一些实现中,在由'new'返回的指针所指向的数据之前立即存储了其大小。 - KeyserSoze
移除关于大小的部分,尽管C++标准说它是未定义的。但是我知道malloc/free对于void*指针是可行的。 - Brian R. Bondy
你有相关标准章节的网址吗?我知道我查看过的一些 new/delete 的实现肯定可以在没有类型知识的情况下正常工作,但我承认我没有查看标准规定。我回想起 C++ 最初删除数组时需要一个数组元素计数,但是最新版本已不再需要。 - KeyserSoze
请查看@Neil Butterworth的答案。在我看来,他的答案应该被接受。 - Brian R. Bondy
@keysersoze:你确定new返回的数据是在其大小之前吗?如果是这样,那对我来说避免使用虚拟函数表所占用的额外空间就有点奇怪了。因为我会为了一个看起来并不是很实用的东西而付出同样的代价(大小可能是4个字节,就像指针一样)。 - anon
2
@keysersoze:一般来说,我不同意你的说法。仅仅因为某些实现在分配内存之前存储大小并不意味着这是一个规则。 - anon

13

删除空指针是危险的,因为它实际指向的值的析构函数将不会被调用。这可能会导致应用程序中的内存/资源泄漏。


4
Char没有构造函数/析构函数。 - rxantos

8

如果你真的必须这样做,为什么不直接调用全局的operator newoperator delete,而不是通过newdelete运算符来间接实现呢?(当然,如果你正在尝试监测newdelete运算符,那么你实际上应该重新实现operator newoperator delete。)

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

请注意,与malloc()不同的是,operator new在失败时会抛出std::bad_alloc(或调用已注册的new_handler)。

这是正确的,因为char没有构造函数/析构函数。 - rxantos

8
这个问题毫无意义。你的困惑可能部分源于人们常用的delete时语言不严谨:
使用delete来销毁动态分配的对象,为此,您需要使用指向该对象的指针形成一个delete表达式。你从来不会“删除指针”。你真正做的是“删除通过其地址标识的对象”。
现在我们明白了这个问题毫无意义的原因: void指针不是“对象的地址”。它只是一个地址,没有任何语义。它可能来自实际对象的地址,但这些信息已经丢失了,因为它被编码在原始指针的类型中。恢复对象指针的唯一方法是将void指针转换回对象指针(这要求作者知道指针的含义)。void本身是不完整的类型,因此永远不是对象的类型,void指针不能用于标识对象。(对象由其类型和地址共同标识。)

感谢您的周到回复。点赞! - An̲̳̳drew
1
@Andrew:恐怕标准对此已经非常明确了:“delete的操作数的值可以是空指针值,也可以是由先前的new表达式创建的非数组对象的指针,或者是表示此类对象的基类的子对象的指针。如果不是这样,行为就是未定义的。”因此,如果编译器在没有诊断的情况下接受您的代码,那么这只是编译器中的一个错误。 - Kerrek SB
1
@KerrekSB - Re it's nothing but a bug in the compiler -- 我不同意。标准规定该行为是未定义的。这意味着编译器/实现可以做任何事情,仍然符合标准。如果编译器的响应是说您不能删除void*指针,那没问题。如果编译器的响应是擦除硬盘,那也没问题。另一方面,如果编译器的响应是不生成任何诊断,而是生成释放与该指针相关联的内存的代码,那也没问题。这是处理此类未定义行为的简单方法。 - David Hammen
只是补充一下:我并不赞成使用 delete void_pointer。这是未定义的行为。程序员永远不应该调用未定义的行为,即使响应似乎完成了程序员想要完成的工作。 - David Hammen
@DavidHammen:好的,可以这样想。将“bug”看作是“不能够成为一个好的和负责任的工具”的“失败”。 - Kerrek SB
显示剩余4条评论

6
由于char类型没有特殊的析构函数逻辑,所以这段代码不起作用。
class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

构造函数不会被调用。

6
很多人已经评论说删除空指针是不安全的。我同意这一点,但我也想补充一下,如果你正在使用空指针来分配连续数组或类似的东西,你可以使用new来做到这一点,这样你就能够安全地使用delete(需要一些额外的工作)。这是通过将一个空指针分配给内存区域(称为“arena”),然后将该指针提供给新的指针来完成的。请参阅C ++ FAQ中的此部分。这是在C++中实现内存池的常见方法。

5
如果您想使用void *,为什么不只使用malloc/free?new/delete不仅仅是内存管理。基本上,new/delete会调用构造函数/析构函数,还有更多的事情发生。如果您只使用内置类型(如char *)并通过void *删除它们,那么它可以工作,但仍不建议这样做。底线是,如果您想使用void *,请使用malloc/free。否则,您可以使用模板函数以方便使用。
template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}

我并没有编写示例中的代码 - 偶然发现这种模式在几个地方被使用,好奇地混合了C/C++内存管理,并想知道具体的危险是什么。 - An̲̳̳drew
编写C/C++代码是失败的配方。谁写这个应该只写其中一个。 - David Thornley
2
@David 这是 C++,不是 C/C++。C 没有模板,也不使用 new 和 delete。 - rxantos

0

如果你只需要一个缓冲区,使用malloc/free。 如果必须使用new/delete,请考虑使用一个简单的包装类:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接