我一直在研究一些使用可变长度结构 (TAPI) 的遗留 C++ 代码,其中结构大小将取决于可变长度字符串。结构体是通过转换数组来分配的 new 因此:

STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];

然而稍后内存被释放使用 delete 称呼:

delete pStruct;

这个数组会混合吗 new [] 和非数组 delete 导致内存泄漏还是取决于编译器?我最好改变这个代码来使用 mallocfree 反而?

有帮助吗?

解决方案

从技术上讲,我相信这可能会导致分配器不匹配的问题,尽管实际上我不知道有哪个编译器不会对此示例执行正确的操作。

更重要的是如果 STRUCT 如果拥有(或曾经被赋予)析构函数,那么它将调用析构函数而不调用相应的构造函数。

当然,如果您知道 pStruct 来自哪里,为什么不直接将其强制删除以匹配分配:

delete [] (BYTE*) pStruct;

其他提示

我个人认为你最好使用 std::vector 来管理你的记忆,所以你不需要 delete.

std::vector<BYTE> backing(sizeof(STRUCT) + nPaddingSize);
STRUCT* pStruct = (STRUCT*)(&backing[0]);

一旦支持离开范围,你的 pStruct 不再有效。

或者,您可以使用:

boost::scoped_array<BYTE> backing(new BYTE[sizeof(STRUCT) + nPaddingSize]);
STRUCT* pStruct = (STRUCT*)backing.get();

或者 boost::shared_array 如果您需要转移所有权。

是的,这会导致内存泄漏。

请参阅除了 C++ 陷阱之外的内容: http://www.informit.com/articles/article.aspx?p=30642 为什么。

Raymond Chen 解释了向量如何 newdelete 与 Microsoft 编译器底层的标量版本不同...这里:http://blogs.msdn.com/oldnewthing/archive/2004/02/03/66660.aspx

恕我直言,您应该将删除修复为:

delete [] pStruct;

而不是切换到 malloc/free, ,如果只是因为这是一个更简单且不会犯错误的更改;)

当然,由于原始分配中的转换,我上面显示的更简单的更改是错误的,应该是

delete [] reinterpret_cast<BYTE *>(pStruct);

所以,我想切换到可能很容易 malloc/free 毕竟 ;)

代码的行为是未定义的。您可能很幸运(或不幸运),它可能适用于您的编译器,但实际上这不是正确的代码。它有两个问题:

  1. delete 应该是一个数组 delete [].
  2. delete 应该在指向与分配的类型相同的类型的指针上调用。

所以为了完全正确,你想做这样的事情:

delete [] (BYTE*)(pStruct);

C++标准明确指出:

delete-expression:
             ::opt delete cast-expression
             ::opt delete [ ] cast-expression

第一种选择适用于非数组对象,第二种选择适用于数组。操作数应具有指针类型,或具有到指针类型的单个转换函数(12.3.2)的类类型。结果的类型为 void。

在第一个替代方案(删除对象)中,删除操作数的值应是指向非数组对象的指针[...]如果不是,则行为未定义。

操作数的值在 delete pStruct 是一个指向数组的指针 char, ,与其静态类型无关(STRUCT*)。因此,任何关于内存泄漏的讨论都是毫无意义的,因为代码格式不正确,并且在这种情况下不需要 C++ 编译器来生成合理的可执行文件。

它可能会泄漏内存,也可能不会,或者它可能会做任何事情导致系统崩溃。事实上,我用来测试代码的 C++ 实现会在删除表达式处中止程序执行。

正如其他帖子中强调的那样:

