Delphi:除了节省一点内存之外,使用System.New()而不是本地变量的优点是什么?

10
让我们回到基础知识。坦白地说,我以前从未使用过NewDispose函数。然而,在我阅读了Embarcadero Technologies网站上的New()文档和示例以及Delphi Basics对New()的解释之后,脑海中留下了一些问题:
除了节省少量内存之外,使用System.New()有什么优点,可以替代使用局部变量? New()的常见代码示例或多或少如下:
  var
      pCustRec : ^TCustomer;
  begin
      New(pCustRec);
      pCustRec^.Name := 'Her indoors';
      pCustRec^.Age  := 55;
      Dispose(pCustRec);
  end;

在什么情况下,上面的代码比下面的代码更合适?
  var
      CustRec : TCustomer;
  begin
      CustRec.Name := 'Her indoors';
      CustRec.Age  := 55;
  end;

1
使用动态数组构造来分配堆空间,而不是使用New/Dispose。它们是引用计数的,并且在超出范围时会自动释放。 - LU RD
1
那么你会创建一个只有一个元素的数组吗?这样的代码并不是很规范。 - alzaimar
@alzaimar,如果只有一个元素,最方便的方法是无论如何都使用堆栈。 - LU RD
@LURD:在这种特殊情况下(以及其他简单的结构)是的。 - alzaimar
6个回答

11

如果可以使用本地变量,务必这样做。这是一个几乎没有例外的规则。这样可以得到最干净、最高效的代码。

如果需要在堆上分配空间,请使用动态数组、GetMem或New。在分配记录时请使用New。

无法使用堆栈的示例包括大小在编译时不知道或非常大的结构体。但对于记录来说,这是New的主要用例,这些关注点很少适用。

因此,如果需要在记录的堆栈和堆之间进行选择,通常情况下应该选择堆栈。


这是一个直截了当的答案,明确地评判了选择。我喜欢这个。谢谢。 - Astaroth
4
使用指针(即使用New)的另一个例子是增长结构,如链表、树等。每个元素都用New()分配。值得一提的是,对于具有正确类型的指针而言,使用New比GetMem更可取,因为New会正确初始化内存,Dispose会正确地完成最终处理。 - Rudy Velthuis
1
+1,终于看到了New()拯救的情况。谢谢。 - Astaroth

8

从另一个角度来看:

两者都可能遭受缓冲区溢出并被利用

如果局部变量溢出,您将获得堆栈损坏

如果堆变量溢出,则会发生堆损坏

有人说堆栈损坏比堆损坏更容易被利用,但总体上这并不是真的

请注意,操作系统、处理器架构、库和语言中有各种机制来帮助防止这些漏洞被利用。

例如,有DEP(数据执行保护)、ASLR(地址空间布局随机化)以及更多在维基百科中提到的措施


2
你可以在完整调试模式下运行FastMM4来跟踪这样的溢出。但是,如果没有指针算术或索引访问,如OP所问,如何触发具有固定大小记录的缓冲区溢出?对于使用堆栈分配的静态变量或New / Dispose,您无法使用此类漏洞。在我看来,您的答案只是超出了范围。 - Arnaud Bouchez
通常情况下,它从简单开始,然后代码不断增长,最终现实会超越。把我的答案视为一种预防性警告,因此使用了“从不同的角度”的说法。 - Jeroen Wiert Pluimers

7
本地静态变量在限制的栈上保留空间。分配的内存位于堆上,即基本上所有可用内存。
正如所提到的,堆栈空间是有限的,因此应避免大型局部变量以及通过值传递的大型参数(在参数声明中缺少var/const)。
关于内存使用的说明: 1.简单类型(integer、char、string、double等)直接位于堆栈上。可以通过sizeof(variable)函数确定使用的字节数。 2.记录变量和数组也是如此。 3.指针和对象需要4/8个字节。

默认的堆栈大小为1 MB(如果需要,可以增加)。我认为除非出现特殊情况,否则1 MB已经足够了。然而,如果有其他有用的信息,则可以再加1。 - Astaroth
1
如果你声明本地数组、记录或者传递这样的参数作为值,特别是在使用递归调用时,你的1MB空间将会非常快地缩小到零。 - alzaimar
1
无限/过度深度递归的情况很少见,但如果是这种情况,只需为问题编写一个非递归解决方案即可。 - Astaroth
2
不要拒绝使用指针,这样更容易实现;-)我不会为了能够利用大型本地变量而创建复杂的迭代解决方案。我宁愿使用动态本地变量(或简单的对象)。我可以给你足够多的现实生活(商业)应用程序例子,其中使用大型(!)本地变量会使堆栈溢出。 - alzaimar

4

每个对象(即类实例)总是在堆上分配。

值结构体(简单的数字类型,仅包含这些类型的记录)可以在堆上分配。

动态数组和字符串内容总是在堆上分配。只有引用指针可以在栈上分配。如果您写:

  function MyFunc;
  var s: string;
  ...

在这里,栈上分配了4/8字节,但字符串内容(文本字符)始终在堆上分配。
因此,使用New()/Dispose()的效益较差。如果它不包含引用计数类型,则可以使用GetMem()/FreeMem(),因为没有内部指针需要设置为零。
New()/Dispose()的主要缺点是,如果出现异常,您需要使用try...finally块:
  var
    pCustRec : ^TCustomer;
  begin
    New(pCustRec);
    try
      pCustRec^.Name := 'Her indoors';
      pCustRec^.Age  := 55;
    finally
      Dispose(pCustRec);
    end;
  end;

如果使用栈进行分配,编译器会以隐式的方式为您完成:

  var
    CustRec : TCustomer;
  begin // here a try... is generated
    CustRec.Name := 'Her indoors';
    CustRec.Age  := 55;
  end;  // here a finally + CustRec cleaning is generated

这就是为什么我几乎从不使用New()/Dispose(),而是在堆栈上分配,或者更好地在类内部分配。


“值结构(简单数值类型,仅包含这些类型的记录)可以在堆栈上分配。” - 这句话应该是“...在堆栈上分配”吧? - Frank Schmitt
@Frank,不,他真的是指堆上分配。 - David Heffernan
  1. 一个本地记录变量位于堆栈上。本地对象位于堆上(尽管指针本身位于堆栈上)。如果记录包含短字符串或数组,则会浪费更多的堆栈。因此,一般来说,应避免使用本地记录变量。
  2. 使用New/Dispose与记录指针时,您不必担心大小,这是我的观点。
  3. 是的。误解了。
- alzaimar
加入: 在简单情况下(我忽略了这个特殊情况),当然更容易的方法是将记录分配为静态局部变量。我变得太笼统了(应该更仔细地阅读问题)。 - alzaimar
@alzaimar 不,通常情况下尽量将记录放在堆栈上。尽可能多地将记录放在堆栈上。 - David Heffernan
显示剩余2条评论

2

堆分配的常见情况是当对象必须比创建它的函数更长久时:

  1. 它作为函数的结果或通过 var/out 参数返回,直接返回或返回某个容器。

  2. 它被存储在一些对象、结构体或集合中,这些对象、结构体或集合被传递进来或者在过程内可访问(这包括被发出信号/排队到另一个线程)。


1
在堆栈空间有限的情况下,您可能更喜欢从堆中分配。
参考文献

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接