第17章 メモリ操作

Win32 のアプリケーション(プロセス)は、その各々が32ビット(4ギガバイト)の仮想メモリ空間を独立に持っている。このことは、ハードウェアとしての物理メモリがどれだけ搭載されているかということとは無関係である。実際、実行に際して仮想メモリ空間におけるアドレスが物理メモリのどのアドレスにマップされるかということはプログラマが心配することではないし、プログラムコードで制御できることではない。これはOSの仕事である。 どんなプログラムでも、実行されるときは物理メモリにマップされ、実行中には、グローバル変数や大きなローカル変数、長い文字列、オブジェクトインスタンスなどの生成、廃棄が頻繁に発生するので、メモリを動的に使用していることになる。プログラムコードで明示的にメモリを操作しなくても、コンパイラによって生成されたコードがそのような操作をしているのだ。しかし、APIを含むDLLから大きなレコードや文字列を受け取る場合などのように、コードで明示的にメモリの割り当てや解放を行なわなければならない場合もある。クリップボードやDDEなどのプロセス間通信では、メモリの動的割り当て操作は必須である。この章では、Win32 のメモリ操作について簡単に調べることにしよう。

17−0 この章の内容

第17章 メモリ操作
17−1 仮想アドレス空間
17−2 プロセスヒープの操作
17−3 ヒープ関数
17−4 動的ヒープの作成
17−5 ポインタを使ったメモリアクセス
17−6 まとめ


| arrw18c.gif 前の章へ | 総目次へ | 次の章へ arrw18d.gif

17−1 仮想アドレス空間

Win32 は、各々のプロセスに4ギガバイトのメモリの仮想アドレス空間を提供しているが、Windows 95/98ではこのうち上位2ギガバイトはOSシステムが使用し、下位2ギガバイトをアプリケーションが使用する。最下位の4メガバイトは、MS−DOS互換性のための予約されているので、正確に言えば4メガバイト($00400000) から2ギガバイト ($7FFFFFFF)までを使用できる。最近発売されたパソコンでも、RAMの容量は通常32メガバイトから64メガバイト程度であるから、実行中のすべてのプロセスのすべてのアドレス空間を同時に物理メモリにマップすることはできない。ではあふれた分は、どこにいるのだろうか? 承知のように通称スワップファイルと呼ばれている、ハードディスクの中に、実行するのを待って一時待避させられているのだ。このように、物理メモリからあふれる(ページフォールト)ことがあると、その時点で優先度が低いプロセスの一部はページメモリと呼ばれるディスクファイルに待避させられる。ページメモリと物理メモリのあいだのやりとりには「ページ」という単位でマッピングが行なわれる。インテルのX86プロセッサの Win32では、ページは4キロバイトである。これは、それぞれの環境下で次のAPIを使って確かめることができる。

procedure GetSystemInfo(var lpSystemInfo: TSystemInfo); stdcall;

この関数のパラメータは、Windows.pas に以下のように定義されている。

  PSystemInfo = ^TSystemInfo;
  TSystemInfo = record
    case Integer of
      0: (
        dwOemId: DWORD);
      1: (
        wProcessorArchitecture: Word; 
        wReserved: Word; 
        dwPageSize: DWORD;
        lpMinimumApplicationAddress: Pointer;
        lpMaximumApplicationAddress: Pointer;
        dwActiveProcessorMask: DWORD;
        dwNumberOfProcessors: DWORD;
        dwProcessorType: DWORD;
        dwAllocationGranularity: DWORD;
        wProcessorLevel: Word;
        wProcessorRevision: Word);
  end;

Win32.hlp を見るとわかるように GetSystemInfo( ) が返すこのレコードの多くのフィールドは、PROCESSOR_ARCHITECTURE_INTEL のようなシンボル定数が定義されているが、Windows.pas にはこれらの定数が定義されていないのですぐには解析できない。それでも上記のページサイズとアプリケーションがつかえるアドレス範囲は読み取ることができる。このことを確かめよう。

  1. エクスプローラを使って、新しいフォルダを 1701PageSize という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 1606FontLabel フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで PageSize.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

以下に PageSize.dpr の全リストを示す。

program PageSize;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  L1: TAPILabel;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeInitMenu(hWindow);

  L1 := CreateLabel(hWindow,10,10,340,80,'');


  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 100;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
