一、Pascal字符串处理的内存陷阱

在Pascal编程中,字符串处理看似简单,但稍不注意就会掉进内存泄漏的坑。比如使用AnsiString时,如果手动分配内存却忘记释放,程序运行时间一长,内存就像漏水的桶一样慢慢被榨干。

举个典型例子(使用Free Pascal技术栈):

procedure LeakyStringDemo;
var
  str: PChar;  // 使用指针类型字符串
begin
  GetMem(str, 256);  // 分配256字节内存
  StrCopy(str, '这段内存永远不会被释放');  // 写入内容
  // 忘记调用 FreeMem(str) !
end;

问题分析
GetMem分配的内存必须手动释放
• 过程结束时str指针丢失,导致256字节内存永久泄漏

二、动态字符串的常见泄漏场景

1. 字符串拼接引发的泄漏

Pascal的+操作符在循环中会产生大量临时对象:

var
  i: Integer;
  result: string;
begin
  result := '';
  for i := 1 to 10000 do
    result := result + IntToStr(i);  // 每次循环都创建新字符串
end;

优化方案
改用TStringBuilder类(Object Pascal提供):

var
  sb: TStringBuilder;
begin
  sb := TStringBuilder.Create;
  try
    for i := 1 to 10000 do
      sb.Append(IntToStr(i));
    result := sb.ToString;
  finally
    sb.Free;  // 必须显式释放
  end;
end;

2. 引用计数失效案例

当混合使用AnsiStringPChar时:

var
  s: AnsiString;
  p: PChar;
begin
  s := '临时字符串';
  p := PChar(s);       // 转换为指针
  s := '';             // 引用计数减1
  // 此时p可能指向已释放内存!
end;

危险点
PChar转换不增加引用计数
• 原字符串修改后可能导致野指针

三、内存泄漏检测实战方案

1. 使用HeapTrc单元

Free Pascal内置的内存检测工具:

program CheckLeak;
uses
  HeapTrc;  // 内存检测单元

procedure TestProc;
var
  p: Pointer;
begin
  GetMem(p, 1024);  // 故意泄漏1KB
end;

begin
  TestProc;
  // 程序退出时会自动报告泄漏信息
end.

输出示例

Heap dump by heaptrc unit
1024 bytes leaked at $000000000062F5A0

2. 自定义内存跟踪器

通过重写内存管理器实现跟踪:

type
  TTrackAlloc = record
    origMemMgr: TMemoryManager;
    allocCount: Integer;
  end;

var
  tracker: TTrackAlloc;

function TrackGetMem(size: PtrUInt): Pointer;
begin
  Result := tracker.origMemMgr.GetMem(size);
  Inc(tracker.allocCount);
end;

procedure InstallTracker;
var
  newMgr: TMemoryManager;
begin
  GetMemoryManager(tracker.origMemMgr);
  newMgr.GetMem := @TrackGetMem;
  // 同样重写FreeMem/ReallocMem...
  SetMemoryManager(newMgr);
end;

四、最佳实践与解决方案

1. 资源管理三原则

谁分配谁释放:比如StrNew必须搭配StrDispose
使用try-finally块

var
  list: TStringList;
begin
  list := TStringList.Create;
  try
    list.LoadFromFile('data.txt');
    // 处理数据...
  finally
    list.Free;  // 确保释放
  end;
end;

2. 智能指针模式

利用接口自动管理生命周期:

type
  IAutoPtr = interface
    function GetPtr: Pointer;
  end;

  TAutoMemPtr = class(TInterfacedObject, IAutoPtr)
  private
    FPtr: Pointer;
  public
    constructor Create(size: PtrUInt);
    destructor Destroy; override;
  end;

constructor TAutoMemPtr.Create(size: PtrUInt);
begin
  GetMem(FPtr, size);
end;

destructor TAutoMemPtr.Destroy;
begin
  FreeMem(FPtr);
  inherited;
end;

3. 字符串处理优化技巧

• 预分配大字符串空间
• 避免频繁的短字符串操作
• 使用SetLength代替连续拼接

五、不同场景下的选择策略

  1. 短期使用的小字符串
    直接使用ShortString类型(栈上分配)

  2. 需要C语言交互
    使用PChar但配合StrAlloc/StrDispose

  3. 大规模文本处理
    采用TMemoryStream+缓冲机制

procedure ProcessLargeFile;
const
  BUF_SIZE = 65536;
var
  stream: TMemoryStream;
  buffer: array[0..BUF_SIZE-1] of Char;
begin
  stream := TMemoryStream.Create;
  try
    while not EOF(inputFile) do
    begin
      BlockRead(inputFile, buffer, BUF_SIZE);
      stream.Write(buffer, BUF_SIZE);
    end;
    // 处理stream中的数据...
  finally
    stream.Free;
  end;
end;

六、总结与经验分享

经过多年Pascal项目实践,我总结出这些血泪教训:

  1. 所有GetMem调用必须肉眼可见对应的FreeMem
  2. 字符串操作优先使用托管类型(如AnsiString
  3. 复杂逻辑务必使用内存检测工具验证
  4. 第三方库的字符串API要仔细阅读文档

最后记住:Pascal不是带GC的语言,每个字节的生命周期都需要你亲自掌控。养成良好的内存管理习惯,才能写出稳定可靠的程序。