<目次>
しかし、Delphiに精通してくると、ポインタを使ったほうが便利になる場面が登場することがある。以下では、ポインタに関する概要を説明した後、実際にポインタを使うと便利な場面を数例紹介する。なお、「ポインタというもの」に関して理論的に知りたい方は、ここを読んでも勉強にならないので他のサイトへ行くことをお勧めする。
さて、データはメモリの中に入っているが、いったいメモリのどこに入っているかを把握する必要がある。そうしないと目的のデータを取り出せない。そのためには、「あなたが必要なデータはメモリのここに入ってますよ」という目印があればよい。このメモリにある目印を「アドレス」という。
他にもメリットはあるが、片手で足りる程度しかない。Delphiではプログラマがポインタを意識しなくてもいいように作ってあるため、「ポインタ使えなきゃDelphiユーザー失格」ということはない。知ればより便利になるだけだ。
一番最後のListItem.Dataへの代入は、このプロパティがPointer型なのでPointerで型キャストして代入する。Pointer型は最も汎用的なポインタ型であり、どのような型に限らず(アドレスを)代入できる。
ListViewの項目を削除した場合にデータもいらなくなるので、ここでメモリを解放する。
TMyItem.Createで必要なメモリ領域も確保されるため、別途「New」で確保する必要はない。
ポインタは、メモリのアドレスを保持しているが、実はアドレスは数値だ。だからアドレスをintegerに型キャストしてTagプロパティへ代入すれば、結果としてどんなデータでも保持できるようになる。裏技のように思えるだろうが、Tagプロパティのヘルプに堂々と掲載された使い方である。
文字列の取り出しは、下記のようにすることもできる。
procedure TForm1.Button5Click(Sender: TObject);
var
pD: PDouble;
begin
New(pD);
pD^ := 3.14;
Form1.Tag := integer(pD);
end;
procedure TForm1.Button6Click(Sender: TObject);
begin
Label1.Caption := FormatFloat('0.00', Double(Pointer(Form1.Tag)^));
Dispose(Pointer(Form1.Tag));
end;
通常なら型エラーではじかれる実数型もポインタを使えば入れることができる。
1.Delphiとポインタの関係
プログラミングもある程度経験を積むと、「ポインタ」という用語を目にすることになるだろう。ただ、Delphiでポインタを積極的に使う意義は低い。Delphiは、ポインタをなるべくプログラマに見せないよう、隠蔽して内側で(例えばVCLソース内で)こっそり使っているからだ。そのため、ユーザーはポインタの存在など気にせずにプログラムを組むことができる。
1-1.データのありか
ポインタの話をする前に、コンピュータで扱うデータはどこに入っているのか、について知る必要がある。「それはハードディスクだ」との答えもあるだろうが、プログラムの世界では、「それはメモリだ」と答えなければならない。CPUとデータのやり取りをするのはメモリであり、ハードディスクは単にデータの巨大な貯蔵庫に過ぎない。だからこそ、大量のデータを扱う場合に「メモリが足りない」という事態が生ずるわけだ。
1-2.ポインタはデータのありかを指す
このアドレスを入れておく変数・定数が「ポインタ」だ。ポインタは、メモリにある目印(アドレス)を保持するために使用する。つまり、ポインタという入れ物にアドレスという目印を入れておくわけだ。
ここで大切な注意。ポインタ自体に入っているものは、単なるアドレスである(16進数の数値)。データは入っていない。データが欲しい場合は、「Delphiさん、このポインタのアドレスにあるデータくださいな」とプログラムする必要がある(実際の記述は記号1つで済むが)。このような仕組みによって、メモリ上のアドレスさえ分かれば、そこに入っているデータを取り出すことができる。
1-3.Delphiは内部でよきにはからっている
変数・定数を扱う場合、本来ならばポインタを使ってメモリのアドレスを指定し、そこへデータの出し入れをするのだが、上の説明を読んだだけでもなんだかめんどくさそうだ。Delphiは、この面倒な処理を内部で引き受け、ユーザーは「メモリのアドレスがどうのこうの…」と意識することなく、気軽に変数・定数を扱えるようになっている(変数を宣言して値を代入するだけでOKですよね)。
1-4.ポインタの便利な点
そんなポインタの便利な点とは、どんな種類のデータでも扱えることだ。通常ならば、変数は型が決まっているため、integer型の変数に文字列を入れることはできない。これは、データの種類が違うからだ。しかし、ポインタに入っているものは単なるアドレス(数値)である。データではない。「アドレス001には文字列」「アドレス005にはレコード型」が入っていたとしても、ポインタに入るのはあくまで「001」「005」というアドレスであってデータではない。そのため、ポインタはどんな種類のデータでも扱える魔法の箱になるわけだ。ポインタから実際のデータを取り出すときに型チェックすればよい。
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プロパティが登場する。
type
TFavData = record
URL: string; //お気に入りのURL
FileName: string; //URLファイルのフルパス
Linked: boolean; //リンク切れか否か
end;
type
PFavData = ^TFavData; //TFavDataのポインタ型
TFavData = record
URL: string; //お気に入りのURL
FileName: string; //URLファイルのフルパス
Linked: boolean; //リンク切れか否か
end;
「PFavData = ^TFavData;」の個所が追加されている。定義した「T○○」型の先頭に山印「^」を付けることで、その型のポインタ型を定義することができる。
var
pFav: PFavData; //ポインタ型
begin
New(pFav); //メモリ領域を確保
これで必要なメモリ領域が確保され、pFavにはそこのアドレスが入れられた。
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つだけで済む。「記号なしは単なるアドレス」「記号ありは実データ」という区別をしっかり意識しよう。
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
Selected: Boolean);
begin
Label1.Caption := TFavData(Item.Data^).FileName;
end;
「Item.Data^」で逆参照して実データを取り出す。しかし、ポインタは単なるアドレスだから入っているデータの型までは教えてくれない。そこでTFavDataで型キャストしてからFileNameを取り出す。
通常の変数ならばDelphiが全部自動で解放してくれるが、手動で確保したメモリは手動で解放しなければならない。ちなみに、手動で確保したメモリ領域を自動で開放する仕組みを「ガーベージ コレクション」という。Javaや.NET Frameworkでサポートされているが、Delphiにはない。
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
Dispose(Item.Data);
end;
Newで確保したメモリを解放するには、Disposeを呼び出すだけだ。「New」したら「Dispose」するのを忘れずに。
TStringListなど既存のクラスや、自前のクラスを入れることもできる。クラス変数はDelphi内部ではポインタとして扱われている。そのため、Dataとのやりとりはレコードよりもスムースに行える。
procedure TForm1.Button1Click(Sender: TObject);
var
MyClass: TMyClass;
begin
MyClass := TMyClass.Create;
ListItem := ListView1.Items.Add;
ListItem.Data := Pointer(MyClass);
end;
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
Selected: Boolean);
begin
with TMyClass(Item.Data) do
begin
//いろいろ
end;
end;
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
TMyClass(Item.Data).Free;
end;
クラスの場合は、NewではなくCreateしてメモリの確保が行われる。Dataに入れる場合も、そのままPointerでキャストするだけだ。
Dataを使用する際は、クラス名でDataをキャストする。クラスはポインタなので、Dataを逆参照する必要はない。そのままキャストする。
Createしたので、最後にFreeで解放するのを忘れずに。
文字列を1つだけ入れておけばいい場合は、以下のようにする。
procedure TForm1.Button1Click(Sender: TObject);
var
pFileName: PString;
ListItem: TListItem;
begin
New(pFileName);
pFileName^ := 'C:\Windows\Favorites\Borland Japan.url';
ListItem := ListView1.Items.Add;
ListItem.Caption := ChangeFileExt(ExtractFileName(pFileName^), '');
ListItem.Data := Pointer(pFileName);
end;
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
Selected: Boolean);
begin
Label1.Caption := string(Item.Data^);
end;
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
Dispose(Item.Data);
end;
stringのポインタ型「PString」を使うところが重要だ。
3.実践その2 <TList.Items>
Delphiユーザーならば、一度はTMemo・TStringList・TListViewなどを扱ったことがあるだろう。どれもデータをリストとして扱える機能を持っていて、項目の追加・削除などが簡単にできる。ここで、自前のデータをリストとして扱うにはどうすればいいのだろうか。それにはTListを使用する。TListが保持する個々の項目は、ポインタである。そのため、データの型を問わずになんでもリスト管理することができる。
例えば、自前のクラスをTListでリスト管理する方法を紹介しよう。構想としては、「TMyList.Items.Add;」「TMyList.Items[0].Text := 'Delphi';」とできるようにしたい。
となると、「TMyList」「TMyItems」「TMyItem」の3クラス必要になる。
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」プロパティを導入した。
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をリストで保持する。キモになる部分だ。
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は単にそれを表面に見せていないだけだ。
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型なので、キャストされて値が返る。
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」と簡略化してもプロパティを参照できるのだ。
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.
//例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型なので数値しか入れることはできない。しかし、ポインタを使えばどんな型のデータでも入れられるのだ。
type
PMyData = ^TMyData; //TMyDataのポインタ型
TMyData = record
Field1: string;
Field2: integer;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
pMy: PMyData;
begin
New(pMy); //メモリ確保
pMy^.Field1 := 'Delphi';
pMy^.Field2 := 6;
Form1.Tag := integer(pMy); //Tagにポインタが指すアドレスを代入
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
Label1.Caption := TMyData(Pointer(Form1.Tag)^).Field1; //TagをTMyDataにキャスト
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Dispose(Pointer(Form1.Tag)); //メモリ解放
end;
データを入れる場合は、New(pMy);でデータを入れるメモリ領域を確保して、そのアドレスを得る。そうしたら、ポインタをintegerでキャストしてTagへ代入する。
データを取り出す場合は、TagをPointer型にキャストして逆参照し、TMyDataでキャストして値を得る。
Newでメモリを確保したので、OnDestroyイベントで必ずDisposeしてメモリを解放する。
procedure TForm1.Button3Click(Sender: TObject);
var
Timer: TTimer;
begin
Timer := TTimer.Create(Self); //TTimer作成
Timer.Enabled := False;
Timer.OnTimer := MyTimer;
Form1.Tag := integer(Timer); //TagにTimerのポインタを代入
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
TTimer(Pointer(Form1.Tag)).Enabled := True; //TagをTTimerにキャスト
end;
procedure TForm1.MyTimer(Sender: TObject);
begin
Label1.Caption := DateTimeToStr(Now);
end;
例としてTTimerを作成してTagへ入れてみた。実は、コンポーネントはそれ自体がポインタである。そのため、コンポーネント変数は、ポインタと同様に扱うことができる。
コンポーネントを入れる場合は、コンポーネントをCreateする。同時にメモリが確保されるため、別途Newする必要はない。コンポーネントはポインタなので、直接integerでキャストしてTagへ入れる。
コンポーネントを取り出す場合は、TagをPointer型でキャストする。コンポーネントはポインタなので、逆参照の必要はなく、そのままTTimerでキャストすればアクセスできるようになる。
メモリの解放は、フォームがしてくれるので(CreateしたときのAOwnerをフォームにした場合)、特に何も記述しなくてよい。
procedure TForm1.Button3Click(Sender: TObject);
var
pS: PString;
begin
New(pS);
pS^ := 'Delphi';
Form1.Tag := integer(Pointer(pS));
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
Label1.Caption := string(Pointer(Form1.Tag)^);
Dispose(Pointer(Form1.Tag));
end;
stringのポインタ型である「PString」を使って操作すればよい。
procedure TForm1.Button4Click(Sender: TObject);
var
S2: string;
begin
Pointer(S2) := Pointer(Form1.Tag);
Label1.Caption := S2;
Dispose(Pointer(Form1.Tag));
end;
S2のポインタとForm1.Tagのポインタが同じアドレスを指すようになるため、取り出したときのデータも同じものになるというわけだ。
TListView.Columns.Item.Tagプロパティについて TListViewの行項目にはTListItem.Dataプロパティがあるが、カラムの列項目であるTListColumnにはDataプロパティがない。その代わりにTagプロパティがあるので、上記の方法を使えば、レコードなどをカラムに入れておくことが出来る。 |