TOPページへ戻る  PCページへ戻る  一覧へ戻る

数値を配列として使いたい - - リストの場合

データを読み込んで計算する場合など、毎回データ数が異なる個数の数値を読み込む ことが多々あります。そんなときは動的配列やリストを使って処理をします。 ここではTListの例を示します。

DelphiにはTListというオブジェクトがあり、そのItemsプロパティに 数値を入れることで配列として使用できます。恐らく、Itemsプロパティの最大数は Integerの最大数(2147483647)なのでかなり大きな値となります。

※整数型変数は符号付き32ビットです。そのため、2^32個の 数値を表すことができ、具体的には-2147483648〜2147483647までの整数になります。 プラス側の最大値がマイナスより1小さいのは、0が含まれるためです。即ち、-2147483648〜-1 で2147483648個、0〜2147483647で2147483648個、計4294967296(=2^32)個となります。

また、TListオブジェクトを使うと、SortメソッドなどのTListオブジェクト が持つメソッドを使うことができます。しかし、Itemsプロパティに代入できるのは ポインタだけです。@演算子で変数のポインタを渡すコードの例を下に示します。 このコードは、動的配列の場合と同様、 フォーム上にLabel1とButton1が配置された簡単なもので、 Button1を押すと1〜65535までの和をLabel1に表示します。 しかし、このように@演算子を使った場合、正しい結果が得られません。


unit Unit1;

 interface

 uses
   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
   Dialogs, StdCtrls;

 type
   TForm1 = class(TForm)
     Label1: TLabel;
     Button1: TButton;
     procedure Button1Click(Sender: TObject);
   private
     { Private 宣言 }
   public
     { Public 宣言 }
   end;

 var
   Form1: TForm1;

 implementation

 {$R *.dfm}

 procedure TForm1.Button1Click(Sender: TObject);
 var List: TList;		// リスト変数の宣言
     i,Answer : Integer;	// カウンタiと答えAnswer
 begin
 List := TList.Create;		// Listオブジェクトの生成

 i := 0;
 try
 while i < 65535 do
   begin
   i := i + 1;
   List.Add(@i);		// AddメソッドでList変数に追加←問題の箇所
   end;

 i := 0;
 Answer := 0;
 while i < 65535 do
   begin
   Answer := Answer + Integer(List.Items[i]^);	// ポインタの逆参照
   i := i + 1;
   end;
 Label1.Caption := IntToStr(Answer);

 finally
   List.Free;			// CreatしたオブジェクトはFreeで解放
  end;

 end;

 end.

実行結果(左:ボタンを押す前、右:押した後)

 


動的配列の場合に比べると、try 〜 finally 〜 end; という文が追加されています。手続きや関数中でエラーが発生すると、それ以降のコードは実行 されなくなってしまいますが、try 〜 finally文を使うと、try 〜 finally間に有るコードで 異常が発生ても必ず※1finally 〜 end部を実行してくれます(エラーが発生しない 時でも必ず実行されます。)。要するに確実にListを破棄(Free) することができます。try 〜 finallyを使わずに、List.FreeをLabel1.Caption := 〜文の 後ろに書いた場合、Button1Click手続き中でエラーが出るとListは破棄されない※2ことに なり、メモリリークを引き起こします。

※1 finally 〜 end;中にエラーが発生しないよう注意してください。 この場合のエラーは処理されないままになります。このような場合を想定してか、finally文中でも try 〜 finally 〜 end;を使う(いれこにする)ことができます。

※2 実際にはアプリケーションを終了した時点で破棄されるようです。 ”ならOK”と思うかも知れませんが、アプリケーションを起動したまま何回も処理を行うと、 徐々にメモリが減って最後はリソース不足になってしまいます。

さて、結果を見ていましょう。 1〜65535までを足した際の正解は、(65535+1)*65535/2=2147450880です。 ところが上の結果は2147385345になっています。2147385345=2147450880-65535なので、 最後の数値が足されていないことを意味します。

