実践・Delphiでポインタ入門

初出:2003/03/10
改定:2004/09/14

<目次>

  1. Delphiとポインタの関係
  2. 実践その1 <TTreeView/ListView.Items.Item.Data>
  3. 実践その2 <TList.Items>
  4. 実践その3 <TComponent.Tag>


1.Delphiとポインタの関係

 プログラミングもある程度経験を積むと、「ポインタ」という用語を目にすることになるだろう。ただ、Delphiでポインタを積極的に使う意義は低い。Delphiは、ポインタをなるべくプログラマに見せないよう、隠蔽して内側で(例えばVCLソース内で)こっそり使っているからだ。そのため、ユーザーはポインタの存在など気にせずにプログラムを組むことができる。

 しかし、Delphiに精通してくると、ポインタを使ったほうが便利になる場面が登場することがある。以下では、ポインタに関する概要を説明した後、実際にポインタを使うと便利な場面を数例紹介する。なお、「ポインタというもの」に関して理論的に知りたい方は、ここを読んでも勉強にならないので他のサイトへ行くことをお勧めする。





2.実践その1 <TTreeView/TListView.Items.Item.Data>

 エクスプローラ風のツリーやリストを使いたい場合に、TTreeView/TListViewは必須だが、その1つ1つの項目(TTreeNode/TListItem)には、Dataプロパティがある。これはPointer型、つまりポインタだ。このプロパティは、項目の1つ1つに入れておきたいデータを保持するために使う。
 例えば、IEのお気に入りを管理するソフトを考えてみよう。項目をクリックしたらそのURLに飛び、項目を削除したらFavoritesフォルダにあるURLファイルも削除される。リンク切れチェック機能もつけよう。
 となると、各項目には、URL・URLファイルのパス・リンク切れか否か、というデータを入れておけば何かと便利だ。だが、これらの異なるデータを直接入れておけるプロパティが「TTreeNode/TListItem」にはない。そこで、何でも入るポインタ型のDataプロパティが登場する。

  1. レコード型を定義する
  2.  まず、これら3つのデータをDataプロパティ1つに収めるために、レコード型を作って1つのレコードにまとめておく。
    type
      TFavData = record
        URL: string;        //お気に入りのURL
        FileName: string;   //URLファイルのフルパス
        Linked: boolean;     //リンク切れか否か
      end;
    

  3. レコードのポインタ型を定義する
  4.  次に、このレコード型をポインタとして扱えるようにレコードのポインタ型を定義する。
    type
      PFavData = ^TFavData;  //TFavDataのポインタ型
      TFavData = record
        URL: string;        //お気に入りのURL
        FileName: string;   //URLファイルのフルパス
        Linked: boolean;     //リンク切れか否か
      end;
    
     「PFavData = ^TFavData;」の個所が追加されている。定義した「T○○」型の先頭に山印「^」を付けることで、その型のポインタ型を定義することができる。

  5. メモリを確保する
  6.  ポインタを使ってデータを入れる場合、データを入れる先のメモリ領域を確保して、そのアドレスを取得しなければならない。直接にポインタを使わない場合はDelphiが裏で自動的にやってくれるが、ポインタを直に使う場合は手動で行う必要がある。
    var
      pFav: PFavData;  //ポインタ型
    begin
      New(pFav);  //メモリ領域を確保
    
     これで必要なメモリ領域が確保され、pFavにはそこのアドレスが入れられた。

  7. Dataプロパティに代入する
  8.  では、Dataプロパティに値を代入しよう。とはいっても、実際のデータを代入するわけではない。あくまでもポインタなわけだから、アドレスを代入しているという意識を忘れずに。
    procedure TForm1.Button1Click(Sender: TObject);
    var
      pFav: PFavData;  //ポインタ型を宣言
      ListItem: TListItem;
    begin
      New(pFav);  //メモリ領域を確保
    
      //レコードに値を代入
      pFav^.URL := 'http://www.borland.co.jp/';
      pFav^.FileName := 'C:\Windows\Favorites\Borland Japan.url';
      pFav^.Linked := False;
    
      //ListItem作成
      ListItem := ListView1.Items.Add;
      ListItem.Caption := ChangeFileExt(ExtractFileName(pFav^.FileName), '');
    
      //Dataプロパティにレコード(のアドレス)を代入
      ListItem.Data := Pointer(pFav);
    end;
    
     pFavレコードに値を代入するとき、後ろに山形マーク「^」が付いている。先ほどは型の先頭につけてポインタ型を定義していた(PFavData = ^TFavData;)。今度はポインタ型の後ろに付けることで、レコード型(TFavData)のデータを表すようになる。これを「逆参照」という。
     pFavはあくまでもポインタでありアドレスしか入っていないので、値を代入するときは実際のデータを持ってくる必要がある。それを行うのがポインタの逆参照だ。このように、ポインタから実データを引っ張り出してくる記述は山形の記号1つだけで済む。「記号なしは単なるアドレス」「記号ありは実データ」という区別をしっかり意識しよう。

     一番最後のListItem.Dataへの代入は、このプロパティがPointer型なのでPointerで型キャストして代入する。Pointer型は最も汎用的なポインタ型であり、どのような型に限らず(アドレスを)代入できる。

  9. Dataプロパティから取り出す
  10.  値を取り出す場合は、Dataを逆参照して実データを持ってきて、それを型キャストする。
    procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
    begin
     Label1.Caption := TFavData(Item.Data^).FileName;
    end;
    
     「Item.Data^」で逆参照して実データを取り出す。しかし、ポインタは単なるアドレスだから入っているデータの型までは教えてくれない。そこでTFavDataで型キャストしてからFileNameを取り出す。

  11. メモリを解放する
  12.  先ほどは手動でメモリを確保したが(New(pFav);)、データが不要になった場合はメモリの領域もいらなくなるので解放して場所を明け渡さなければならない。これを忘れるとアプリケーションを終了させてもずっと領域を確保しっぱなしになり、何回も起動・終了させると確保した領域がどんどん増えていき、空きメモリが減っていく。これを「メモリ リーク」という。
     通常の変数ならばDelphiが全部自動で解放してくれるが、手動で確保したメモリは手動で解放しなければならない。ちなみに、手動で確保したメモリ領域を自動で開放する仕組みを「ガーベージ コレクション」という。Javaや.NET Frameworkでサポートされているが、Delphiにはない。

     ListViewの項目を削除した場合にデータもいらなくなるので、ここでメモリを解放する。

    procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
    begin
     Dispose(Item.Data);
    end;
    
     Newで確保したメモリを解放するには、Disposeを呼び出すだけだ。「New」したら「Dispose」するのを忘れずに。





