删除空指针是否安全?


96

假设我有以下代码:

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

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

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


为什么要自己进行内存管理?您正在创建什么数据结构?在C ++中,很少需要显式的内存管理。您通常应该使用从STL(或紧要的Boost)为您处理类的类。
布赖恩2009年

6
只是供阅读的人使用,我将void *变量用作win c ++中线程的参数(请参见_beginthreadex)。通常,他们通常是在指向类。
ryansstack

3
在这种情况下,它是用于新建/删除的通用包装程序,可以包含分配跟踪统计信息或优化的内存池。在其他情况下,我看到对象指针被错误地存储为void *成员变量,并且在析构函数中被错误删除,而没有转换回适当的对象类型。所以我很好奇安全/陷阱。
2009年

1
对于new / delete的通用包装,您可以重载new / delete运算符。根据您使用的环境,您可能会迷上内存管理以跟踪分配。如果最终您不知道要删除的内容,则可以强烈暗示您的设计欠佳,需要重构。
汉斯(Hans)2009年

38
我认为有太多的问题要问而不是回答。(不仅在这里,而且在所有方面如此)
Petruza 2011年

Answers:


24

这取决于“安全”。通常会起作用,因为信息与有关分配本身的指针一起存储,因此取消分配器可以将其返回到正确的位置。从这个意义上讲,只要您的分配器使用内部边界标记,它就是“安全的”。(很多)

但是,如其他答案中所述,删除void指针不会调用析构函数,这可能是一个问题。从这个意义上讲,它不是“安全的”。

没有充分的理由按照自己的方式去做自己的事情。如果要编写自己的释放函数,则可以使用函数模板来生成具有正确类型的函数。这样做的一个很好的理由是生成池分配器,它对于特定类型可能非常高效。

如其他答案所述,这是C ++中的未定义行为。通常,最好避免出现不确定的行为,尽管主题本身很复杂并且充满了相互矛盾的观点。


9
这是如何被接受的答案?“删除空指针”没有任何意义-安全是有争议的。
Kerrek SB 2013年

6
“没有充分的理由按照自己的方式去做自己的事情。” 那是您的意见,不是事实。
rxantos

8
@rxantos提供一个反例,其中在C ++中,执行问题的作者想做的事是个好主意。
Christopher

我想这个答案其实主要是合理的,但我也认为,任何回答这个问题,需要至少提,这是不确定的行为。
凯尔·斯特兰德

@Christopher尝试编写一个非垃圾回收器系统,该系统不是特定于类型的,而是可以正常工作的。事实上,sizeof(T*) == sizeof(U*)所有的T,U建议,它应该是可能有1个非模板,void *基于垃圾收集器的实现。但是,当gc实际上必须删除/释放指针时,就会出现此问题。为了使它起作用,您要么需要lambda函数析构器包装器(urgh),要么需要某种动态的“类型作为数据”类型的东西,它允许在类型和可存储的东西之间来回移动。
BitTickler

147

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

在第一个替代方案(删除对象)中,如果操作数的静态类型与动态类型不同,则静态类型应为操作数动态类型的基类,并且静态类型应具有虚拟析构函数或行为未定义。在第二种选择(删除数组)中,如果要删除的对象的动态类型与静态类型不同,则行为是不确定的。

及其脚注:

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


6
您确定打对了吗?我认为脚注是指此文本:“在第一个备选方案(删除对象)中,如果操作数的静态类型与它的动态类型不同,则静态类型应为操作数的动态类型的基类,而静态类型应具有虚拟析构函数或行为未定义。在第二种选择(删除数组)中,如果要删除的对象的动态类型不同于其静态类型,则行为未定义。” :)
Johannes Schaub-litb

2
您说得对-我已经更新了答案。我认为它不会否​​定基本观点吗?

不,当然不是。它仍然说是UB。更重要的是,现在它规范地说删除void *是UB :)
Johannes Schaub-litb

NULL应用程序内存管理填充空白指针的有针对性的地址存储器吗?
artu-hnrq

25

这不是一个好主意,也不是您在C ++中会做的事情。您无缘无故丢失了类型信息。

当您为非基本类型调用析构函数时,不会在要删除的数组对象上调用析构函数。

您应该改写new / delete。

删除void *可能会偶然地正确释放您的内存,但这是错误的,因为结果是不确定的。

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


5
您对未调用析构函数是正确的,但是对于未知的大小却是错误的。如果你给删除一个指针,你从新得到的,它确实在事实上知道的事情的大小被删除,从类型完全分开。C ++标准未指定它的作用方式,但我看到了一些实现,其中大小在“ new”所返回的指针指向的数据之前存储。
KeyserSoze

删除了有关大小的部分,尽管C ++标准说它是未定义的。我知道,对于void *指针,malloc / free可以工作。
Brian R. Bondy

您是否没有指向标准相关部分的网络链接?我知道我研究过的new / delete的少数实现在没有类型知识的情况下肯定可以正常工作,但是我承认我没有研究标准指定的内容。IIRC C ++最初在删除数组时需要一个数组元素计数,但不再使用最新版本。
KeyserSoze

请参阅@Neil Butterworth答案。我认为他的答案应该被接受。
Brian R. Bondy

2
@keysersoze:通常我不同意你的说法。仅仅因为某些实现确实在分配的内存之前存储了大小,并不意味着这是规则。

13

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


1
char没有构造函数/析构函数。
rxantos

9

这个问题没有道理。您的困惑可能部分是由于人们经常使用粗俗的语言delete