var
  SI :TSystemInfo;
begin
  GetSystemInfo(SI);
  L1.Text := 'PageSize = '+AIntToStr(SI.dwPageSize)+#13+
     'MinAddress = $'+AIntToHex(integer(SI.lpMinimumApplicationAddress),8)+#13+
     'MaxAddress = $'+AIntToHex(integer(SI.lpMaximumApplicationAddress),8);

end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifPageSize.zip(16.501kB)
PageSize.gif

ここでは、ラベルを一つつくって OnTest( ) ハンドラで GetSystemInfo( ) を実行しているだけだ。実行時のイメージを右に示した。これは、筆者の Windows95 (PentiumII 233) の環境のものである。

redball.gifこの章の目次へ戻る

17−2 プロセスヒープの操作

アプリケーションが使える約2ギガバイトの仮想メモリ空間のうち、Win32APIレベルでは、操作しやすいようにいくつかのメモリのかたまりとしてヒープ (Heap) という単位を使っている。この heap の意味を辞書をつかって調べると、文字どおり「かたまり」のことである。すべてのプロセスには、暗黙のうちにひとつのヒープがシステムにより割り当てられている。これをプロセスヒープという。これまでの章で、APIから文字列を受け取ったり、TIntegrList のような動的配列を実装するときや、メモコントロールに長い文字列を設定するときなどは、パスカルの標準手続きである、

procedure GetMem(var P: Pointer; Size: Integer);
procedure ReallocMem(var P: Pointer; Size: Integer);
procedure FreeMem(var P: Pointer[; Size: Integer]);

を使ってきた。System.pas に実装されているこれらのメモリ操作関数は、プロセスヒープに対する操作を行なう。ほとんどの場合、これらの標準手続きを使うだけでメモリ操作は充分である。以下に、第12章で作った TAPIMemo クラスの一行の文字列を取得するメソッドを示す。

function TAPIMemo.GetLine(line: integer): string;
var
  PC: PChar;
  PCLen: Word;
  Len: integer;
begin
  PCLen := GetLineLength(line);
  PCLen := PCLen+1;
  GetMem(PC,PCLen);
  Move(PCLen,PC^,SizeOf(Word));
  Len := SendMessage(FHandle,EM_GETLINE,line,integer(PC));
  SetString(result,PC,Len);
  FreeMem(PC);
end;

ここでは、文字列をさすポインタであるローカル変数 PC に文字列の長さ分のメモリを GetMem( ) を使ってプロセスヒープから確保し、その先頭に Move( ) で長さを書き込み、SendMessage( ) によりメモコントロールから文字列を受け取り、戻り値である string 型の resultSetString( ) によりコピーしてから、FreeMem( ) で解放している。

上記の標準手続きに対応するAPIとして以下のものがある。

function GlobalAlloc(uFlags: UINT; dwBytes: DWORD): HGLOBAL; stdcall;
function GlobalReAlloc(hMem: HGLOBAL; dwBytes: DWORD; uFlags: UINT): HGLOBAL; stdcall;
function GlobalFree(hMem: HGLOBAL): HGLOBAL; stdcall;

uFlags には、以下の定数の組み合わせを指定する。

const
  GMEM_FIXED = 0;       //固定メモリブロックを割り当てる
  GMEM_MOVEABLE = 2;    //移動可能メモリブロックを割り当てる
  GMEM_ZEROINIT = $40;  //割り当てたメモリブロックをゼロで初期化する

  GHND = GMEM_MOVEABLE or GMEM_ZEROINIT;
  GPTR = GMEM_FIXED or GMEM_ZEROINIT;

GlobalAlloc( ) をつかう場合、特に事情がないかぎり GPTR を指定するとよい。この関数の戻り値は、メモリハンドルであるが、GMEM_FIXED をフラグに含んでいるときは、メモリブロックのポインタの値と一致しているので型キャストしてすぐに使用できる。これに対して GMEM_MOVEABLE を含むときは、このハンドルを使って

function GlobalLock(hMem: HGLOBAL): Pointer; stdcall;

により、ポインタに変換してから使用する。そして、使い終わってから

function GlobalUnlock(hMem: HGLOBAL): BOOL; stdcall;