1) 调用 new/delete 分配内存并可能调用构造函数/析构函数 (C++ '03 5.3.4/5.3.5)

2) 混合数组/非数组版本 newdelete 是未定义的行为。(C++ '03 5.3.5/4)

查看来源,似乎有人搜索并替换了 mallocfree 以上就是结果。C++确实有一个直接替代这些函数的方法,那就是调用分配函数 newdelete 直接地:

STRUCT* pStruct = (STRUCT*)::operator new (sizeof(STRUCT) + nPaddingSize);
// ...
pStruct->~STRUCT ();  // Call STRUCT destructor
::operator delete (pStruct);

如果应该调用 STRUCT 的构造函数,那么您可以考虑分配内存,然后使用放置 new:

BYTE * pByteData = new BYTE[sizeof(STRUCT) + nPaddingSize];
STRUCT * pStruct = new (pByteData) STRUCT ();
// ...
pStruct->~STRUCT ();
delete[] pByteData;

@eric - 感谢您的评论。但你总是说一些让我抓狂的事情:

那些运行时库处理内存管理在独立操作系统一致语法中对操作系统的呼叫,这些运行时库负责使Malloc和New Works在linux,Windows,solaris,aix等等OSS之间保持一致性和新工作。 。

这不是真的。例如,编译器编写者提供了 std 库的实现,并且他们完全可以自由地在操作系统中实现这些库 依赖的 方式。例如,他们可以自由地对 malloc 进行一次巨大的调用,然后按照自己的意愿管理块内的内存。

提供兼容性是因为std等的API是相同的 - 不是因为运行时库全部转过来并调用完全相同的操作系统调用。

关键字 new 和 delete 的各种可能用法似乎会造成相当多的混乱。在 C++ 中构造动态对象总是有两个阶段:原始内存的分配以及在分配的内存区域中构造新对象。在对象生命周期的另一边,存在对象的销毁和对象所在内存位置的释放。

这两个步骤通常由单个 C++ 语句执行。

MyObject* ObjPtr = new MyObject;

//...

delete MyObject;

除了上述之外,您还可以使用 C++ 原始内存分配函数 operator newoperator delete 和显式构造(通过放置 new)并销毁以执行等效步骤。

void* MemoryPtr = ::operator new( sizeof(MyObject) );
MyObject* ObjPtr = new (MemoryPtr) MyObject;

// ...

ObjPtr->~MyObject();
::operator delete( MemoryPtr );

请注意,这里不涉及转换,并且在分配的内存区域中仅构造一种类型的对象。使用类似的东西 new char[N] 作为分配原始内存的一种方式在技术上是不正确的,因为从逻辑上讲, char 对象在新分配的内存中创建。我不知道在什么情况下它不能“正常工作”,但它模糊了原始内存分配和对象创建之间的区别,所以我建议不要这样做。

在这种特殊情况下,将以下两个步骤分开是没有任何好处的: delete 但您确实需要手动控制初始分配。上面的代码在“一切正常”的情况下工作,但在构造函数的情况下,它会泄漏原始内存 MyObject 抛出异常。虽然可以在分配时使用异常处理程序捕获并解决这个问题,但提供自定义运算符 new 可能更简洁,以便可以通过放置 new 表达式来处理完整的构造。

class MyObject
{
    void* operator new( std::size_t rqsize, std::size_t padding )
    {
        return ::operator new( rqsize + padding );
    }

    // Usual (non-placement) delete
    // We need to define this as our placement operator delete
    // function happens to have one of the allowed signatures for
    // a non-placement operator delete
    void operator delete( void* p )
    {
        ::operator delete( p );
    }

    // Placement operator delete
    void operator delete( void* p, std::size_t )
    {
        ::operator delete( p );
    }
};

这里有几个微妙的点。我们定义了一个新的类放置,以便我们可以为类实例分配足够的内存以及一些用户可指定的填充。因为我们这样做,所以我们需要提供一个匹配的放置删除,以便如果内存分配成功但构造失败,则自动释放分配的内存。不幸的是,我们的放置删除的签名与非放置删除的两个允许签名之一匹配,因此我们需要提供另一种形式的非放置删除,以便我们真正的放置删除被视为放置删除。(我们可以通过向我们的新展示位置和删除展示位置添加额外的虚拟参数来解决这个问题,但这需要在所有调用站点进行额外的工作。)

// Called in one step like so:
MyObject* ObjectPtr = new (padding) MyObject;

使用单个 new 表达式,我们现在可以保证,如果 new 表达式的任何部分抛出异常,内存都不会泄漏。

在对象生命周期的另一端,因为我们定义了operator delete(即使我们没有定义,无论如何,对象的内存最初都来自全局operator new),所以以下是销毁动态创建的对象的正确方法。

delete ObjectPtr;

概括!

  1. 看看没有演员! operator newoperator delete 处理原始内存,放置new可以在原始内存中构造对象。来自 a 的显式强制转换 void* 指向对象指针通常是逻辑上错误的标志,即使它确实“正常工作”。

  2. 我们完全忽略了new[]和delete[]。这些可变大小的对象在任何情况下都不能在数组中工作。

  3. 放置 new 允许 new 表达式不泄漏,new 表达式仍然计算为指向需要销毁的对象和需要释放的内存的指针。使用某些类型的智能指针可能有助于防止其他类型的泄漏。从好的方面来说,我们让一个简单的 delete 是执行此操作的正确方法,因此大多数标准智能指针都可以工作。

如果你 真的 必须做这种事情,你可能应该打电话给操作员 new 直接地:

STRUCT* pStruct = operator new(sizeof(STRUCT) + nPaddingSize);

我相信以这种方式调用它可以避免调用构造函数/析构函数。

我目前无法投票,但是 切片石灰的答案 优于 罗布·沃克的回答, ,因为问题与分配器或 STRUCT 是否有析构函数无关。

另请注意,示例代码不一定会导致内存泄漏 - 这是未定义的行为。几乎任何事情都可能发生(从没什么坏事到很远很远的事故)。

示例代码导致未定义的行为,简单明了。slicedlime 的回答很直接,也很中肯(需要注意的是,“向量”这个词应该改为“数组”,因为向量是 STL 的东西)。

C++ FAQ(第 16.12、16.13 和 16.14 节)很好地介绍了此类内容:

http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.12

您指的是数组删除([]),而不是向量删除。向量是 std::vector,它负责删除其元素。

您可以强制转换回 BYTE * 并删除:

delete[] (BYTE*)pStruct;

是的,可能,因为你用 new[] 分配但用 delelte 取消分配,是的 malloc/free 在这里更安全,但在 c++ 中你不应该使用它们,因为它们不会处理(de)构造函数。

此外,您的代码将调用解构函数,但不会调用构造函数。对于某些结构,这可能会导致内存泄漏(如果构造函数分配了更多内存,例如字符串)

更好的是正确执行,因为这也将正确调用任何构造函数和解构函数

STRUCT* pStruct = new STRUCT;
...
delete pStruct;

最好始终保持任何资源的获取/释放尽可能平衡。虽然在这种情况下泄漏与否很难说。它取决于编译器对向量(取消)分配的实现。

BYTE * pBytes = new BYTE [sizeof(STRUCT) + nPaddingSize];

STRUCT* pStruct = reinterpret_cast< STRUCT* > ( pBytes ) ;

 // do stuff with pStruct

delete [] pBytes ;

伦:问题是 pStruct 是一个 STRUCT*,但分配的内存实际上是一个大小未知的 BYTE[]。因此,delete[] pStruct 不会取消分配所有已分配的内存。

您有点混合了 C 和 C++ 的做事方式。为什么分配的大小超过 STRUCT 的大小?为什么不只是“新结构”?如果您必须这样做,那么在这种情况下使用 malloc 和 free 可能会更清楚,因为这样您或其他程序员可能不太可能对分配的对象的类型和大小做出假设。

@Matt Cruikshank您应该注意并阅读我写的内容,因为我从未建议不调用Delete [],然后让操作系统清理。您对管理堆的 C++ 运行时库的看法是错误的。如果是这种情况,那么 C++ 将不会像今天这样可移植,并且崩溃的应用程序永远不会被操作系统清理。(承认存在特定于操作系统的运行时,这使得 C/C++ 显得不可移植)。我挑战你在 kernel.org 的 Linux 源代码中找到 stdlib.h。C++ 中的 new 关键字实际上与 malloc 相同的内存管理例程进行通信。

C++ 运行时库进行操作系统系统调用,并且由操作系统管理堆。您是部分正确的,因为运行时库指示何时释放内存,但是,它们实际上并不直接遍历任何堆表。换句话说,您链接的运行时不会向您的应用程序添加代码来遍历堆进行分配或释放。Windows、Linux、Solaris、AIX 等中都是这种情况......这也是您在任何 Linux 内核源代码中都找不到 malloc 的原因,也无法在 Linux 源代码中找到 stdlib.h。了解这些现代操作系统具有虚拟内存管理器,这使事情变得更加复杂。

有没有想过为什么可以在 1G 机器上调用 malloc 获取 2G 内存,并且仍然返回有效的内存指针?

x86 处理器上的内存管理是在内核空间内使用三个表进行管理的。PAM(页分配表)、PD(页目录)和 PT(页表)。这是我所说的硬件级别。操作系统内存管理器(而不是 C++ 应用程序)所做的事情之一是在 BIOS 调用的帮助下查明启动过程中机器上安装了多少物理内存。操作系统还处理异常,例如当您尝试访问内存时,您的应用程序也没有权限。(GPF 一般保护故障)。

马特,我们可能在说同样的事情,但我认为您可能有点混淆了引擎盖下的功能。我曾经以维护 C/C++ 编译器为生......

@ericmayo - 哭泣。好吧,用 VS2005 进行实验,我无法从向量新创建的内存标量删除中得到诚实的泄漏。我猜编译器的行为在这里是“未定义的”,这是我能采取的最好的防御措施。

但你必须承认,按照原始海报所说的去做是一种非常糟糕的做法。

如果真是这样,那么C ++将无法像今天一样便携,并且OS永远不会清理崩溃的应用程序。

但这个逻辑并不成立。我的断言是编译器的运行时可以管理操作系统返回给它的内存块内的内存。这就是大多数虚拟机的工作方式,因此在这种情况下您反对可移植性的论点没有多大意义。

@马特·克鲁克香克

“嗯,用 VS2005 进行实验,我无法从向量 new 生成的内存标量删除中得到诚实的泄漏。我猜编译器的行为在这里是“未定义的”,这是我能采取的最好的防御措施。”

我不同意这是编译器行为甚至编译器问题。正如您所指出的,“new”关键字被编译并链接到运行时库。这些运行时库以独立于操作系统的一致语法处理对操作系统的内存管理调用,并且这些运行时库负责使 malloc 和新工作在 Linux、Windows、Solaris、AIX 等操作系统之间保持一致... 。这就是我提到可移植性论点的原因;试图向您证明运行时实际上也不管理内存。

操作系统管理内存。

操作系统的运行时库接口..在 Windows 上,这是虚拟内存管理器 DLL。这就是为什么 stdlib.h 在 GLIB-C 库中实现,而不是在 Linux 内核源代码中实现;如果 GLIB-C 在其他操作系统上使用,则它会执行 malloc 更改以进行正确的操作系统调用。在VS、Borland等..您也永远不会找到任何与其编译器一起提供的实际管理内存的库。但是,您将找到 malloc 的操作系统特定定义。

既然我们有Linux的源码,你可以去看一下malloc是如何实现的。您将看到 malloc 实际上是在 GCC 编译器中实现的,而 GCC 编译器基本上会向内核发出两个 Linux 系统调用来分配内存。malloc 本身从来没有真正管理内存!

别把它从我身边夺走。阅读 Linux 操作系统的源代码,或者您可以看看 K&R 对此有何评论...这是 C 上 K&R 的 PDF 链接。

http://www.oberon2005.ru/paper/kr_c.pdf

参见第 149 页近端:“对 malloc 和 free 的调用可以按任何顺序发生;Malloc呼吁操作系统在必要时获得更多内存。这些例程说明了以相对机器独立的方式编写机器相关代码所涉及的一些注意事项,并且还展示了结构、联合和 typedef 的实际应用。”

“不过你必须承认,按照原发帖者所说的去做是一种非常糟糕的做法。”

哦,我不同意这一点。我的观点是,原始发布者的代码不利于内存泄漏。我就是这么说的。我没有插话最佳实践方面的事情。由于代码正在调用删除,因此内存正在释放。

我同意,为你辩护,如果原始发布者的代码从未退出或从未进入删除调用,则该代码可能存在内存泄漏,但因为他声明稍后他看到删除被调用。“然而,稍后使用删除调用释放内存:”

此外,我做出回应的原因是由于OP的评论“可变长度结构(TAPI),其中结构大小将取决于可变长度字符串”

该评论听起来像是他在质疑针对正在进行的转换的分配的动态性质,并因此想知道这是否会导致内存泄漏。如果你愿意的话,我正在阅读字里行间;)。

