# 【Delphi】構造体と文字列とポインタ --- tags: Delphi programming objectpascal created_at: 2018-10-19 updated_at: 2020-08-29 --- # はじめに あまり深い意味はありません。思い付きで C 言語と Object Pascal (Delphi) の違いを記述してみる事にしました。 # コード ## C 言語でのコードと素直な移植 C 言語で以下のようなコードがあったとします。 ```C: #include #include // 構造体の宣言 typedef struct { int num; char *str; } strct; int main(void) { // ポインタ型の変数を生成 strct *entity; // 動的メモリの確保 entity = (strct*)malloc(sizeof(strct)); // メンバの初期化 entity->num = 0; entity->str = (char*)malloc(sizeof(char) * 32); // メモリに文字列を代入 sprintf(entity->str, "%s %s!", "Hello", "World"); printf("%s\n", entity->str); // メモリの解放 free(entity->str); free(entity); return 0; } ``` これをベタ移植するとこうなります。 ```pascal:ANSI_Delphi版 program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; type // 構造体の定義 Tstrct = record num: Integer; str: PChar; end; // 構造体へのポインタの定義 Pstrct = ^Tstrct; var // 構造体へのポインタの変数 entity: Pstrct; begin // メモリの確保 GetMem(entity, SizeOf(Tstrct)); // メンバの初期化 entity^.num := 0; GetMem(entity^.str, SizeOf(Char) * 32); // メモリに文字列をコピー StrPCopy(entity^.str, Format('%s %s!', ['Hello', 'World'])); Writeln(entity^.str); // メモリの解放 FreeMem(entity^.str); FreeMem(entity); end. ``` C 言語の Char は 8bit なので、Delphi 2007 以前の ANSI 版 Delphi では上記コードで構わないのですが、最近の Delphi は Unicode 版なので正確にはこうなります。 ```Pascal:Unicode_Delphi版 program Project1; {$APPTYPE CONSOLE} uses System.SysUtils, System.AnsiStrings; type // 構造体の定義 Tstrct = record num: Integer; str: PAnsiChar; end; // 構造体へのポインタの定義 Pstrct = ^Tstrct; var // 構造体へのポインタの変数 entity: Pstrct; // [1] begin // メモリの確保 GetMem(entity, SizeOf(Tstrct)); // [2] // メンバの初期化 entity^.num := 0; // [3] GetMem(entity^.str, SizeOf(AnsiChar) * 32); // [4] // メモリに文字列をコピー System.AnsiStrings.StrPCopy(entity^.str, AnsiString(Format('%s %s!', ['Hello', 'World']))); // [5] Writeln(entity^.str); // メモリの解放 FreeMem(entity^.str); // [6] FreeMem(entity); // [7] end. ``` 何をやっているのか順に説明します。 ### [1] 変数の宣言 ポインタ型 (Pstrct 型) の変数 entity がメモリ上に確保されます。 ![image.png](./images/c12b64ae-cb2d-8707-7869-16b5f064668b.png) この時点ではポインタが指すメモリ上のアドレスは不定です。 ### [2] メモリの確保 [GetMem()](http://docwiki.embarcadero.com/Libraries/ja/System.GetMem) でメモリを確保します。確保するサイズは構造体 Tstrct のサイズです。確保するとポインタ entity は確保したメモリの先頭を指します。 ![image.png](./images/df469c1a-6e9c-4175-01f5-192591824944.png) メンバ num 及び str の内容は不定です。 ### [3] メンバの初期化 (1) メンバ num が 0 で初期化されました。 ![image.png](./images/c61e9a54-242f-c0c2-32cd-f7e34d749d4f.png) ### [4] メンバの初期化 (2) ここはメンバ str の内容が初期化されているのではなく、str が指す先に 32 文字 (バイト) 分のメモリを確保しています。確保されたメモリの中身は不定です。また、C 言語の文字列 (文字配列) は**ヌルターミネート**なので、32 文字を格納したいのなら 33 文字分確保する必要があります。 ![image.png](./images/7b087791-4a15-845f-af98-3f204eb0de53.png) ### [5] メモリに文字列をコピー [StrPCopy()](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.StrPCopy) によって str が指す先に確保されたメモリに Hello World! が格納されます。文字列の最後に**ヌルターミネーター**が付加され、それ以降は不定となります。 ![image.png](./images/73937aa6-7129-510e-b400-eee774642b73.png) ### [6] メモリの解放 (1) 原則としてメモリは確保した順番とは逆に解放します。まずは str が指す先 (アドレス B) のメモリを解放します。 ![image.png](./images/cf2c4f41-9a99-aa32-b2f4-cee9d9401348.png) 確保されたメモリは解放されますが、中身は初期化されませんし、ポインタの指すアドレスもそのままです。だからといってメモリ解放後にこのポインタを参照してはいけません。 ### [7] メモリの解放 (2) 今度はポインタ変数 entity (Pstrct 型) が指す先 (アドレス A) のメモリを解放します。 ![image.png](./images/337556b3-f6de-736a-49dc-b951c2effb03.png) 確保されたメモリは解放されますが、中身は初期化されませんし、ポインタの指すアドレスもそのままです。 先に entity (Pstrct 型) が指す先 (アドレス A) のにメモリを解放してしまうと、アドレス A から連続するメモリをアプリケーションが使った時に、アドレス B を解放できなかったり、全然関係ないアドレスのメモリを解放しようとしてしまいます。 ![image.png](./images/5090fab7-b968-fec7-6025-297fbd87a4f2.png) アドレス B を指すポインタ変数をもう一つ用意 (ポインタのコピー) しておけばメモリ解放はできるかもしれませんが、処理が複雑になるので特別な理由がない限りやめておいた方がいいでしょう。 ## やりたい事 このコードでやっている事は構造体メンバーへの文字列格納なので、これを最新の Delphi でやるにはどうするのか考えてみましょう。 ### 文字列の処理 コードの中身を考えれば文字列処理なので、Unicode が扱えるように最初のコードに戻します。 ```pascal: program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; type // 構造体の定義 Tstrct = record num: Integer; str: PChar; end; // 構造体へのポインタの定義 Pstrct = ^Tstrct; var // 構造体へのポインタの変数 entity: Pstrct; begin // メモリの確保 GetMem(entity, SizeOf(Tstrct)); // メンバの初期化 entity^.num := 0; GetMem(entity^.str, SizeOf(Char) * 32); // メモリに文字列をコピー StrPCopy(entity^.str, Format('%s %s!', ['Hello', 'World'])); Writeln(entity^.str); // メモリの解放 FreeMem(entity^.str); FreeMem(entity); end. ``` ### ^ (キャレット) キャレットが型識別子の前に現れた場合、それは**ポインタの型**である事を表します。 ```pascal: type // 構造体の定義 Tstrct = record num: Integer; str: PChar; end; // 構造体へのポインタの定義 Pstrct = ^Tstrct; // ポインタ型の定義 ``` Pstrct を定義しなくとも、以下のように記述すれば同等のコードとなります。 ```pascal: var // 構造体へのポインタの変数 entity: ^Tstrct; // <- コレ ``` キャレットがポインタ変数の後に現れた場合、それは**ポインタの逆参照**...つまり、ポインタが指し示す先 (動的変数) の値を取得します。 ```pascal: // メンバの初期化 entity^.num := 0; GetMem(entity^.str, SizeOf(Char) * 32); ``` ではここは何故 ```pascal: Writeln(entity^.str); ``` こうではないのでしょう? ```pascal: Writeln(entity^.str^); ``` それは str が PChar 型であり、str^ は Char 型だからです。実際にやってみると解りますが、str^ にした場合には Hello World! ではなく H のみが表示されます。 ...さてこのポインタ変数の後に現れるキャレット (逆参照演算子) ですが、よほど古い Delphi でない限りは省略可能です。 ```pascal: program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; type // 構造体の定義 Tstrct = record num: Integer; str: PChar; end; // 構造体へのポインタの定義 Pstrct = ^Tstrct; var // 構造体へのポインタの変数 entity: Pstrct; begin // メモリの確保 GetMem(entity, SizeOf(Tstrct)); // メンバの初期化 entity.num := 0; GetMem(entity.str, SizeOf(Char) * 32); // メモリに文字列をコピー StrPCopy(entity.str, Format('%s %s!', ['Hello', 'World'])); Writeln(entity.str); // メモリの解放 FreeMem(entity.str); FreeMem(entity); end. ``` ちょっとすっきりしましたね。 ### 確保したメモリの初期化 [GetMem()](http://docwiki.embarcadero.com/Libraries/ja/System.GetMem) の代わりに [AllocMem()](http://docwiki.embarcadero.com/Libraries/ja/System.AllocMem) を使うとメモリ確保と同時に初期化 (ヌルクリア) が行われます。 ```pascal: program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; type // 構造体の定義 Tstrct = record num: Integer; str: PChar; end; // 構造体へのポインタの定義 Pstrct = ^Tstrct; var // 構造体へのポインタの変数 entity: Pstrct; begin // メモリの確保 entity := AllocMem(SizeOf(Tstrct)); // メモリ確保&ヌルクリア // メンバの初期化 entity.str := AllocMem(SizeOf(Char) * 32); // メモリ確保&ヌルクリア // メモリに文字列をコピー StrPCopy(entity.str, Format('%s %s!', ['Hello', 'World'])); Writeln(entity.str); // メモリの解放 FreeMem(entity.str); FreeMem(entity); end. ``` ### 構造体のメンバを String 型に 構造体の str メンバを String 型にすれば文字列用のメモリ確保は不要です。 ```pascal: program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; type // 構造体の定義 Tstrct = record num: Integer; str: String; // String 型に変更 end; // 構造体へのポインタの定義 Pstrct = ^Tstrct; var // 構造体へのポインタの変数 entity: Pstrct; begin // メモリの確保 entity := AllocMem(SizeOf(Tstrct)); // メモリに文字列をコピー entity.str := Format('%s %s!', ['Hello', 'World']); Writeln(entity.str); // メモリの解放 Finalize(entity.str); // これがないとメモリリークが報告される FreeMem(entity); end. ``` `Finalize(entity.str);` の所は `SetLength(entity.str, 0);` でも構いません。 **See Also:** - [System.Finalize (DocWiki)](hhttp://docwiki.embarcadero.com/Libraries/ja/System.Finalize) - [System.SetLength (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SetLength) - [【Delphi】String 型変数で身に覚えのないメモリリークが報告される (Qiita)](./c3912518bb9c4e18b40a.md) ### いっその事 ポインタ使わなきゃいいんじゃね? ```pascal: program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; type // 構造体の定義 Tstrct = record num: Integer; str: String; end; var // 構造体変数 entity: Tstrct; begin // メモリに文字列をコピー entity.num := 0; entity.str := Format('%s %s!', ['Hello', 'World']); Writeln(entity.str); end. ``` ...身も蓋もないですが (^^;A # おわりに Delphi では文字列の処理にポインタを使う事も使わない事もできます。詳細については[ドキュメント (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8) を参照してみてください。 メモリリークのレポートは [ReportMemoryLeaksOnShutdown](http://docwiki.embarcadero.com/Libraries/ja/System.ReportMemoryLeaksOnShutdown) を True に設定する事で行えます。 ```pascal: ... begin // メモリリークのレポート ReportMemoryLeaksOnShutdown := True; ... ``` コンソールアプリケーションの場合には標準出力に、フォームのあるアプリケーションの場合にはダイアログでメモリリークがレポートされます (但し Windows アプリケーションのみ)。 **See Also:** - [Delphi Community Edition (無償版) (Embarcadero)](https://www.embarcadero.com/jp/products/delphi/starter) - [ポインタとポインタ型(Delphi)(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%9D%E3%82%A4%E3%83%B3%E3%82%BF%E3%81%A8%E3%83%9D%E3%82%A4%E3%83%B3%E3%82%BF%E5%9E%8B%EF%BC%88Delphi%EF%BC%89) - [文字列型(Delphi)(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%96%87%E5%AD%97%E5%88%97%E5%9E%8B%EF%BC%88Delphi%EF%BC%89) - [内部データ形式(Delphi)(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%86%85%E9%83%A8%E3%83%87%E3%83%BC%E3%82%BF%E5%BD%A2%E5%BC%8F%EF%BC%88Delphi%EF%BC%89) - [メモリと文字列バッファ確保の関数 (Mr.XRAY)](http://mrxray.on.coocan.jp/Delphi/Others/MemAlloc.htm) - [変数やオブジェクトの初期値 (Mr.XRAY)](http://mrxray.on.coocan.jp/Delphi/Others/VarInitialize.htm#09) - [【C言語入門】mallocの使い方(memset, memcpy, free, memcmp) (侍エンジニア塾)](https://www.sejuku.net/blog/25002)