はじめに
Delphi ではプロジェクトファイルの先頭で次のように記述しておくと、アプリケーション終了時に (していれば) メモリリークが報告されます 1。
ReportMemoryLeaksOnShutdown := True;
そこそこ大きなプログラムを書くと、何故か String 型変数でメモリリークが報告される事があります。こういう奴です。
Unexpected Memory Leak
An unexpected memory leak has occurred. The unexpected small block leaks are:
nn - nn bytes: UnicodeString x 1
See also:
原因
例えば次のようなコードがあったとします。
program SimpleRec1;
{$APPTYPE CONSOLE}
type
TUserPassRec = record
User: Integer;
Pass: String;
end;
PUserPassRec = ^TUserPassRec;
var
PUP: PUserPassRec;
begin
ReportMemoryLeaksOnShutdown := True;
New(PUP);
try
PUP^.User := 123;
PUP^.Pass := 'HogeHoge';
Writeln(PUP^.User, PUP^.Pass);
finally
DisPose(PUP);
end;
end.
このコードはメモリリークを起こしませんが、例えば C / C++ 由来のコードを移植したとしましょう。「ヌル終端文字列は扱いづらいので String にした」とかそういうシチュエーションです。メモリ確保/解放関数に別のものが使われているかもしれません。
program SimpleRec2;
{$APPTYPE CONSOLE}
type
TUserPassRec = record
User: Integer;
Pass: String;
end;
PUserPassRec = ^TUserPassRec;
var
PUP: PUserPassRec;
begin
ReportMemoryLeaksOnShutdown := True;
PUP := GetMemory(SizeOf(TUserPassRec));
try
PUP^.User := 123;
PUP^.Pass := 'HogeHoge';
Writeln(PUP^.User, PUP^.Pass);
finally
FreeMemory(PUP);
end;
end.
このコード例ではレコード型動的変数のメモリが正しく解放されているにも関わらず、メモリリークが報告されると思います。何故かと言うと、文字列型の変数を未割当て状態にせずにメモリ解放したからです。
説明になっていないかもしれませんが、Delphi の String 型変数は特定の条件下では 未割当て状態にしてからメモリを解放
する必要があります。その条件とは次の 2 つを満たす場合です。
-
Dispose()
以外の方法でメモリを解放した場合。 - String 型変数の中身が空ではない場合 (未割当て状態でない場合)。
この条件を満たす場合には、メモリ解放前に Finalize()
を使って String 型変数を未割当て状態にしなければなりません。
program SimpleRec3;
{$APPTYPE CONSOLE}
type
TUserPassRec = record
User: Integer;
Pass: String;
end;
PUserPassRec = ^TUserPassRec;
var
PUP: PUserPassRec;
begin
ReportMemoryLeaksOnShutdown := True;
PUP := GetMemory(SizeOf(TUserPassRec));
try
PUP^.User := 123;
PUP^.Pass := 'HogeHoge';
Writeln(PUP^.User, PUP^.Pass);
Finalize(PUP^.Pass); // <- 未割当て状態にする
finally
FreeMemory(PUP);
end;
end.
レコード型に String 型のフィールドを配置すると、この状況が発生しうるのです。
また、String 型フィールドを未割当て状態にするのに、Finalize()
の代わりに SetLength()
を使う事もできます。
SetLength(PUP^.Pass, 0); // <- 長さを 0 にする (未割当て状態にする)
See also:
- System.Initialize (DocWiki)
- System.Finalize (DocWiki)
- System.SetLength (DocWiki)
- 長い文字列型 - 内部データ形式(Delphi)(Qiita)
- 10.2. New と Dispose - <10> ポインタ型 (標準 Pascal 範囲内での Delphi 入門) (Qiita)
少し複雑な例
上記例では問題解決方法を説明するために、String 型のフィールドを未割当状態にしていました。少し複雑な例を見てみましょう。
program SimpleRec4;
{$APPTYPE CONSOLE}
type
TUserPassRec = record
User: Integer;
Pass: String;
end;
TEmployeeRec = record
Code: Integer;
FirstName: String;
LastName: String;
UserPass: TUserPassRec;
end;
PEmployeeRec = ^TEmployeeRec;
var
PE: PEmployeeRec;
begin
ReportMemoryLeaksOnShutdown := True;
PE := GetMemory(SizeOf(TEmployeeRec));
try
PE^.Code := 1;
PE^.FirstName := 'John';
PE^.LastName := 'Smith';
PE^.UserPass.User := 123;
PE^.UserPass.Pass := 'HogeHoge';
Writeln(PE^.UserPass.User, PE^.UserPass.Pass);
Finalize(PE^.FirstName); // <- 未割当て状態にする
Finalize(PE^.LastName ); // <- 未割当て状態にする
Finalize(PE^.UserPass.Pass); // <- 未割当て状態にする
finally
FreeMemory(PE);
end;
end.
コード例のように String 型のフィールドを一つずつ未割当て状態にしてもいいのですが、Finalize()
にレコード型の動的変数を指定すれば String 型のフィールドすべてを一気に未割当て状態にする事ができます。
...
Writeln(PE^.UserPass.User, PE^.UserPass.Pass);
Finalize(PE^); // <- 未割当て状態にする (動的変数指定)
finally
...
但し、この時 Finalize()
にレコード型ポインタ変数を渡してはいけません。次の例は誤りです。
...
Writeln(PE^.UserPass.User, PE^.UserPass.Pass);
Finalize(PE); // <- × 未割当て状態にする (ポインタ変数指定)
finally
...
おわりに
レコード型を動的変数として使う場合には、由緒正しく New()
と Dispose()
を使いましょう、というお話でした。
See also:
-
素の状態で使えるのは Delphi 2006 (BDS 2006 / Turbo Delphi) から。 ↩