除了上面的精彩回答外,我还想补充一点:

如果你的代码在Linux上运行或者你可以在Linux上编译它那么我建议通过运行它 瓦尔格林德. 。它是一个出色的工具,在它产生的无数有用警告中,它还会告诉您何时将内存分配为数组,然后将其作为非数组释放(反之亦然)。

使用运算符new和delete:

struct STRUCT
{
  void *operator new (size_t)
  {
    return new char [sizeof(STRUCT) + nPaddingSize];
  }

  void operator delete (void *memory)
  {
    delete [] reinterpret_cast <char *> (memory);
  }
};

void main()
{
  STRUCT *s = new STRUCT;
  delete s;
}

我认为没有内存泄漏。

STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];

这被转换为操作系统内的内存分配调用,并返回指向该内存的指针。分配内存时,大小为 sizeof(STRUCT) 和大小 nPaddingSize 为了满足针对底层操作系统的任何内存分配请求。

因此分配的内存被“记录”在操作系统的全局内存分配表中。内存表通过其指针进行索引。所以在相应的delete调用中,原来分配的所有内存都是空闲的。(内存碎片也是这个领域的一个热门话题)。

你看,C/C++ 编译器不管理内存,底层操作系统在管理内存。

我同意有更干净的方法,但OP确实说这是遗留代码。

简而言之,我没有看到内存泄漏,因为接受的答案认为存在内存泄漏。

罗布·沃克 回复 很好。

只是小小的补充,如果您没有任何构造函数或/和析构函数,那么您基本上需要分配和释放一块原始内存,请考虑使用 free/malloc 对。

ericmayo.myopenid.com 错得离谱,有足够声誉的人应该对他投反对票。

C 或 C++ 运行时库正在管理由操作系统分块提供给它的堆,有点像您指出的那样,Eric。但它 开发人员有责任向编译器指示应进行哪些运行时调用来释放内存,并可能破坏其中的对象。在这种情况下,向量删除(又名 delete[])是必要的,以便 C++ 运行时使堆保持有效状态。事实上,当进程终止时,操作系统足够聪明,可以释放底层内存块,这并不是开发人员应该依赖的。这就像根本不调用删除一样。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top