10

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

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

但是稍后使用delete调用释放内存:

delete pStruct;

new[]数组和非数组的这种混合delete会导致内存泄漏还是取决于编译器?malloc更改此代码以使用它会更好free吗?

4

24 回答 24

12

从技术上讲,我相信它可能会导致分配器不匹配的问题,尽管在实践中我不知道任何编译器不会对这个例子做正确的事情。

更重要的是,如果STRUCT在哪里有(或曾经给出)一个析构函数,那么它将调用析构函数而不调用相应的构造函数。

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

delete [] (BYTE*) pStruct;
于 2008-09-16T14:50:18.320 回答
7

我个人认为你最好用它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,如果您需要转移所有权。

于 2008-09-16T15:39:51.323 回答
6

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

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

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

delete [] (BYTE*)(pStruct);
于 2008-09-16T14:52:55.320 回答
6

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

除了 C++ Gotchas 之外,请参见: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毕竟;)

于 2008-09-16T15:00:47.110 回答
4

C++ 标准明确规定:

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

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

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

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

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

于 2008-09-16T15:14:07.810 回答
3

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

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

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

查看源代码,似乎有人对 and 进行了搜索和替换,malloc以上free是结果。C++ 确实可以直接替换这些函数,即直接调用分配函数 fornewdelete

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;
于 2008-09-16T16:27:35.903 回答
2

如果你真的必须做这种事情,你应该new直接打电话给运营商:

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

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

于 2008-09-16T16:20:14.643 回答
2

@eric - 感谢您的评论。你一直在说些什么,这让我抓狂:

这些运行时库以独立于操作系统的一致语法处理对操作系统的内存管理调用,并且这些运行时库负责使 malloc 和 new 工作在 Linux、Windows、Solaris、AIX 等操作系统之间保持一致...... .

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

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

于 2008-09-17T16:27:25.693 回答
2

关键字 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 );
    }
};

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

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

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

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

delete ObjectPtr;

概括!

  1. 看没有演员表!operator new并且operator delete处理原始内存,placement new 可以在原始内存中构造对象。从 avoid*到对象指针的显式转换通常是逻辑错误的标志,即使它确实“正常工作”。

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

  3. 放置 new 允许新表达式不泄漏,新表达式仍计算为指向需要销毁的对象和需要解除分配的内存的指针。使用某种类型的智能指针可能有助于防止其他类型的泄漏。从好的方面来说,我们让 plaindelete成为正确的方法,所以大多数标准智能指针都可以工作。

于 2008-09-20T15:19:08.647 回答
1

我目前无法投票,但slicedlime 的回答Rob Walker 的回答更可取,因为这个问题与分配器或 STRUCT 是否有析构函数无关。

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

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

C++ FAQ(第 16.12、16.13 和 16.14 节)很好地涵盖了这类内容:

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

于 2008-09-16T16:27:07.573 回答
1

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

于 2008-09-16T16:47:48.647 回答
0

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

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

最好是正确地做到这一点,因为这也将正确地调用任何构造函数和解构函数

STRUCT* pStruct = new STRUCT;
...
delete pStruct;
于 2008-09-16T14:49:41.537 回答
0

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

delete[] (BYTE*)pStruct;
于 2008-09-16T14:52:16.023 回答
0

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

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

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

 // do stuff with pStruct

delete [] pBytes ;
于 2008-09-16T14:54:50.760 回答
0

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

于 2008-09-16T14:55:21.570 回答
0

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

于 2008-09-16T15:04:23.343 回答
0

使用运算符 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;
}
于 2008-09-16T15:07:34.690 回答
0

我认为没有内存泄漏。

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

这被翻译成操作系统内的内存分配调用,在该调用上返回指向该内存的指针。在分配内存时sizeof(STRUCT),将知道 的大小和大小,nPaddingSize以便满足针对底层操作系统的任何内存分配请求。

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

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

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

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

于 2008-09-16T16:42:08.740 回答
0

@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 的 RAM 并仍然得到一个有效的内存指针?

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

可能我们说的是同一件事,马特,但我认为您可能会有点混淆引擎盖下的功能。我用来维持一个 C/C++ 编译器为生...

于 2008-09-17T03:23:01.973 回答
0

@ericmayo - 绉纱。好吧,尝试使用 VS2005,我无法从由 vector new 生成的内存上的标量删除中得到诚实的泄漏。我猜编译器的行为在这里是“未定义的”,这是我能召集的最好的防御。

不过,您必须承认,按照原始海报所说的那样做是一种非常糟糕的做法。

如果是这样的话,那么 C++ 就不会像今天这样可移植,并且崩溃的应用程序永远不会被操作系统清理。

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

于 2008-09-17T14:42:15.337 回答
0

@马特克鲁克香克

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

我不同意这是编译器行为,甚至是编译器问题。正如您所指出的,“new”关键字被编译并链接到运行时库。这些运行时库以独立于操作系统的一致语法处理对操作系统的内存管理调用,并且这些运行时库负责使 malloc 和 new 工作在 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),其中结构大小将取决于可变长度字符串”

这个评论听起来像是他在质疑分配的动态性质,而不是正在进行的演员表,因此想知道这是否会导致内存泄漏。如果您愿意的话,我正在字里行间阅读;)。

于 2008-09-17T15:16:15.600 回答
0

除了上面的优秀答案,我还想补充一下:

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

于 2008-09-17T15:29:45.393 回答
-1

Rob Walker的回答很好。

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

于 2008-09-16T14:54:13.757 回答
-1

ericmayo.myopenid.com 大错特错,以至于有足够声誉的人应该对他投反对票。

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

于 2008-09-16T19:20:54.550 回答