でアンロックしてから、GlobalFree( ) で解放する。GlobalFree( ) は失敗するとゼロ以外の値を戻す。GMEM_FIXED を指定して GlobalAlloc( ) により確保したメモリブロックは、GetMem( ) で確保したものと同様に使うことができる。しかし、クリップボード関連の関数などのように、メモリブロックのハンドルをパラメータとして要求するものには、これらのAPIを使わなければならない。メモリのポインタが分かっているとき、メモリ同士のあいだの内容をやりとりするAPIには以下のものがある。

procedure MoveMemory(Destination: Pointer; Source: Pointer; Length: DWORD);
procedure CopyMemory(Destination: Pointer; Source: Pointer; Length: DWORD);
procedure FillMemory(Destination: Pointer; Length: DWORD; Fill: Byte);
procedure ZeroMemory(Destination: Pointer; Length: DWORD);

これらは、stdcall 呼び出し規約が宣言されていないので分かるように、Delphiでは標準手続き Move( ) FillChar( ) を使って実装されている。この実装では、MoveMemory( )CopyMemory( ) はまったく同一である。以上のAPIのいくつかを使ってみよう。

  1. エクスプローラを使って、新しいフォルダを 1702GlobalMm という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 1701PageSize フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで GlobalMm.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

以下に GlobalMm.dpr の全リストを示す。

program GlobalMm;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  Mm: TAPIMemo;
  L1: TAPILabel;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeInitMenu(hWindow);

  Mm := CreateMemo(hWindow,10,10,430,110);
  Mm.Clear;
  Mm.Text := 'GlobalAlloc( ) をつかう場合、特に指定がないかぎり GPTR を指定'+
         'するとよい。この関数の戻り値は、メモリハンドルであるが、GMEM_FIXED'+
         ' をフラグに含んでいるときは、メモリブロックのポインタの値と一致して'+
         'いるので型キャストしてすぐに使用できる。';

  L1 := Createlabel(hwindow,10,130,415,120,'');

  with MainWnd do begin
    ClientWidth := 450;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
var
  PC: PChar;
  s: string;
begin
  PC := PChar(GlobalAlloc(GPTR,Mm.TextLength+1));
  SendMessage(Mm.Handle,WM_GETTEXT,Mm.TextLength+1,integer(PC));
  Setlength(s,Mm.TextLength);
  MoveMemory(PChar(s),PC,Mm.TextLength);
  if GlobalFree(HGLOBAL(PC))<>0 then MessageBeep($FFFFFFFF);
  L1.Text := s;                             
end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.
DownloadExe.gifGlobalMm.zip(17.747kB)
GlobalMm.gif

実行すると、長い文字列を含むメモと、その下にラベルが作られる。Test! メニューをクリックすると長い文字列がコピーされてラベルに表示される。このときの様子を右図に示す。
OnTest( ) ハンドラを見てみよう。

procedure OnTest(hWindow: HWND);
var
  PC: PChar;
  s: string;
begin
  PC := PChar(GlobalAlloc(GPTR,Mm.TextLength+1));
  SendMessage(Mm.Handle,WM_GETTEXT,Mm.TextLength+1,integer(PC));
  Setlength(s,Mm.TextLength);
  MoveMemory(PChar(s),PC,Mm.TextLength);
  if GlobalFree(HGLOBAL(PC))<>0 then MessageBeep($FFFFFFFF);
  L1.Text := s;                             
end;

ここでは、PChar 型のローカル変数 PCGlobalAlloc( ) により固定メモリ領域を確保して、メモコントロールから文字列を受け取り、string 型のローカル変数 MoveMemory( ) でコピーし、PC を解放してから、ラベルに文字列を設定している。このように、GlobalXXX 関数は、ほとんど GetMem( ) などの標準手続きと同じ手軽さで使用できる。

最新のヘルプを読むと、GlobalXXX 関数は低速なので、他のメモリ操作関数を使うように、という注意が書いてある。割りあてたメモリの「ハンドル」が必要な場合を除いて、次節で紹介する HeapXXX 関数を使うようにすればよいだろう。

redball.gifこの章の目次へ戻る

17−3 ヒープ関数

メモリヒープはハンドルをもつリソースオブジェクトである。プロセスヒープのハンドルは、以下のAPIで取得できる。

function GetProcessHeap: THandle; stdcall;

