【问题标题】:Is it safe to delete a void pointer?删除空指针是否安全?
【发布时间】:2010-10-30 19:42:31
【问题描述】:

假设我有以下代码:

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

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

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

【问题讨论】:

  • 为什么要自己做内存管理?您正在创建什么数据结构?需要进行显式内存管理在 C++ 中非常少见。您通常应该使用从 STL(或在紧要关头从 Boost)为您处理它的类。
  • 仅供阅读,我使用 void* 变量作为我在 win c++ 中的线程的参数(请参阅 _beginthreadex )。通常它们会准确地指向类。
  • 在这种情况下,它是新/删除的通用包装器,可以包含分配跟踪统计信息或优化的内存池。在其他情况下,我看到对象指针被错误地存储为 void* 成员变量,并在析构函数中被错误地删除而没有转换回适当的对象类型。所以我很好奇安全/陷阱。
  • 对于 new/delete 的通用包装,您可以重载 new/delete 运算符。根据您使用的环境,您可能会挂钩到内存管理以跟踪分配。如果您最终遇到不知道要删除什么的情况,请将其视为您的设计不是最佳且需要重构的强烈暗示。
  • 我认为这个问题有太多的疑问而不是回答它。 (不仅在这里,而且在所有 SO)

标签: c++ memory-management casting void-pointers


【解决方案1】:

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

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

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

正如其他答案中提到的,这是 C++ 中的undefined behavior。一般来说,最好避免未定义的行为,尽管主题本身很复杂并且充满了相互矛盾的意见。

【讨论】:

  • 这是一个公认的答案吗? “删除 void 指针”没有任何意义——安全是一个有争议的问题。
  • “没有充分的理由按照你正在做的事情做你正在做的事情。”这是你的意见,不是事实。
  • @rxantos 提供一个反例,说明在 C++ 中做问题作者想做的事情是个好主意。
  • 我认为这个答案实际上大部分是合理的,但我也认为对这个问题的任何答案需要至少提到这是未定义的行为。
  • @Christopher 尝试编写一个单一的垃圾收集器系统,它不是特定于类型但可以正常工作的。 sizeof(T*) == sizeof(U*) 代表所有 T,U 的事实表明应该有 1 个非模板化、基于 void * 的垃圾收集器实现。但是,当 gc 实际上必须删除/释放指针时,就会出现这个问题。要使其工作,您要么需要 lambda 函数析构函数包装器(urgh),要么需要某种动态的“类型即数据”之类的东西,它允许在类型和可存储的东西之间来回切换。
【解决方案2】:

很多人已经评论说不,删除 void 指针是不安全的。我同意这一点,但我还想补充一点,如果您使用 void 指针来分配连续数组或类似的东西,您可以使用 new 执行此操作,以便您可以使用 @987654323 @ 安全(加上,咳咳,一些额外的工作)。这是通过为内存区域(称为“arena”)分配一个 void 指针,然后将指向该 arena 的指针提供给 new 来完成的。请参阅C++ FAQ 中的此部分。这是在 C++ 中实现内存池的常用方法。

