此C ++代码是否会导致内存泄漏(新的广播数组)

时间:2020-03-05 18:56:11  来源:igfitidea点击:

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

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

后来,使用delete调用释放了内存:

delete pStruct;

数组" new []"和非数组" delete"的混合使用会导致内存泄漏还是取决于编译器?我最好将这段代码更改为使用mallocfree吗?

解决方案

回答

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

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

最好是正确执行此操作,因为这也会正确调用任何构造函数和反构造函数

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

回答

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

更重要的是,如果" STRUCT"在何处(或者曾经被提供)一个析构函数,则它将调用该析构函数而不调用相应的构造函数。

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

delete [] (BYTE*) pStruct;

回答

我们可能会回退到BYTE *和删除:

delete[] (BYTE*)pStruct;

回答

代码的行为是不确定的。我们可能很幸运(也可能不是很幸运),并且可以与编译器一起使用,但这确实不是正确的代码。它有两个问题:

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

因此,要完全正确,我们想做这样的事情:

delete [] (BYTE*)(pStruct);

回答

Rob Walker的回复很好。

只是很小的补充,如果我们没有任何构造函数或者/和析构函数,因此我们基本上需要分配和释放大量原始内存,请考虑使用free / malloc对。

回答

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

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

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

 // do stuff with pStruct

delete [] pBytes ;

回答

我们正在混合使用C和C ++的处理方式。为什么分配的资源超过STRUCT的大小?为什么不只是"新STRUCT"?如果必须执行此操作,则在这种情况下使用malloc和free可能会更清楚,因为那样的话,我们或者其他程序员可能不太可能对分配的对象的类型和大小进行假设。

回答

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

除了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可能很容易;)

回答

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

回答

使用运算符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;
}

回答

C ++标准明确指出:

delete-expression:
             ::opt delete cast-expression
             ::opt delete [ ] cast-expression
The first alternative is for non-array objects, and the second is for arrays. The operand shall have a pointer type, or a class type having a single conversion function (12.3.2) to a pointer type. The result has type void.
  
  In the first alternative (delete object), the value of the operand of delete shall be a pointer to a non-array object [...] If not, the behavior is undefined.

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

它可能会泄漏内存,不能泄漏内存,或者可能导致系统崩溃。确实,我测试过代码的C ++实现在delete表达式点中止了程序执行。

回答

我个人认为我们最好使用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

回答

如果我们确实必须执行此类操作,则可能应该直接调用运算符new

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

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

回答

我目前无法投票,但是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

回答

如其他文章中所强调的:

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