您可以delete用来销毁动态分配的对象。这样做,您将形成一个指向该对象指针删除表达式。您永远不会“删除指针”。您真正要做的是“删除由其地址标识的对象”。

现在我们明白为什么这个问题没有意义了:空指针不是“对象的地址”。这只是一个地址,没有任何语义。它可能来自实际对象的地址,但是该信息丢失了,因为它是以原始指针的类型进行编码的。还原对象指针的唯一方法是将void指针强制转换回对象指针(这要求作者知道指针的含义)。void本身是不完整的类型,因此永远不是对象的类型,并且空指针永远不能用于标识对象。(对象通过它们的类型和地址共同标识。)


诚然,如果没有周围的环境,这个问题就没有多大意义。一些C ++编译器仍然会很高兴地编译这种荒谬的代码(如果他们觉得有帮助,可能会发出警告)。因此,提出该问题是为了评估运行包含此错误操作的旧代码的已知风险:它将崩溃吗?泄漏部分或全部字符数组内存?还有其他特定于平台的内容吗?
2013年

感谢您的周到答复。赞!
2013年

1
@Andrew:恐怕标准对此很明确:“的操作数的值delete可能是空指针值,指向由先前的new-expression创建的非数组对象的指针或指向a的指针。子对象,代表此类对象的基类。否则,该行为未定义。” 因此,如果编译器在没有诊断的情况下接受了您的代码,那只不过是编译器中的错误……
Kerrek SB 2013年

1
@KerrekSB- 没什么,只是编译器中的一个错误 -我不同意。该标准说行为是不确定的。这意味着编译器/实现可以执行任何操作,并且仍然符合标准。如果编译器的响应是说您不能删除void *指针,那就可以了。如果编译器的响应是擦除硬盘驱动器,那也可以。OTOH,如果编译器的响应是不生成任何诊断信息,而是生成释放与该指针关联的内存的代码,那也可以。这是处理这种形式的UB的简单方法。
David Hammen 2014年

补充一点:我不宽容使用delete void_pointer。这是未定义的行为。程序员永远都不应调用未定义的行为,即使响应看起来像程序员想做的一样。
David Hammen 2014年

6

如果您真的必须这样做,为什么不删掉中间人(newdelete运算符)operator newoperator delete直接调用全局函数呢?(当然,如果您尝试使用newand delete运算符,则实际上应该重新实现operator newand operator 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

5

因为char没有特殊的析构函数逻辑。这行不通。

class foo
{
   ~foo() { printf("huzza"); }
}

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

d'ctor不会接到电话。


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,2009年

2
@David那是C ++,不是C / C ++。C没有模板,也没有使用new和delete。
rxantos

5

很多人已经发表评论说不,删除void指针并不安全。我同意这一点,但是我还想补充一点,如果您正在使用空指针来分配连续数组或类似的东西,则可以这样做,new以便可以delete安全地使用(使用ahem ,还有一些额外的工作)。这是通过为存储区域分配一个空指针(称为“竞技场”),然后将指向竞技场的指针提供给new来完成的。请参阅C ++常见问题解答中的此部分。这是在C ++中实现内存池的常用方法。


0

几乎没有理由这样做。

首先,如果您不知道数据的类型,而只是知道它是void*,那么您实际上应该将数据视为二进制数据()的无类型斑点unsigned char*,并使用malloc/ free处理它。有时对于诸如波形数据之类的东西是必需的,您需要在其中传递void*指向C api的指针。没关系。

如果您确实知道数据的类型(即,它具有ctor / dtor),但是由于某种原因,您最终得到了一个void*指针(无论出于何种原因),那么您实际上应该将其转换为您知道的类型是,然后调用delete它。


0

我在框架中使用void *(又称未知类型)进行代码反射和其他含糊之处,到目前为止,我还没有遇到任何编译器带来的麻烦(内存泄漏,访问冲突等)。仅警告由于操作不规范。

删除未知数(void *)非常有意义。只需确保指针遵循这些准则,否则它可能会失去意义:

1)未知指针一定不能指向具有琐碎解构函数的类型,因此,当将其转换为未知指针时,绝对不应将其删除。仅在将未知指针投射回ORIGINAL类型后才将其删除。

2)实例是否在堆栈绑定或堆绑定内存中被引用为未知指针?如果未知指针引用了堆栈上的实例,则永远不要删除它!

3)您是否100%肯定未知指针是有效的内存区域?不,那么它永远都不要被删除!

总之,使用未知(void *)指针类型可以完成的直接工作很少。但是,间接地,void *是C ++开发人员在需要数据歧义时可以依靠的巨大资产。


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;

0

对于char的特殊情况。

char是没有特殊析构函数的内部类型。因此,泄漏论点是有争议的。

sizeof(char)通常为1,因此也没有对齐参数。对于sizeof(char)不为1的罕见平台,它们会为char分配足够对齐的内存。因此,alignment参数也是有争议的。

在这种情况下,malloc / free会更快。但是您没收std :: bad_alloc,必须检查malloc的结果。调用全局new和delete运算符可能会更好,因为它绕过了中间人。


1
sizeof(char)通常是一个 ” sizeof(char)通常是一个
curiousguy

直到最近(2019年),人们才认为new实际上定义为抛出。这不是真的。它取决于编译器和编译器开关。请参阅例如MSVC2019 /GX[-] enable C++ EH (same as /EHsc)开关。同样在嵌入式系统上,许多人选择不为C ++异常支付性能税。因此,以“但是您没收std :: bad_alloc ...”开头的句子值得怀疑。
BitTickler
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.