Delphi內存專題

第一課:編程

Windows 是多任務的操做系統, 一個任務就是一個應用(應用程序)、一個應用佔一個進程; 在一個進程裏面, 又能夠運行多個線程(因此就有了不少"多線程編程"的話題).小程序



對 Win32 來說, 系統給每一個進程 4GB 的地址空間:
低端 2GB($00000000 - $7FFFFFFF) 給用戶支配;
高端 2GB($80000000 - $FFFFFFFF) 留給系統使用.數組



文件或程序要調入內存才能工做, 先看看咱們的內存到底有多大吧.

在系統盤根目錄下有個 pagefile.sys 文件, 這就是咱們的 "虛擬內存"(虛擬內存是以文件的形式存在的).

把 pagefile.sys 叫作 "虛擬內存" 彷佛不妥, 所謂的 "虛擬" 只是相對真實的物理內存(RAM)來說的; 不少書上的 "物理內存" 指的實際上是: RAM + 虛擬內存, 也就是全部可用內存.

"虛擬內存" 在有些書上也被稱做 "頁文件" 、"頁面文件" 或 "交換文件". "虛擬內存" 的大小能夠從 "控制面板" 裏設置, 默認是由系統自動管理的.

使用 "虛擬內存" 是系統的機制, 無論 RAM 有多大, 也應該使用 "虛擬內存".

RAM 大了, 系統就會少用 "虛擬內存", 從而提升速度; 但 RAM 也不是越大越好, 若是你真的放 4G 的內存條, 系統可以識別並使用的也就是 3G 左右, 由於 Win32 只有 4G 的管理能力(尋址能力), 固然這在 Win64 下要另當別論.多線程



所謂系統給每一個程序 4G, 是給 4G 的 "虛擬的地址表", 毫不是真實的內存, 否則一個記事本、一個計算器就得須要 8G.

這個 "虛擬的地址表" 在有些書上叫 "虛地址表"、"頁映射表" 或 "虛內存地址", 也有叫 "虛擬內存地址", 很容易和 "虛擬內存" 的概念混淆.

這個 "虛擬的地址表" 上有 4G 個(4294967296 個)地址(0 - $FFFFFFFF), 雖然每一個程序都有這樣一個表, 但它們並不會衝突, 就由於這些地址是虛擬的, 系統在須要的時候會把它們映射成具體的真實內存的地址. 這樣就阻斷了一個進程對另外一個進程的訪問.

在 Win2000 之前的版本中, 用 GlobalAlloc 申請公用內存, 用 LocalAlloc 申請私有內存; 如今經過 "虛擬的地址表" 使用內存, 在進程中申請的內存都是私有的, 如今的 GlobalAlloc、LocalAlloc 沒有區別, 都是執行一樣的代碼.

若是須要跨進程的公用內存空間, 須要用 "內存映射" 等手段, 這須要再專題學習.dom



總結概念: 物理內存、虛擬內存、虛地址表.
函數 GlobalMemoryStatus 能夠獲取它們的信息, 獲取後放在 TMemoryStatus 結構中.函數


//TMemoryStatus 是 _MEMORYSTATUS 的重命名:
_MEMORYSTATUS = record
  dwLength: DWORD;        {結構長度}
  dwMemoryLoad: DWORD;    {表示已使用的內存比例的一個整數}
  dwTotalPhys: DWORD;     {物理內存總數}
  dwAvailPhys: DWORD;     {可用物理內存總數}
  dwTotalPageFile: DWORD; {虛擬內存總數}
  dwAvailPageFile: DWORD; {可用虛擬內存總數}
  dwTotalVirtual: DWORD;  {虛地址表中的地址總數}
  dwAvailVirtual: DWORD;  {虛地址表中可用的地址總數}
end;

作個小程序看看內存狀況:學習


unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
var
  m: TMemoryStatus;
const
  num = 1024 * 1024;
