【问题标题】:Delphi: Memory leak when creating a TStringList inside a classDelphi:在类中创建 TStringList 时发生内存泄漏
【发布时间】:2014-05-28 15:56:01
【问题描述】:

我有这段代码

TSql = class
  private
    FConnString: TStringList;
  public
    property ConnString: TStringList read FConnString write FConnString;
    constructor Create;
    destructor Destroy;
  end;

var 
  Sql: TSql;

...

implementation

{$R *.dfm}

constructor TSql.Create;
begin
  //inherited Create;
  FConnString:=TStringList.Create;
end;

destructor TSql.Destroy;
begin
  FConnString.Free;
  //inherited Destroy;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Sql.Create;
  Sql.ConnString.Add('something');
  showmessage(Sql.ConnString.Text);
  Sql.Destroy;
end;

为什么在创建 FConnString 时,按下按钮后会造成内存泄漏?

....................... ..................................... ..................................... ....................................

【问题讨论】:

  • 让我展开。如果您能够正确调用 Sql.Create,则您已经拥有 Tsql 的有效实例。当您在其上调用 Create 时,它​​会创建另一个 FConnString。 IOW,删除那条线,你会没事的。
  • 应该是Sql := TSql.Create;?
  • 是的……我的错。你是对的!有用!无论如何,我的真实代码仍然存在问题。我会更深入地搜索。在我的原始代码中使用 Sql:=TSql.Create 但仍然有问题。
  • 我回滚了这个问题。瞄准移动的目标并不好玩。
  • 那个全局变量让我感觉不舒服

标签: class delphi memory-leaks tstringlist


【解决方案1】:

我看到了两件事。其中第一个已经被其他 cmets 覆盖,并回答了关于析构函数缺乏“覆盖”的问题。

第二个问题是属性声明本身。通常,您永远不应该在“write”子句中声明引用对象字段的属性。原因是分配给该属性将“泄漏”该字段中的现有实例。使用属性声明的“write”子句的方法:

property ConnString: TStringList read FConnString write SetConnString;
...
procedure TSql.SetConnString(Value: TStringList);
begin
  FConnString.Assign(Value);
end;

还要注意,此方法也不会覆盖 FConnString 字段。它只是将 Value TStringList 的“值”或“内容”复制到 FConnString 实例中。以这种方式,TSql 实例完全控制了该字段的生命周期。分配该属性的代码负责控制 Value TStringlist 的生命周期。

【讨论】:

    【解决方案2】:

    编辑实际问题

    问题中的原代码如下:

    procedure TForm1.Button1Click(Sender: TObject);
    begin
      Sql.Create;
      Sql.ConnString.Add('something');
      showmessage(Sql.ConnString.Text);
      Sql.Destroy;
    end;
    

    问题行是Sql.Create;,应该是Sql := TSql.Create;。这导致内存泄漏的原因如下:

    • Sql.Create;nil 引用中调用。
    • 这会调用TStringList.Create; 并尝试将结果分配给FConnString
    • 因为 Sql 是一个 nil 引用,这会触发访问冲突。
    • 问题是无法销毁已创建的TStringList 实例。

    原始答案中的其他问题

    你的析构函数是虚拟的,你没有覆盖。
    你没有调用你继承的析构函数。

    TSql = class
      private
        FConnString: TStringList;
      public
        property ConnString: TStringList read FConnString write FConnString;
        constructor Create;
        destructor Destroy; override; //Correction #1
      end;
    
    destructor TSql.Destroy;
    begin
      FConnString.Free;
      inherited Destroy; //Correction #2
    end;
    

    编辑

    一些一般提示:

    1. 我赞赏你使用组合(使FConnString 成为成员而不是从TStringList 继承)。但是,通过公开暴露它,您将失去许多好处。具体来说,您将面临Law of Demeter 违规行为。我不是说永远不要这样做。但请注意,如果大量客户端代码直接访问ConnString,您可能会产生维护问题。

    2. 声明FConnString: TStringList;违反了Program to an interface, not an implementation的原则。 TStringListTStrings 的特定实现,并且此声明阻止使用 TStrings 的其他子类。这更多的是与#1 相结合的问题:如果在几年后您发现并想要切换到绑定到TStrings 客户端代码的不同/更好的子类实现@ 987654339@ 现在会产生更多的工作和风险.基本上首选的方法可以总结为:

      • 将变量声明为抽象基类型。
      • 将实例创建为专门选择的实现子类。
      • 让多态性确保子类行为在被覆盖的方法中正确应用。

    这也是一般准则。如果您特别需要访问在层次结构的TStringList 级别添加的属性/方法,那么您必须绑定到该类。但如果你不需要它,就不要这样做。

    【讨论】:

    • 析构函数已经是虚拟的,在TObject。你需要声明为override
    • Correction #2 不会修复泄漏,TObject.Destroy 什么都不做。 #1 也是如此。
    • @SertacAkyuz 正确,它们是附加问题。我已经更新了对实际内存泄漏的解释。
    • @Craig - 谢谢。我确信这不是问题,因为问题中会提到 AV。问题是,这个问题是假的。
    • 很好的答案。除了错误之外,我还是从您的详细回答中学到了更多东西。谢谢@CraigYoung!
    【解决方案3】:

    您忘记使用 override 说明符声明 Destroy(),因此当对象被销毁时实际上并没有调用 TSql.Destroy

    destructor Destroy; override;
    

    【讨论】:

    • 没有被调用?他正在调用 Tsql.Destroy。
    • 虽然他应该覆盖Destroy,但在这种情况下,代码会直接调用Destroy,所以这个问题并没有解决真正的问题。
    【解决方案4】:

    对象未正确创建。必须是:

    Sql:=TSql.Create;
    

    从这里开始出现内存泄漏。

    【讨论】:

    • 不,那会是一个 AV。
    • @SertacAkyuz 我赞成您的评论,然后意识到错误实际上导致内存泄漏(请参阅我的更新答案)。 (唉,我无法删除对评论的赞成票。)
    • @Craig - 我认为我的评论没有任何问题。提问者肯定没有 AV,否则他的问题中就会有它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-10
    • 2023-03-10
    • 1970-01-01
    • 2018-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多