この関数はパラメータをとらないのでプログラミングでは「GetProcessHeap」という文字列そのものをプロセスヒープのハンドルと同一視できる。ヒープハンドルを手に入れたら、以下のAPIを用いてメモリを操作できる。これらの関数の第一パラメータには、ヒープハンドルを指定する。 まず、ヒープからメモリを割り当てるためには、

function HeapAlloc(hHeap: THandle; 
                 dwFlags, 
                 dwBytes: DWORD): Pointer; stdcall;

を使用する。dwFlags には、HEAP_GENERATE_EXCEPTIONS などのいくつかのシンボル定数の組み合わせを指定できるが、Windows.pas にはこれらの定義が無いので、ここではゼロを指定する。dwBytes には割り当てるバイト数を指定する。この関数の戻り値は、割り当てられたメモリブロックの先頭を指すポインタである。再割り当てには、

function HeapReAlloc(hHeap: THandle; 
                   dwFlags: DWORD; 
                     lpMem: Pointer; 
                   dwBytes: DWORD): Pointer; stdcall;

を使う。ここでも、dwFlags にはゼロを指定する。lpMem には、割り当て直すメモリブロックの先頭を指すポインタを指定する。メモリブロックを解放するには、

function HeapFree(hHeap: THandle; 
                dwFlags: DWORD; 
                  lpMem: Pointer): BOOL; stdcall;

を使う。成功するとゼロ以外の数値を返す。ヒープの中の特定のメモリブロックのサイズを調べるには、

function HeapSize(hHeap: THandle; 
                dwFlags: DWORD; 
                  lpMem: Pointer): DWORD; stdcall;

を使う。lpMem には、サイズを調べるメモリブロックの先頭を指すポインタを指定する。この関数は失敗すると -1 を返す。

それでは、プロセスヒープに対してこれらの関数の幾つかを試してみよう。

  1. エクスプローラを使って、新しいフォルダを 1703HeapFunc という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 1702GlobalMm フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで HeapFunc.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

以下に HeapFunc.dpr の全リストを示す。

program HeapFunc;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
type
  TIntArray = array[0..1000000] of integer;
  PIntArray = ^TIntArray;

var
  MainWnd: TSDIMainWindow;

  LB: TAPIListbox;

  IT: array[0..300] of integer;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeInitMenu(hWindow);

  LB := CreateListBox(hWindow,10,10,340,240,nil);
  Randomize;

  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
var
  Size,i: integer;
  Pt: PIntArray;