begin
  GlobalMemoryStatus(m);
  Memo1.Clear;
  with Memo1.Lines do begin
    Add(Format('dwLength:'        + #9 + '%d', [m.dwLength]));
    Add(Format('dwMemoryLoad:'    + #9 + '%d', [m.dwMemoryLoad]));
    Add(Format('dwTotalPhys:'     + #9 + '%d', [m.dwTotalPhys div num]));
    Add(Format('dwAvailPhys:'     + #9 + '%d', [m.dwAvailPhys div num]));
    Add(Format('dwTotalPageFile:' + #9 + '%d', [m.dwTotalPageFile div num]));
    Add(Format('dwAvailPageFile:' + #9 + '%d', [m.dwAvailPageFile div num]));
    Add(Format('dwTotalVirtual:'  + #9 + '%d', [m.dwTotalVirtual div num]));
    Add(Format('dwAvailVirtual:'  + #9 + '%d', [m.dwAvailVirtual div num]));
  end;
end;

end.

我這裏的運行效果圖:

操作系統


 第二課:線程

靜態數組, 在聲明時就分配好內存了, 譬如:指針


var
  arr1: array[0..255] of Char;
  arr2: array[0..255] of Integer;
begin
  ShowMessageFmt('數組大小分別是: %d、%d', [SizeOf(arr1), SizeOf(arr2)]);
  {數組大小分別是: 5十二、1024}
end;


對靜態數組指針, 雖然在聲明之處並無分配內存, 但這個指針應該分配多少內存是有定數的.

這種狀況, 咱們應該用 New 和 Dispose 來分配與釋放內存. 譬如:


type
  TArr1 = array[0..255] of Char;
  TArr2 = array[0..255] of Integer;
var
  arr1: ^TArr1;
  arr2: ^TArr2;
begin
  New(arr1);
  New(arr2);

  arr1^ := '萬一的 Delphi 博客';
  ShowMessageFmt('%s%s', [arr1^[0], arr1^[1]]); {萬一}
//  ShowMessageFmt('%s%s', [arr1[0], arr1[1]]); {這樣也能夠}

  arr2[Low(arr2^)] := Low(Integer); {第一個元素賦最小值}
  arr2[High(arr2^)] := MaxInt;      {第一個元素賦最大值}
  ShowMessageFmt('%d, %d', [arr2[0], arr2[255]]); {-2147483648, 2147483647}

  Dispose(arr1);
  Dispose(arr2);
end;

//變通一下, 再作一遍這個例子:
type
  TArr1 = array[0..255] of Char;
  TArr2 = array[0..255] of Integer;
  PArr1 = ^TArr1;
  PArr2 = ^TArr2;
var
  arr1: PArr1;
  arr2: PArr2;
begin
  New(arr1);
  New(arr2);

  arr1^ := '萬一的 Delphi 博客';
  ShowMessageFmt('%s%s', [arr1[0], arr1[1]]);

  arr2[Low(arr2^)] := Low(Integer);
  arr2[High(arr2^)] := MaxInt;
  ShowMessageFmt('%d, %d', [arr2[0], arr2[255]]); {-2147483648, 2147483647}

  Dispose(arr1);
  Dispose(arr2);
end;


給已知大小的指針分配內存應該用 New, 上面的例子是關於靜態數組指針的, 後面要提到的結構體(記錄)的指針也是如此.

New 的本質也函數調用 GetMem, 但不須要咱們指定大小了.

但這對動態數組就不合適了, 不過給動態數組分配內存 SetLength 應該足夠了, 譬如:


var
  arr: array of Integer;
begin
  SetLength(arr, 3);

  arr[0] := Random(100);
  arr[1] := Random(100);
  arr[2] := Random(100);

  ShowMessageFmt('%d,%d,%d', [arr[0],arr[1],arr[2]]); {0,3,86}
end;


那怎麼給動態數組的指針分配內存呢? 其實動態數組變量自己就是個指針, 就不要繞來繞去再給它弄指針了.

不過有一個理念仍是滿重要的, 那就是咱們能夠把一個無類型指針轉換爲動態數組類型, 譬如:


type
  TArr = array of Integer;
var
  p: Pointer;
begin
  GetMem(p, 3 * SizeOf(Integer)); {分配能容納 3 個 Integer 的空間}

  {這和 3 個元素的 TArr 的大小是同樣的, 但使用時須要進行類型轉換}
  TArr(p)[0] := Random(100);
  TArr(p)[1] := Random(100);
  TArr(p)[2] := Random(100);

  ShowMessageFmt('%d,%d,%d', [TArr(p)[0], TArr(p)[1], TArr(p)[2]]); {0,3,86}

  FreeMem(p);
end;


這裏用到了 GetMem 和 FreeMem, 對分配無類型指針這是比較經常使用的; 對其餘類型的指針它能夠, 但不見得是最好的方案, 譬如:


//獲取窗口標題(顯然不如用前面說過的 StrAlloc 更好)
var
  p: Pointer;
begin
  GetMem(p, 256);
  GetWindowText(Handle, p, 256);
  ShowMessage(PChar(p)); {Form1}
  FreeMem(p);
end;


應該提倡用 GetMemory 和 FreeMemory 代替 GetMem、FreeMem, 譬如:


var
  p: Pointer;
begin
  p := GetMemory(256);
  GetWindowText(Handle, p, 256);
  ShowMessage(PChar(p)); {Form1}
  FreeMemory(p);
end;


先總結下:
New 是給已知大小的指針分配內存;
GetMem 主要是給無類型指針分配內存;
儘可能使用 GetMemory 來代替 GetMem.

還有個 AllocMem 和它們又有什麼區別呢?

AllocMem 分配內存後會同時初始化(爲空), GetMem 則不會, 先驗證下:


var
  p1,p2: Pointer;
begin
  p1 := AllocMem(256);
  ShowMessage(PChar(p1)); {這裏會顯示爲空}
  FreeMemory(p1);

  p2 := GetMemory(256);
  ShowMessage(PChar(p2)); {這裏會顯示一些垃圾數據, 內容取決與在分配之前該地址的內容}
  FreeMemory(p2);
end;


關於 FreeMemory 與 FreeMem 的區別:
一、FreeMemory 會檢查是否爲 nil 再 FreeMem, 這有點相似: Free 與 Destroy;
二、FreeMem 還有個默認參數能夠指定要釋放的內存大小, 不指定就所有釋放(不必只釋放一部分吧);
三、New 對應的 Dispose 也能夠用 FreeMem 或 FreeMemory 代替.

儘可能使用 FreeMemory 來釋放 GetMem、GetMemory、AllocMem、ReallocMem、ReallocMemory 分配的內存.

ReallocMem、ReallocMemory 是在已分配的內存的基礎上從新分配內存, 它倆差很少 ReallocMemory 比 ReallocMem 多一個 nil 判斷, 儘可能使用 ReallocMemory 吧. 譬如:


type
  TArr = array[0..MaxListSize] of Char;
  PArr = ^TArr;
var
  arr: PArr;
  i: Integer;
begin
  arr := GetMemory(5);
  for i := 0 to 4 do arr[i] := Chr(65+i);
  ShowMessage(PChar(arr)); {ABCDE}

  arr := ReallocMemory(arr, 26);
  ShowMessage(PChar(arr)); {ABCDE}
  for i := 0 to 25 do arr[i] := Chr(65+i);
  ShowMessage(PChar(arr)); {ABCDEFGHIJKLMNOPQRSTUVWXYZ}
end;


注意上面這個例子中 TArr 類型, 它被定義成一個足夠大的數組; 這種數組留出了足夠的可能性, 但通常不會所有用到.
咱們通常只使用這種數組的指針, 不然一初始化將會內存不足而當機.
即使是使用其指針, 也不能用 New 一次行初始化; 應該用 GetMem、GetMemory、AllocMem、ReallocMem、ReallocMemory 等用多少申請多少.
須要注意的是, 從新分配內存也多是越分越少; 若是越分越大應該能夠保證之前數據的存在.
這在 VCL 中 TList 類用到的理念.

若是你在內心上接受不了那麼大一個數組(其實沒事, 一個指針纔多大? 咱們只使用其指針), 也能夠這樣:


type
  TArr = array[0..0] of Char;
  PArr = ^TArr;
var
  arr: PArr;
  i: Integer;
begin
  arr := GetMemory(5);
  for i := 0 to 4 do arr[i] := Chr(65+i);
  ShowMessage(PChar(arr)); {ABCDE}

  arr := ReallocMemory(arr, 26);
  ShowMessage(PChar(arr)); {ABCDE}
  for i := 0 to 25 do arr[i] := Chr(65+i);
  ShowMessage(PChar(arr)); {ABCDEFGHIJKLMNOPQRSTUVWXYZ}
end;

 
這好像又讓人費解, 只有一個元素的數組能幹什麼?
應該這樣理解: 僅僅這一個元素就足夠指示數據的起始點和數據元素的大小和規律了.

另外的 SysGetMem、SysFreeMem、SysAllocMem、SysReallocMem 四個函數, 應該是上面這些函數的底層實現, 在使用 Delphi 默認內存管理器的狀況下, 咱們仍是不要直接使用它們.

 

熱愛工做,熱愛生活,熱愛文學
相關文章
相關標籤/搜索