LoginSignup
3
6

More than 3 years have passed since last update.

【Delphi】構造体と文字列とポインタ

Last updated at Posted at 2018-10-19

はじめに

あまり深い意味はありません。思い付きで C 言語と Object Pascal (Delphi) の違いを記述してみる事にしました。

コード

C 言語でのコードと素直な移植

C 言語で以下のようなコードがあったとします。

#include <stdio.h>
#include <stdlib.h>

// 構造体の宣言
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;
}

これをベタ移植するとこうなります。

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 版なので正確にはこうなります。

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

この時点ではポインタが指すメモリ上のアドレスは不定です。

[2] メモリの確保

GetMem() でメモリを確保します。確保するサイズは構造体 Tstrct のサイズです。確保するとポインタ entity は確保したメモリの先頭を指します。
image.png
メンバ num 及び str の内容は不定です。

[3] メンバの初期化 (1)

メンバ num が 0 で初期化されました。
image.png

[4] メンバの初期化 (2)

ここはメンバ str の内容が初期化されているのではなく、str が指す先に 32 文字 (バイト) 分のメモリを確保しています。確保されたメモリの中身は不定です。また、C 言語の文字列 (文字配列) はヌルターミネートなので、32 文字を格納したいのなら 33 文字分確保する必要があります。
image.png

[5] メモリに文字列をコピー

StrPCopy() によって str が指す先に確保されたメモリに Hello World! が格納されます。文字列の最後にヌルターミネーターが付加され、それ以降は不定となります。
image.png

[6] メモリの解放 (1)

原則としてメモリは確保した順番とは逆に解放します。まずは str が指す先 (アドレス B) のメモリを解放します。
image.png
確保されたメモリは解放されますが、中身は初期化されませんし、ポインタの指すアドレスもそのままです。だからといってメモリ解放後にこのポインタを参照してはいけません。

[7] メモリの解放 (2)

今度はポインタ変数 entity (Pstrct 型) が指す先 (アドレス A) のメモリを解放します。
image.png
確保されたメモリは解放されますが、中身は初期化されませんし、ポインタの指すアドレスもそのままです。

先に entity (Pstrct 型) が指す先 (アドレス A) のにメモリを解放してしまうと、アドレス A から連続するメモリをアプリケーションが使った時に、アドレス B を解放できなかったり、全然関係ないアドレスのメモリを解放しようとしてしまいます。
image.png
アドレス B を指すポインタ変数をもう一つ用意 (ポインタのコピー) しておけばメモリ解放はできるかもしれませんが、処理が複雑になるので特別な理由がない限りやめておいた方がいいでしょう。

やりたい事

このコードでやっている事は構造体メンバーへの文字列格納なので、これを最新の Delphi でやるにはどうするのか考えてみましょう。

文字列の処理

コードの中身を考えれば文字列処理なので、Unicode が扱えるように最初のコードに戻します。

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.

^ (キャレット)

キャレットが型識別子の前に現れた場合、それはポインタの型である事を表します。

type
  // 構造体の定義
  Tstrct = record
    num: Integer;
    str: PChar;
  end;
  // 構造体へのポインタの定義
  Pstrct = ^Tstrct; // ポインタ型の定義

Pstrct を定義しなくとも、以下のように記述すれば同等のコードとなります。

var
  // 構造体へのポインタの変数
  entity: ^Tstrct; // <- コレ

キャレットがポインタ変数の後に現れた場合、それはポインタの逆参照...つまり、ポインタが指し示す先 (動的変数) の値を取得します。

  // メンバの初期化
  entity^.num := 0;
  GetMem(entity^.str, SizeOf(Char) * 32);

ではここは何故

  Writeln(entity^.str);

こうではないのでしょう?

  Writeln(entity^.str^);

それは str が PChar 型であり、str^ は Char 型だからです。実際にやってみると解りますが、str^ にした場合には Hello World! ではなく H のみが表示されます。

...さてこのポインタ変数の後に現れるキャレット (逆参照演算子) ですが、よほど古い 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.

ちょっとすっきりしましたね。

確保したメモリの初期化

GetMem() の代わりに AllocMem() を使うとメモリ確保と同時に初期化 (ヌルクリア) が行われます。

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 型にすれば文字列用のメモリ確保は不要です。

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:

いっその事

ポインタ使わなきゃいいんじゃね?

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) を参照してみてください。

メモリリークのレポートは ReportMemoryLeaksOnShutdown を True に設定する事で行えます。

  ...

begin
  // メモリリークのレポート
  ReportMemoryLeaksOnShutdown := True;

  ...

コンソールアプリケーションの場合には標準出力に、フォームのあるアプリケーションの場合にはダイアログでメモリリークがレポートされます (但し Windows アプリケーションのみ)。

See Also:

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6