2)将" new"和" delete"的数组/非数组版本混合在一起是未定义的行为。 (C ++ '03 5.3.5 / 4)

从源头看,似乎有人进行了搜索并替换了" malloc"和" free",以上就是结果。 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;

回答

我认为这是没有内存泄漏。

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

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

因此,分配的内存被"记录"在操作系统的全局内存分配表中。内存表由它们的指针索引。因此,在相应的delete调用中,最初分配的所有内存都是可用的。 (内存碎片化也是该领域的热门主题)。

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

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

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

回答

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

回答

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

C或者C ++运行时库正在管理由操作系统按块分配给它的堆,就像我们指出的那样,Eric。但是开发人员有责任向编译器指示应进行哪些运行时调用以释放内存,并可能破坏那里的对象。在这种情况下,必须使用向量删除(aka delete []),以便C ++运行时将堆置于有效状态。当PROCESS终止时,OS足够智能以取消分配底层内存块这一事实并不是开发人员应该依赖的。这就像根本不调用delete一样。

回答

@马特·克鲁克申克
我们应该注意并再次阅读我写的内容,因为我从不建议不要调用delete [],而只是清理操作系统。而且我们对管理堆的C ++运行时库错了。如果真是这样,那么C ++将不会像当今那样具有可移植性,并且崩溃的应用程序永远也不会被操作系统清除。 (确认存在特定于操作系统的运行时,使C / C ++显得不可移植)。我挑战我们从kernel.org的Linux源代码中找到stdlib.h。实际上,C ++中的new关键字正在使用与malloc相同的内存管理例程。

C ++运行时库进行OS系统调用,并且由OS来管理堆。部分正确之处在于,运行时库指示何时释放内存,但是,它们实际上并没有直接遍历任何堆表。换句话说,我们链接的运行时不会将代码添加到应用程序中以遍历堆进行分配或者取消分配。在Windows,Linux,Solaris,AIX等中就是这种情况。这也是为什么我们不会在任何Linux内核源代码中对malloc进行优化,也不会在Linux源代码中找到stdlib.h的原因。了解这些现代操作系统的虚拟内存管理器会使事情进一步复杂化。

有没有想过为什么我们可以在1G盒子上调用malloc以获得2G RAM并仍返回有效的内存指针?

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

可能是我们在说同样的话Matt,但我认为我们可能会稍微混淆一下功能。我过去经常维护C / C ++编译器...

回答

@ericmayo薄饼。好吧,尝试使用VS2005,我无法从vector new进行的内存中标量删除中得到诚实的泄漏。我猜编译器的行为在这里是"未定义的",这是我可以召集的最佳防御方法。

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

If that were the case then C++ would
  not be portable as is today and a
  crashing application would never get
  cleaned up by the OS.

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

回答

@马特·克鲁克申克

"好吧,在VS2005上进行实验,我无法从向量new产生的内存中进行标量删除中得到诚实的泄漏。我猜这里的编译器行为是"未定义的",这是我可以召集的最佳防御方法。"

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

操作系统管理内存。

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

既然我们有Linux的源代码,那么我们可以去看看malloc是如何实现的。我们将看到malloc实际上是在GCC编译器中实现的,该编译器反过来基本上在内核中进行了两个Linux系统调用来分配内存。永远不要malloc本身,实际上是在管理内存!

而且不要拿走我。阅读Linux OS的源代码,或者我们可以看到K&R对此的评价...这是指向C上的K&R的PDF链接。

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

请参阅第149页的结尾处:
"对malloc和free的调用可以按任何顺序进行; malloc调用
在操作系统上获得必要的更多内存。这些例程说明了以相对机器无关的方式编写与机器相关的代码所涉及的一些注意事项,并且还显示了结构,联合和类型定义的实际应用。"

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

哦,我不同意。我的观点是,原始张贴者的代码不利于内存泄漏。我就是这么说在最佳实践方面,我并没有参与其中。由于代码调用了delete,因此内存正在释放。

在辩护中,我同意,如果原始发布者的代码从未退出或者从未进入过Delete调用,那么该代码可能会发生内存泄漏,但是由于他后来声明将看到Delete被调用。 "但是以后,可以使用删除调用释放内存:"

而且,我之所以做出回应是因为OP的注释"可变长度结构(TAPI),其中结构大小将取决于可变长度字符串"

那句话听起来像是他在质疑分配相对于所进行的强制转换的动态性质,因此想知道这是否会导致内存泄漏。如果我们愿意的话,我正在两字之间阅读。

回答

除了上述出色的答案,我还想补充一下:

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

回答

@eric感谢评论。但是,我们一直在说些什么,这让我很生气:

Those run-time libraries handle the
  memory management calls to the OS in a
  OS independent consistent syntax and
  those run-time libraries are
  responsible for making malloc and new
  work consistently between OSes such as
  Linux, Windows, Solaris, AIX, etc....

这不是真的。例如,编译器编写器提供了std库的实现,并且它们绝对可以自由地以依赖于OS的方式实现它们。例如,它们是免费的,可以对malloc进行一次大型调用,然后根据需要在块中管理内存。

之所以提供兼容性,是因为std等的API相同,而不是因为运行时库都转过来并调用完全相同的OS调用。

回答

关键字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"对象。我不知道它不能"正常运行"的任何情况,但是它模糊了原始内存分配和对象创建之间的区别,因此我建议不要这样做。

在这种特殊情况下,将"删除"的两个步骤分开是没有好处的,但是我们确实需要手动控制初始分配。上面的代码在"一切正常"的情况下工作,但是在MyObject的构造函数抛出异常的情况下,它将泄漏原始内存。尽管可以在分配时使用异常处理程序来捕获并解决此问题,但提供一个新的自定义运算符可能会比较整洁,以便可以通过一个placement 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;

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

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

delete ObjectPtr;

概括!

  • 看起来没有演员! operator newoperator delete处理原始内存,放置new可以在原始内存中构造对象。从" void *"到对象指针的显式强制转换通常是某些逻辑上错误的标志,即使它确实"起作用"。
  • 我们已经完全忽略了new []和delete []。这些可变大小的对象在任何情况下都不会在数组中工作。
  • new放置使新表达式不泄漏,新表达式仍然求值指向需要销毁的对象和需要释放的内存的指针。使用某种类型的智能指针可能有助于防止其他类型的泄漏。从好的方面来说,我们已经将普通的`delete'删除作为执行此操作的正确方法,因此大多数标准的智能指针都可以使用。