この原因は、 @ i でList変数に代入したのが変数 i が保持している メモリの先頭アドレス(=ポインタ)でしかないのでためです。そう、変数の値そのものでは ないのです。そのため、変数の値が変わるとList.Items[ i ]の中身(=実際の値)も変わって しまっているのです

具体的に言えば、最初のwhile文ではポインタの中身は1 〜 65535になって います。しかし、2回目のwhile文ではiに0〜65534が代入されますのでList[i]^の値も0〜65534 になっているのです。そのため最後の数65535が足し算されていない現象が起きたのです。

たとえて言うならば、Listに代入されたメモリのアドレスは”友達の住所”、 メモリにある実際の値は”そこに住む友達の名前”です。@演算子を使いList.add[ @ i ]で リストに追加することは、”友達の住所”は覚えているが、誰が住んでいたかは知らないと いうことになります。

上述のような不具合を避けるには、 変数 i とは別のメモリ領域を確保して そこに i の値を入れておく必要があります。いわば、”友達の住所と 名前をメモしておく”ことです。下にコードの例(procedure部のみ)を示します。


procedure TForm1.Button1Click(Sender: TObject);
 var List: TList;		// リスト変数の宣言
     i,Answer : Integer;	// カウンタiと答えAnswer
     pIn: PInteger;		// 整数型ポインタ変数の宣言
 begin
 List := TList.Create;

 i := 0;
 try
 while i < 65535 do
   begin
   try				// メモリ不足に備える
   New(pIn);			// pIn用に別途メモリを確保
   i := i + 1;
   pIn^ := i;			// pInのメモリ領域に値を代入
   List.Add(pIn);
   except

 i := 0;
 Answer := 0;
 while i < 65535 do
   begin
   Answer := Answer + Integer(List.Items[i]^);	// ポインタの逆参照
   i := i + 1;
   end;
 Label1.Caption := IntToStr(Answer);

 finally
   begin
   i := 0;
   while i < List.Count do		// New()したポインタはDisposeで解放
     begin
     if Assigned(List.Items[i]) then Dispose(List.Items[i]);
     i := i + 1;
     end;
   List.Free;		// CreatしたオブジェクトはFreeで解放
    on E:EOutOfMemory do	// メモリ不足が発生した際の処理
	begin
	ShowMessage('メモリ不足です');
	exit;			// Button1Click手続きを終了しfinally節へ移行
	end;
    end;
   end;
   end;
  end;

 end;

実行結果(左:ボタンを押す前、右:押した後)

 


簡単な説明

変数 i とは別にメモリを確保するには、New()手続きで行います。 ただNew()手続きを実行しても、どこに新たなメモリが確保されたか分かりません。 そのため、pIntというポインタ変数を用いてNew(pInt)とすることで、pIntに 新たに確保したメモリの先頭アドレスが代入されます。また、新たに確保されたメモリには 整数型のデータがはいるので、pInt変数は”整数型のポインタ変数(PInteger)”と 宣言しています。 ちなみに、PIntegerの代わりに^Integerと書くこともできます。また、New()手続きは メモリ不足のエラー(EOutOfMemory)を引き起こす可能性があるため、try 〜 except end; でそのエラーを感知&処理します。except on 〜 end;中でexitを呼び出すとfinally 〜 end; へ処理が移行します。

変数pIntは、”pInt: Pointer;”のように型を付けずに宣言することも可能です。しかし、 この場合、メモリ領域に値を代入(pInt^ := i;)することができません。これは、型が分からない ので、ポインタのアドレスから何ビットのデータを使って良いか分からないからです。

メモリの確保にはNew()手続きを使いますが、New()手続きで 確保したメモリ領域は自動で開放してくれません。そのため、Dispose手続きで メモリを解放する必要があります。これもList.Free同様メモリリークを避けるために、 finally 〜 end;中で行うべきです。

pIntのメモリを確保したら、そのメモリ領域に値を代入します。 ^演算子をポインタ変数の後ろに付けるとメモリ領域に値を代入することができます。

TOPページへ戻る  PCページへ戻る  一覧へ戻る