3.実践その2 <TList.Items>

 Delphiユーザーならば、一度はTMemo・TStringList・TListViewなどを扱ったことがあるだろう。どれもデータをリストとして扱える機能を持っていて、項目の追加・削除などが簡単にできる。ここで、自前のデータをリストとして扱うにはどうすればいいのだろうか。それにはTListを使用する。TListが保持する個々の項目は、ポインタである。そのため、データの型を問わずになんでもリスト管理することができる。
 例えば、自前のクラスをTListでリスト管理する方法を紹介しよう。構想としては、「TMyList.Items.Add;」「TMyList.Items[0].Text := 'Delphi';」とできるようにしたい。
 となると、「TMyList」「TMyItems」「TMyItem」の3クラス必要になる。

  1. TMyItemの作成
  2.  まずは「TMyItem」を考える。このクラスはリスト管理したいデータの本体にあたる。例では文字列を保持する「Text」プロパティを作成した。
    type
      TMyItem = class(TObject)
      private
        FOwner: TMyItems;
        FText: string;
        function GetIndex: integer;
      public
        procedure Delete;
        property Index: integer read GetIndex;
        property Text: string read FText write FText;  //任意のプロパティ
      end;
    
     ここで、TMyItemを削除するための「Delete」メソッド、TMyItemのインデックスを表す「Index」プロパティを導入した。

  3. TMyItemsの作成
  4.  「TMyItems」は、複数の「TMyItem」を保持するリストであり、追加・削除などのリスト操作ができるようにする。位置付けとしてはTStringListと同じような機能を持つ。
    type
      TMyItems = class(TObject)
      private
        FList: TList;
        function GetCount: integer;
        function GetItem(Value: integer): TMyItem;
        procedure SetItem(Index: integer; Value: TMyItem);
      public
        constructor Create;
        destructor Destroy; override;
        function Add: integer;
        function AddItem: TMyItem;
        procedure Clear;
        procedure Delete(Index: integer);
        property Count: integer read GetCount;
        property Item[Index: integer]: TMyItem read GetItem write SetItem; default;
      end;
    
     ここで、private宣言した「FList: TList;」に注目して欲しい。ここにTMyItemをリストで保持する。キモになる部分だ。

  5. TListへの代入
  6.  ソース全体を紹介するのは後回しにして、TListへの代入方法を先に紹介する。「TMyItems.Add」メソッドを実行するとFListへ項目を追加できるようにしよう。
    function TMyItems.Add: integer;
    var
     MyItem: TMyItem;
    begin
     MyItem := TMyItem.Create;
     Result := FList.Add(Pointer(MyItem));
    end;
    
     TListで個々に保持するリストはPointer型なので、FList.AddするときにMyItemをPointer型でキャストする。ここで、MyItemはTMyItem型であり、PMyItemつまりTMyItemのポインタ型である必要はない。なぜなら、クラスはDelphi内部ではポインタとして扱われているからだ。TMyItem型自体がすでにポインタ型なのである。Delphiは単にそれを表面に見せていないだけだ。

     TMyItem.Createで必要なメモリ領域も確保されるため、別途「New」で確保する必要はない。

  7. TListからの取り出し
  8.  FListからデータを取り出すために「GetItem」メソッドを実行する。
    function TMyItems.GetItem(Index: integer): TMyItem;
    begin
     if Index >= 0 then
      Pointer(Result) := FList[Index]
     else
      Result := nil;
    end;
    
     「FList[Index]」は、リスト項目のポインタだ。「Pointer(Result)」で、Resultのポインタを表すようにする。これで両方ともPointer型になったので、めでたくアドレスを代入できる。ResultのポインタとFList[Index]のポインタは同じアドレスを指すようになるため、両者のデータも同じものとなる。ResultがTMyItem型なので、キャストされて値が返る。

  9. TMyListの作成
  10.  TMyItemsを保持する「TMyList」を作成する。例えるならプロパティにリストを持っている(TMemo.Lines・TListView.Items)、「TMemo」や「TListView」のようなものだ。
      TMyList = class(TObject)
      private
        FItems: TMyItems;
      public
        constructor Create;
        destructor Destroy; override;
        property Items: TMyItems read FItems write FItems;
      end;
    
     ItemsプロパティがTMyItems型になっているので、「Items.Add」のようにできる。また、TMyItemsを定義した「property Item[Index: integer]: TMyItem」のところを見て欲しい。一番最後に「default」指令が付いている。これが付いているから本来の「Items.Item[0].Text」の他に「Items[0].Text」と簡略化してもプロパティを参照できるのだ。

  11. 全ソースの掲載
  12.  TMyListクラスの雛型を掲載する。使用する場合は、そのままコピー貼り付けしてから、クラス名の「TMy」を好きなクラス名で一括置換すればよい。最低限の機能しか持っていないため、必要に応じて改良して欲しい。
    unit MyListUnit;
    
    interface
    
    uses
      Windows, SysUtils, Classes;
    
    type
      TMyItems = class;
    
      TMyItem = class(TObject)
      private
        FOwner: TMyItems;
        FText: string;
        function GetIndex: integer;
      public
        procedure Delete;
        property Index: integer read GetIndex;
        property Text: string read FText write FText;  //任意のプロパティ
      end;
    
      TMyItems = class(TObject)
      private
        FList: TList;
        procedure FreeAllItem;
        function GetCount: integer;
        function GetItem(Index: integer): TMyItem;
        procedure SetItem(Index: integer; Value: TMyItem);
      public
        constructor Create;
        destructor Destroy; override;
        function Add: integer;
        function AddItem: TMyItem;
        procedure Clear;
        procedure Delete(Index: integer);
        property Count: integer read GetCount;
        property Item[Index: integer]: TMyItem read GetItem write SetItem; default;
      end;
      
      TMyList = class(TObject)
      private
        FItems: TMyItems;
      public
        constructor Create;
        destructor Destroy; override;
        property Items: TMyItems read FItems write FItems;
      end;
    
    
    implementation
    
    
    //----------------------------------------------------------------------------
    //  TMyItem
    //----------------------------------------------------------------------------
    function TMyItem.GetIndex: integer;
    begin
      Result := FOwner.FList.IndexOf(Self);
    end;
    
    procedure TMyItem.Delete;
    begin
      FOwner.Delete(Index);
    end;
    
    
    //----------------------------------------------------------------------------
    //  TMyItems
    //----------------------------------------------------------------------------
    constructor TMyItems.Create;
    begin
     FList := TList.Create;
    end;
    
    destructor TMyItems.Destroy;
    begin
     FreeAllItem;
     FList.Free;
    end;
    
    procedure TMyItems.FreeAllItem;
    var
     i: integer;
    begin
     for i := 0 to FList.Count - 1 do
      Item[i].Free;  //TMyItemを解放
    end;
    
    function TMyItems.Add: integer;
    var
     MyItem: TMyItem;
    begin
     MyItem := TMyItem.Create;
     MyItem.FOwner := Self;
     Result := FList.Add(Pointer(MyItem));
    end;
    
    function TMyItems.AddItem: TMyItem;
    var
     Index: integer;
    begin
     Index := Add;
     Result := Item[Index];
    end;
    
    procedure TMyItems.Clear;
    begin
     FreeAllItem;
     FList.Clear;
    end;
    
    procedure TMyItems.Delete(Index: integer);
    begin
     if Index >= 0 then
     begin
      Item[Index].Free;
      FList.Delete(Index);
      FList.Capacity := FList.Capacity - 1;
     end;
    end;
    
    function TMyItems.GetCount: integer;
    begin
     Result := FList.Count;
    end;
    
    function TMyItems.GetItem(Index: integer): TMyItem;
    begin
     if Index >= 0 then
      Pointer(Result) := FList[Index]
     else
      Result := nil;
    end;
    
    procedure TMyItems.SetItem(Index: integer; Value: TMyItem);
    begin
     if (Index >= 0) and Assigned(Value) then
      FList[Index] := Pointer(Value);
    end;
    
    
    //----------------------------------------------------------------------------
    //  TMyList
    //----------------------------------------------------------------------------
    constructor TMyList.Create;
    begin
     Items := TMyItems.Create;
    end;
    
    destructor TMyList.Destroy;
    begin
     Items.Free;
    end;
    
    end.
    

  13. TMyListの使い方
  14. //例1:TMyListのCreateとTMyItemの作成
    var
      MyList: TMyList;
    
    procedure Test01;
    var
      MyItem: TMyItem;
    begin
      MyList := TMyList.Create;
      MyItem := MyList.Items.AddItem;
      MyItem.Text := 'aiueo';
    end;
    
    //例2:TMyItemsの操作
    procedure Test02;
    var
      i: integer;
    begin
      for i := 0 to MyList.Items.Count - 1 do
        if MyList.Items[i].Text = 'aiueo' then
          MyList.Items[i].Delete;
    end;
    
    //例3:TMyListの解放
    procedure TForm1.FormCloseQuery(Sender: TObject;
      var CanClose: Boolean);
    begin
      MyList.Free;
    end;
    




4.実践その3 <TComponent.Tag>

 コンポーネントにはすべてTagプロパティが付いている。特定の用途が決まっているわけではなく、数値の入れ物として自由に使ってください、というプロパティだ。ここはinteger型なので数値しか入れることはできない。しかし、ポインタを使えばどんな型のデータでも入れられるのだ。

 ポインタは、メモリのアドレスを保持しているが、実はアドレスは数値だ。だからアドレスをintegerに型キャストしてTagプロパティへ代入すれば、結果としてどんなデータでも保持できるようになる。裏技のように思えるだろうが、Tagプロパティのヘルプに堂々と掲載された使い方である。

TListView.Columns.Item.Tagプロパティについて

 TListViewの行項目にはTListItem.Dataプロパティがあるが、カラムの列項目であるTListColumnにはDataプロパティがない。その代わりにTagプロパティがあるので、上記の方法を使えば、レコードなどをカラムに入れておくことが出来る。


Copyright © 2003-2004 H'Imagine.
All rights reserved.