begin
  Pt := HeapAlloc(GetProcessHeap,0,SizeOf(Integer)*301);
  for i := 0 to 300 do begin
    IT[i] := Random(50000000);
    Pt^[i] := IT[i];
  end;

  LB.Clear;
  LB.Redraw := false;
  for i := 0 to 300 do
    LB.AddString(AIntToStr(IT[i])+#9+' = '+AIntToStr(Pt^[i]));

  Size := HeapSize(GetProcessHeap,0,Pt);
  LB.AddString('');
  LB.AddString('Size '+#9+' = '+AIntToStr(Size));
  LB.Redraw := true;
  HeapFree(GetProcessHeap,0,Pt);
end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifHeapFunc.zip(16.591kB)
HeapFunc.gif

実行して、Test! メニューをクリックしたときの様子を右図に示す。
コードを見てみよう。グローバルな型として、

type
  TIntArray = array[0..1000000] of integer;
  PIntArray = ^TIntArray;

を新たに定義している。TIntArray 型は、1000001個までの整数配列のための型を定義しているだけであり、実際に1000001個分のメモリを確保しているのではない。そのようなデータ型を定義しているだけだ。PIntArray 型は、TIntArray 型を指すポインタ型である。OnTest( ) ハンドラを見てみよう。

procedure OnTest(hWindow: HWND);
var
  Size,i: integer;
  Pt: PIntArray;
begin
  Pt := HeapAlloc(GetProcessHeap,0,SizeOf(Integer)*301);
  for i := 0 to 300 do begin
    IT[i] := Random(50000000);
    Pt^[i] := IT[i];
  end;

  LB.Clear;
  LB.Redraw := false;
  for i := 0 to 300 do
    LB.AddString(AIntToStr(IT[i])+#9+' = '+AIntToStr(Pt^[i]));

  Size := HeapSize(GetProcessHeap,0,Pt);
  LB.AddString('');
  LB.AddString('Size '+#9+' = '+AIntToStr(Size));
  LB.Redraw := true;
  HeapFree(GetProcessHeap,0,Pt);
end;

ここでは、最初に HeapAlloc( ) を使ってプロセスヒープから301個分の整数配列に相当するサイズのメモリを割り当て、PIntArray 型のローカル変数 Pt に代入している。このようにすると ポインタを逆参照して配列のインデックスを指定するだけで対応する部分の変数にアクセスできる。実際、ここでは、for ループのなかでグローバルな配列と同じランダムな数値を逆参照とインデックスを使って代入している。そして、リストボックスで各々の値を表示して確認している。このように、配列やレコードを指すポインタにメモリを割り当てると、ポインタを明示的に操作(シーク)しなくても、配列要素やレコードのフィールド変数に直接アクセスできる。コードでは、サイズを調べて表示してから HeapFree( ) を使ってメモリを解放している。

以上、ここまででプロセスヒープに対するメモリ操作として

  1. GetMem( ) などのパスカルの標準手続きを使う方法
  2. GlobalAlloc( ) などの GlobalXXX 関数を使う方法
  3. HeapAlloc( ) などの HeapXXX 関数を使う方法

を示してきた。使い分けとしては、特別の事情が無い限りパスカルの標準手続きを使うようにする。メモリハンドルが必要な場合(クリップボード関数や一部のカスタムDLLなど)のみ、GlobalXXX 関数を使う。HeapXXX 関数は次節で示す「動的ヒープ」の中でのみ使うようにするとよいだろう。

redball.gifこの章の目次へ戻る

17−4 動的ヒープの作成

プログラマは、デフォルトで割り当てられるプロセスヒープとは別に、複数のヒープを持つことができる。前節のヒープ関数で見たように、ヒープはハンドルで識別される Win32 のオブジェクトである。プログラマが新しくヒープを作ることのメリットは、すぐに思いつくだけでも、

1)比較的大きな配列データなどを分離して管理できる。
2)スレッドごとに分離したヒープを使用できる。
3)ヒープ内に確保した小さなメモリブロックを一度に解除できる。
4)均一なデータ構造ごとにヒープを動的に確保することで断片化を抑制できる。

などが挙げられる。ヒープを新しく作るには、

function HeapCreate(flOptions, 
                dwInitialSize, 
                dwMaximumSize: DWORD): THandle; stdcall;

を使う。flOptions には、HEAP_GENERATE_EXCEPTIONS などのいくつかのシンボル定数の組み合わせを指定できるが、Windows.pas にはこれらの定義が無いので、ここではゼロを指定する。dwInitialSize には、作成時に予め確保しておくメモリ容量を設定する。ゼロでもよい。dwMaximumSize は、将来このヒープで使うことになる最大容量を設定する。ゼロ以外の数値を設定したときは「成長できないヒープ」となり、この値を越えた容量の割り当てを要求するとエラーになる。ゼロを設定すると「成長できるヒープ」となり、仮想メモリ空間の割り当てがあるかぎり容量を拡大できる。プロセスヒープも「成長できるヒープ」である。HeapCreate( ) は成功するとヒープハンドルを返す。失敗するとゼロを返す。返されたヒープハンドルを使えば、前節のヒープ関数を使ってプロセスヒープと同じように使用できる。実際、こまごまとした一時的メモリバッファを割り当てることもできるし、大量のデータ配列を割り当てることもできる。動的に作成したヒープを使い終わったら、

function HeapDestroy(hHeap: THandle): BOOL; stdcall;

で解放する。このとき、ヒープ内の各メモリブロックを HeapFree( ) で個別に解放していなくてもヒープ全体が解放(破棄)される。それでは、動的ヒープをつくって試してみよう。

  1. エクスプローラを使って、新しいフォルダを 1704CustomHeap という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 1703HeapFunc フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで CustomHeap.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

以下に CustomHeap.dpr の全リストを示す。

program CustomHeap;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
type
  TMyRecord = record
    Int: integer;
    Wrd: word;
    byt: byte;
  end;
  
  TRecArray = array[0..1000000] of TMyRecord;
  PRecArray = ^TRecArray;