【讨论】:

    【解决方案3】:

    对于 char 的特殊情况。

    char 是一种没有特殊析构函数的内在类型。所以泄漏的论点是没有实际意义的。

    sizeof(char) 通常是 1,因此也没有对齐参数。在 sizeof(char) 不是 1 的稀有平台的情况下,它们分配的内存足够对齐到它们的 char。所以对齐论点也是一个没有实际意义的论点。

    malloc/free 在这种情况下会更快。但是你放弃了 std::bad_alloc 并且必须检查 malloc 的结果。调用全局 new 和 delete 操作符可能会更好,因为它绕过了中间人。

    【讨论】:

    • "sizeof(char) 通常是一" sizeof(char) 总是一
    • 直到最近(2019 年)人们才认为new 实际上被定义为抛出。这不是真的。它依赖于编译器和编译器开关。例如,参见 MSVC2019 /GX[-] enable C++ EH (same as /EHsc) 开关。同样在嵌入式系统上,许多人选择不为 C++ 异常支付性能税。所以以“But you forfeit std::bad_alloc...”开头的句子是有问题的。
    【解决方案4】:

    如果您只需要一个缓冲区,请使用 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;
    

    【讨论】:

      【解决方案5】:

      我在我的框架中使用了 void*,(又名未知类型),同时用于代码反射和其他模棱两可的壮举,到目前为止,我没有遇到来自任何编译器的问题(内存泄漏、访问冲突等) .仅因操作不规范而发出警告。

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

      1) 未知指针不能指向具有普通解构函数的类型,因此当转换为未知指针时,它永远不会被删除。仅在将未知指针转换回 ORIGINAL 类型后删除它。

      2) 实例是否被引用为堆栈绑定或堆绑定内存中的未知指针?如果未知指针引用堆栈上的一个实例,那么它永远不应该被删除!

      3) 你 100% 肯定未知指针是一个有效的内存区域吗?不,那么它永远不应该被删除!

      总而言之,使用未知 (void*) 指针类型可以完成的直接工作非常少。然而,间接地,void* 是 C++ 开发人员在需要数据模糊性时依赖的重要资产。

      【讨论】:

        【解决方案6】:

        这个问题毫无意义。您的困惑可能部分是由于人们经常使用 delete 时使用的草率语言:

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

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

        【讨论】:

        • 诚然,如果没有任何周围的上下文,这个问题没有多大意义。一些 C++ 编译器仍然会愉快地编译这些无意义的代码(如果他们觉得有帮助,可能会发出警告)。因此,为了评估运行包含这种不明智操作的遗留代码的已知风险,提出了这个问题:它会崩溃吗?泄漏部分或全部字符数组内存?其他特定于平台的东西?
        • 感谢您的周到回复。点赞!
        • @Andrew:恐怕标准对此很清楚:“delete 的操作数的值可能是空指针值,指向由以前的 new-expression,或指向表示此类对象的基类的子对象的指针。如果不是,则行为未定义。因此,如果编译器在没有诊断的情况下接受您的代码,那只不过是编译器中的一个错误......
        • @KerrekSB - Re 这只是编译器中的一个错误 - 我不同意。标准说行为是未定义的。这意味着编译器/实现可以做任何事情并且仍然符合标准。如果编译器的响应是说您不能删除 void* 指针,那没关系。如果编译器的响应是擦除硬盘,那也没关系。 OTOH,如果编译器的响应是不生成任何诊断,而是生成释放与该指针关联的内存的代码,那也可以。这是处理这种形式的 UB 的简单方法。
        • 补充一句:我不宽恕使用delete void_pointer。这是未定义的行为。程序员不应该调用未定义的行为,即使响应似乎做了程序员想要做的事情。
        【解决方案7】:

        几乎没有理由这样做。

        首先,如果您不知道数据的类型,而您只知道它是void*,那么您真的应该将该数据视为无类型unsigned char*)的strong>blob,并使用malloc/free处理。这有时对于波形数据之类的东西是必需的,您需要将 void* 指针传递给 C api。没关系。

        如果你确实知道数据的类型(即它有一个ctor/dtor),但由于某种原因你最终得到了一个void*指针(无论你有什么原因)那么您真的应该将其转换回您知道的类型,并在其上调用delete

        【讨论】:

          【解决方案8】:

          如果您真的必须这样做,为什么不去掉中间人(newdelete 运算符)并直接调用全局 operator newoperator delete? (当然,如果您尝试检测 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 没有构造函数/析构函数。
          【解决方案9】:

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

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

          及其脚注:

          这意味着一个对象不能 使用 void* 类型的指针删除 因为没有类型的对象 无效

          .

          【讨论】:

          • 您确定引用正确吗?我认为脚注是指这段文字:“在第一种选择(删除对象)中,如果操作数的静态类型与其动态类型不同,则静态类型应为操作数动态类型的基类,而静态类型应具有虚拟析构函数或行为未定义。在第二种选择(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。 :)
          • 你是对的 - 我已经更新了答案。我不认为它否定了基本观点?
          • 不,当然不是。它仍然说它是UB。更重要的是,现在它规范地声明删除 void* 是 UB :)
          • NULL填充空指针的指向地址内存对应用程序内存管理有什么影响?
          • 这是在 2009 年回答的,在 C++17/20 时仍然如此吗?
          【解决方案10】:

          如果你想使用 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++ 内存管理,并且想知道具体的危险是什么。
          • 编写 C/C++ 是失败的秘诀。不管是谁写的,都应该写其中一个。
          • @David 那是 C++,不是 C/C++。 C 没有模板,也没有使用 new 和 delete。
          【解决方案11】:

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

          当您为非原始类型调用析构函数时,不会对您正在删除的数组中的对象调用析构函数。

          您应该改写新/删除。

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

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

          【讨论】:

          • 你是正确的关于没有调用析构函数,但错误的大小是未知的。如果你给 delete 一个你从 new 得到的指针,它实际上确实知道被删除的东西的大小,完全不同于类型。 C++ 标准没有指定它是如何工作的,但我已经看到了在“new”返回的指针指向的数据之前立即存储大小的实现。
          • 删除了关于大小的部分,尽管 C++ 标准说它是未定义的。我知道 malloc/free 虽然适用于 void* 指针。
          • 不认为您有指向标准相关部分的网络链接?我知道我看过的几个 new/delete 实现在没有类型知识的情况下肯定可以正常工作,但我承认我没有看过标准规定的内容。 IIRC C++ 最初在删除数组时需要数组元素计数,但在最新版本中不再需要。
          • 请参阅@Neil Butterworth 的回答。在我看来,他的回答应该是公认的。
          • @keysersoze:一般我不同意你的说法。仅仅因为某些实现在分配内存之前确实存储了大小并不意味着这是一个规则。
          【解决方案12】:

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

          class foo
          {
             ~foo() { printf("huzza"); }
          }
          
          main()
          {
             foo * myFoo = new foo();
             delete ((void*)foo);
          }
          

          d'ctor 不会被召唤。

          【讨论】:

            【解决方案13】:

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

            【讨论】:

            • char 没有构造函数/析构函数。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-08-12
            • 1970-01-01
            • 2015-07-02
            • 2015-12-21
            • 2018-09-12
            相关资源
            最近更新 更多