这个C ++代码是否会导致内存泄漏(将数组转换为新的)

时间:2008-09-16 14:47:23

标签: c++ memory-management memory-leaks

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

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

稍后,使用delete调用释放内存:

delete pStruct;

数组new []和非数组delete的混合会导致内存泄漏还是依赖于编译器?我是否最好更改此代码以改为使用mallocfree

24 个答案:

答案 0 :(得分:12)

从技术上讲,我认为它可能会导致分配器不匹配的问题,但在实践中我不知道任何编译器在这个例子中做得不对。

更重要的是,如果STRUCT在哪里(或者曾经给过)析构函数,那么它将在不调用相应构造函数的情况下调用析构函数。

当然,如果您知道pStruct来自何处,为什么不将其转换为删除以匹配分配:

delete [] (BYTE*) pStruct;

答案 1 :(得分: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如果您需要移动所有权。

答案 2 :(得分:6)

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

  1. delete应为数组delete []
  2. 应该在指向与分配类型相同类型的指针上调用delete
  3. 所以完全正确,你想要做这样的事情:

    delete [] (BYTE*)(pStruct);
    

答案 3 :(得分: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可能很容易;)

答案 4 :(得分:4)

C ++标准明确指出:

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

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

     

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

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

它可能会泄漏内存,它可能无法泄漏内存,或者它可能会导致系统崩溃。实际上,我测试代码的C ++实现会在删除表达式中中止程序执行。

答案 5 :(得分:3)

正如其他帖子所强调:

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;

答案 6 :(得分: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的构造函数抛出异常的情况下,它会泄漏原始内存。虽然可以在分配点使用异常处理程序捕获并解决这个问题,但是提供自定义运算符可能更新,以便可以通过放置新表达式来处理完整的构造。

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;

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

在对象生命周期的另一端,因为我们定义了运算符删除(即使我们没有,对象的内存最初来自全局运算符new),以下是破坏运算符的正确方法动态创建的对象。

delete ObjectPtr;

概要!

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

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

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

答案 7 :(得分:2)

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

  

那些运行时库处理   内存管理调用操作系统   OS独立的一致语法和   那些运行时库是   负责制作malloc和new   在操作系统之间始终如一地工作   Linux,Windows,Solaris,AIX等....

事实并非如此。例如,编译器编写器提供std库的实现,并且它们完全可以以OS 依赖方式实现它们。例如,他们可以自由地对malloc进行一次巨大的调用,然后按照他们的意愿管理块内的内存。

提供兼容性是因为std等的API是相同的 - 不是因为运行时库都转向并调用完全相同的OS调用。

答案 8 :(得分:2)

如果真的必须做这类事情,你应该直接调用operator new

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

我相信这样称呼它可以避免调用构造函数/析构函数。

答案 9 :(得分:1)

我目前无法投票,但slicedlime's answer优于Rob Walker's answer,因为问题与分配器无关或STRUCT是否具有析构函数。

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

示例代码导致未定义的行为,简单明了。 slicelime的答案是直接的和重点(警告说'vector'一词应该改为'array',因为vector是STL的东西)。

在C ++ FAQ(第16.12,16.13和16.14节)中很好地介绍了这种东西:

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

答案 10 :(得分:1)

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

答案 11 :(得分:0)

使用operator new并删除:

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;
}

答案 12 :(得分:0)

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

答案 13 :(得分:0)

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

答案 14 :(得分:0)

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

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

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

 // do stuff with pStruct

delete [] pBytes ;

答案 15 :(得分:0)

我认为没有内存泄漏。

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

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

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

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

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

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

答案 16 :(得分:0)

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

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

有没有想过为什么你可以在1G机箱上调用2G内存的malloc并仍然获得有效的内存指针?

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

我们可能会说同样的事情马特,但我认为你可能会混淆引擎盖功能。我用来维护一个C / C ++编译器...

答案 17 :(得分:0)

@ericmayo - cripes。好吧,试验VS2005,我无法通过vector new对内存中的标量删除进行诚实的泄露。我想这里的编译器行为是“未定义的”,是关于我可以集合的最佳防御。

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

  

如果是这样,那么C ++会   不像今天那样便携,而且   崩溃的应用程序永远不会得到   由操作系统清理。

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

答案 18 :(得分:0)

@Matt Cruikshank

“好吧,试验VS2005,我无法通过vector new的内存上的标量删除得到一个诚实的泄漏。我猜这里的编译器行为是”未定义的“,是关于我可以集合的最好的防御。“

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

操作系统管理内存。

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

由于我们拥有Linux的源代码,您可以查看malloc是如何在那里实现的。您将看到malloc实际上是在GCC编译器中实现的,而GCC编译器基本上会对内核进行两次Linux系统调用以分配内存。从来没有,malloc本身,实际上管理记忆!

不要从我这里拿走它。阅读Linux操作系统的源代码,或者你可以看看K&amp; R对它的评价......以下是C和K&amp; R的PDF链接。

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

见第149页附近: “对malloc和free的调用可能以任何顺序发生; malloc调用 在操作系统上根据需要获取更多内存。这些例程说明了以相对机器相关的方式编写依赖于机器的代码所涉及的一些注意事项,并且还显示了结构,联合和typedef的实际应用。“

“你必须承认,做原始海报所说的那是一种非常糟糕的做法。”

哦,我不同意。我的观点是原始海报的代码不利于内存泄漏。这就是我所说的。我并没有在最佳实践方面提出异议。由于代码调用了delete,因此内存空闲。

我同意,在你的辩护中,如果原始海报的代码从未退出或从未进入删除调用,那么代码可能会有内存泄漏,但由于他说明后来他看到删除被调用。 “稍后,使用删除调用释放内存:”

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

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

答案 19 :(得分:0)

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

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

答案 20 :(得分:0)

你可以回到BYTE *和删除:

delete[] (BYTE*)pStruct;

答案 21 :(得分:0)

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

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

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

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

答案 22 :(得分:-1)

Rob Walker reply很好。

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

答案 23 :(得分:-1)

ericmayo.myopenid.com是如此错误,有足够声誉的人应该投票给他。

C或C ++运行时库正在管理操作系统以块为单位给出的堆,有点像你指示的那样,Eric。但是 是开发人员的责任,要向编译器指示应该对哪些运行时调用释放内存,并可能破坏那里的对象。在这种情况下,Vector delete(aka delete [])是必需的,以便C ++运行时将堆保持在有效状态。事实上,当PROCESS终止时,操作系统足够智能以释放底层内存块并不是开发人员应该依赖的东西。这就像从不调用删除一样。