var
  MainWnd: TSDIMainWindow;

  LB: TAPIListbox;

  hMyHeap: THandle;
  MR: array[0..100] of TMyRecord;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeInitMenu(hWindow);

  hMyHeap := HeapCreate(0,0,0);

  LB := CreateListBox(hWindow,10,10,340,240,nil);
  Randomize;

  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
var
  Size,i: integer;
  Pt: PRecArray;
begin
  Pt := HeapAlloc(hMyHeap,0,SizeOf(TMyRecord)*101);
  for i := 0 to 100 do begin
    with MR[i] do begin
      INT := Random(50000000);
      Wrd := Random(30000);
      byt := Random(256);
    end;
    Pt^[i] := MR[i];
  end;

  LB.Clear;
  LB.Redraw := false;
  for i := 0 to 100 do begin
    LB.AddString(AIntToStr(MR[i].INT)+#9+' = '+AIntToStr(Pt^[i].INT));
    LB.AddString(AIntToStr(MR[i].Wrd)+#9+' = '+AIntToStr(Pt^[i].Wrd));
    LB.AddString(AIntToStr(MR[i].byt)+#9+' = '+AIntToStr(Pt^[i].Byt));
    LB.AddString('');
  end;

  Size := HeapSize(hMyHeap,0,Pt);
  LB.AddString('');
  LB.AddString('Size '+#9+' = '+AIntToStr(Size));
  LB.Redraw := true;
  HeapFree(hMyHeap,0,Pt);
end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  HeapDestroy(hMyHeap);
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifCustomHeap.zip(15.481kB)
CustomHeap.gif

実行して、Test! メニューをクリックすると右図のようになる。
ここでは、新たに次のようなレコードを定義して、その配列を動的ヒープに格納している。

  TMyRecord = record
    Int: integer;
    Wrd: word;
    byt: byte;
  end;
  
  TRecArray = array[0..1000000] of TMyRecord;
  PRecArray = ^TRecArray;

MainCreate( ) ハンドラで動的ヒープを作成して、そのハンドルをグローバル変数に格納している。

hMyHeap := HeapCreate(0,0,0);

ここでは、「成長できるヒープ」を作成している。そして、MainDestroy( ) ハンドラでこのヒープを破棄している。OnTest( ) ハンドラでは、TMyRecord101個分のメモリをこのヒープから確保し、各フィールドにランダムな値を代入してから、配列の値との比較をリストボックスに表示している。最後にメモリブロックを解放している。

redball.gifこの章の目次へ戻る

17−5 ポインタを使ったメモリアクセス

17−2節では、GlobalAlloc( ) で割り当てた固定メモリブロックの先頭のポインタのみを使用して文字列をアクセスした。3節では、HeapAlloc( ) で返されたポインタを整数配列へのポインタに代入して、逆参照の配列のインデックスを使ってメモリにアクセスして、各配列の要素の値を取得していた。4節では、HeapAlloc( ) のポインタを、レコードの配列へのポインタに代入し、逆参照の配列のインデックスとフィールドを参照して値を取得していた。このように、配列やレコードへのポインタは、逆参照のインデックスやフィールドを使って値を取得できるので、メモリの中の各値が格納されている場所をポインタを使って計算しなくてよいので便利である。インデックスやフィールドを使うことにより、コンパイラが自動的にアドレスを計算するコードを生成するのである。
しかし、後に示すビットマップファイルやアイコンファイルのイメージをメモリに展開したときのように、不定長のレコード型へのアクセスには、ポインタの操作によるアクセスが必須である。この節では、インデックスやフィールドを参照しないでメモリにアクセスすることを試みよう。

前節では以下のようなレコード型を定義して、その配列へのポインタを定義した。

type
  TMyRecord = record
    Int: integer;
    Wrd: word;
    byt: byte;
  end;
  
  TRecArray = array[0..1000000] of TMyRecord;
  PRecArray = ^TRecArray;

ここで定義した TMyRecord 型のサイズは、4+2+1=7 バイトのはずであるが、SizeOf( ) 関数を使って確かめてみるとが返される。実際、前節のプログラムを実行してリストボックスの最後の欄を見て、Size = 808 となっているので、「あれっ」と思われた方も多いだろう。Delphi のパスカルの仕様なのか、あるいは Windows の仕様なのかレコードのサイズに奇数バイトを割り当てることを嫌う傾向がある。(追記。 これは Delphi のパスカルの仕様である。ヘルプの「構造化型」のところに書いてあるが、Delphi の Object Pascal では、アクセスを速くするために構造化型は、ワード(2バイト)またはダブルワード(4バイト)の境界で配置される。アクセス速度を犠牲にして、キーワード packed を使うと、上記のようなレコード型でも SizeOf( ) で7バイトと返すように配置される。 2000/7/5)したがって、808バイトの中の101個の配列のうち、各配列の末尾1バイトは使われていないので不定の値が格納されている。以下のプログラムでは、配列を SizeOf( )を使わず代入し、取得はフィールドを参照しないでポインタを使ってアクセスしよう。

  1. エクスプローラを使って、新しいフォルダを 1705GetFromPointer という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 1704CustomHeap フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで GetFromPointer.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

以下に、GetFromPointer.dpr の全リストを示す。

program GetFromPointer;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
type
  TMyRecord = record
    Int: integer;
    Wrd: word;
    byt: byte;
  end;
  
  TRecArray = array[0..1000000] of TMyRecord;
  PRecArray = ^TRecArray;

var
  MainWnd: TSDIMainWindow;

  LB: TAPIListbox;
  Btn1,Btn2: TAPIButton;

  hMyHeap: THandle;
  MR: array[0..100] of TMyRecord;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------
procedure Btn1click(var Msg: TMessage);
var
  Size,i: integer;
  Pt: PRecArray;
begin
  Pt := HeapAlloc(hMyHeap,0,SizeOf(TMyRecord)*101);
  for i := 0 to 100 do begin
    Pt^[i] := MR[i];
  end;

  LB.Clear;
  LB.Redraw := false;
  for i := 0 to 100 do begin
    LB.AddString(AIntToStr(MR[i].INT)+#9+' = '+AIntToStr(Pt^[i].INT));
    LB.AddString(AIntToStr(MR[i].Wrd)+#9+' = '+AIntToStr(Pt^[i].Wrd));
    LB.AddString(AIntToStr(MR[i].byt)+#9+' = '+AIntToStr(Pt^[i].Byt));
    LB.AddString('');
  end;

  Size := HeapSize(hMyHeap,0,Pt);
  LB.AddString('');
  LB.AddString('Size '+#9+' = '+AIntToStr(Size));
  LB.Redraw := true;
  HeapFree(hMyHeap,0,Pt);
end;

procedure Btn2click(var Msg: TMessage);
  function PPlus(p:Pointer;dp:integer):Pointer;
  begin
    result := Pointer(integer(p)+dp);
  end;
var
  i,IT,Size:integer;
  BasePtr,Ptr,p: Pointer;
  WD:Word;
  BT:byte;
begin
  BasePtr := HeapAlloc(hMyHeap,0,7*101);
  Ptr := BasePtr;
  for i := 0 to 100 do begin
    MoveMemory(Ptr,@MR[i],7);
    Ptr := PPlus(Ptr,7);
  end;

  Ptr := BasePtr;
  LB.Clear;
  LB.Redraw := false;
  for i := 0 to 100 do begin
    p := Ptr;
    MoveMemory(@IT,p,SizeOf(integer));
    p := PPlus(p,SizeOf(integer));
    MoveMemory(@WD,p,SizeOf(word));
    p := PPlus(p,SizeOf(word));
    MoveMemory(@BT,p,SizeOf(byte));
    LB.AddString(AIntToStr(MR[i].INT)+#9+' = '+AIntToStr(IT));
    LB.AddString(AIntToStr(MR[i].Wrd)+#9+' = '+AIntToStr(WD));
    LB.AddString(AIntToStr(MR[i].byt)+#9+' = '+AIntToStr(BT));
    LB.AddString('');
    Ptr := PPlus(Ptr,7);
  end;
  Size := HeapSize(hMyHeap,0,BasePtr);
  LB.AddString('');
  LB.AddString('Size '+#9+' = '+AIntToStr(Size));
  LB.Redraw := true;
  HeapFree(hMyHeap,0,BasePtr);
end;
//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
  i:integer;
begin
  hWindow := MainWnd.Handle;
  MakeInitMenu(hWindow);

  hMyHeap := HeapCreate(0,0,0);

  LB := CreateListBox(hWindow,10,10,340,240,nil);
  Btn1 := CreateButton(hWindow,360,10,90,35,'Array',Btn1Click);
  Btn2 := CreateButton(hWindow,360,55,90,35,'Memory',Btn2Click);

  Randomize;
  for i := 0 to 100 do
    with MR[i] do begin
      INT := Random(50000000);
      Wrd := Random(30000);
      byt := Random(256);
    end;

  with MainWnd do begin
    ClientWidth := 460;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  MessageBeep($FFFFFFFF);
end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  HeapDestroy(hMyHeap);
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifGetFromPointer.zip(17.505kB)
GetFromPointer.gif

実行して、'Array' ボタンを押すと前節のプログラムとまったく同じ表示が現れ、やはりサイズは808バイトとなっているのが確認できる。'Memory' のボタンを押すと表示される各数値は同じであるが、サイズは708バイトであることが分かるだろう。この様子を右図に示す。
コードを見てみよう。Btn2Click( ) ハンドラでは初めに以下のコードでメモリを割り当てて、配列を代入している。

  BasePtr := HeapAlloc(hMyHeap,0,7*101);
  Ptr := BasePtr;
  for i := 0 to 100 do begin
    MoveMemory(Ptr,@MR[i],7);
    Ptr := PPlus(Ptr,7);
  end;

動的ヒープに707バイトのメモリを確保し、その先頭を指すポインタを BasePtr に格納している。そして、各配列要素には、バイトずつポインタをずらしながら MoveMemory( ) で値を代入している。PPlus( ) ヘルパ関数は、ポインタをずらすためのものだ。

function PPlus(p:Pointer;dp:integer):Pointer;
begin
  result := Pointer(integer(p)+dp);
end;

後方へ移動するためには、dp にマイナス値を設定する。各配列要素の各フィールドへアクセスするコードを見てみよう。

  for i := 0 to 100 do begin
    p := Ptr;
    MoveMemory(@IT,p,SizeOf(integer));
    p := PPlus(p,SizeOf(integer));
    MoveMemory(@WD,p,SizeOf(word));
    p := PPlus(p,SizeOf(word));
    MoveMemory(@BT,p,SizeOf(byte));
    LB.AddString(AIntToStr(MR[i].INT)+#9+' = '+AIntToStr(IT));
    LB.AddString(AIntToStr(MR[i].Wrd)+#9+' = '+AIntToStr(WD));
    LB.AddString(AIntToStr(MR[i].byt)+#9+' = '+AIntToStr(BT));
    LB.AddString('');
    Ptr := PPlus(Ptr,7);
  end;

このように、Ptrバイトごとにずらし、各 Ptr に対して p を各フィルードのサイズ分ずらしながら MoveMemory( ) で変数のアドレスに代入している。このように、各フィールドの深さごとにポインタ変数をずらしながらアクセスしていくと、レコード型のフィールドを持つレコード型の場合でも、いくらでも深くフィールド値にアクセスできる。

以上のように、ポインタをあらわに操作してメモリにアクセスするのが最も低レベルで最も強力なアクセス方法である。最後に、この例では、707バイトのメモリを要求したのに実際のサイズは708バイトであった。やはり、ここでも奇数バイトは避けられるようだ。これは Windows の仕様なのだろう。

redball.gifこの章の目次へ戻る

17−6 まとめ

本章では、Win32 のメモリ空間について説明し、デフォルトで割り当てられるプロセスヒープからのメモリ割り当て方法を調べ、さらに動的ヒープを作成して使用する方法を調べた。割り当てられたメモリ領域を、先頭ポインタ、配列のインデックス、レコード型のフィールドを参照してアクセスする方法、さらに、ポインタを操作してアクセスする方法を示した。

ここで示したメモリ割り当て方法のほかに、ヒープという概念を離れて比較的大容量のメモリを割り当てて、ページング動作を制御できる一連の VirtualXXX( ) 関数もあるが、ここでは扱わなかった。


ここまでのプログラムファイル(本章ではユニットファイルに変更を加えなかった)

  1. Skeleton03.dpr (2 kB)
  2. UtilFunc.pas (11 kB)
  3. UtilClass.pas (38 kB)
  4. GDIObject.pas (21 kB)
  5. APIControl.pas (61 kB)
  6. APIWindow.pas (19 kB)



redball.gifこの章の目次へ戻る