前言
在程式語言中,資料型態 (data type) 規範資料所占用的記憶體大小及合法的操作。主流的程式語言都有資料型態的概念。本文介紹 Pascal 中可見的資料型態。
Pascal 的資料型態
以下是 Pascal 中可見的資料型態:
- 基礎型態(primitive type)
- 布林 (boolean)
- 字元 (character)
- 整數 (integer)
- 浮點數 (floating point number)
- 列舉 (enumeration)
- 容器 (collection)
- 陣列 (array)
- 集合 (set)
- 複合型態 (compound type)
- 記錄 (record)
- 物件 (object) (Object Pascal)
- 類別 (class) (Object Pascal)
- 指標 (pointer)
- 不定型態 (Variant)
基礎型態代表最簡約的 (atomic) 資料型態。這些資料在概念上無法再拆分成更小的單位。Pascal 支援數種在程式語言中常見的基礎資料型態。
容器用來裝載多個相同型態的資料。Pascal 的容器相當靈活,除了用來裝載純量外,也可用來裝載複合型態,甚至組成多維度的容器,像是多維陣列。
複合型態是用來表達純量以外的資料。由於複合型態是由程式設計者自行宣告,使用者有更多的彈性,利用複合型態表達無法以純量表達的概念。原本 Pascal 的複合型態只有記錄,後來在 Object Pascal 新增物件和類別。後兩者是為了在 Pascal 中導入物件導向特性而新增的特性。
指標是為了操作記憶體而設置的語法特性,算是程式設計中特有的概念。由於指標在現實生活中沒有相對應的概念,一開始較不易上手。
在早期的高階語言中,Pascal 的資料型態相當完整。該語言中有關型態的概念影響了許多後來的程式語言。
預設型態建議表
本節根據實務考量選擇預設型態,建議在程式碼中開啟以下 Compiler Directive:
{$ifdef FPC} {$mode DELPHIUNICODE} {$endif}
由於 Pascal 具有系統語言的特性,實際上有許多內建資料型態。在撰寫一般應用程式時,不需要對資料型態斤斤計較。建議採用以下資料型態:
| 類別 | 建議型態 | 理由 |
|---|---|---|
| 整數 | Integer |
32 位元,兼顧效能與範圍(約 21 億)。 |
| 布林 | Boolean |
標準邏輯判斷,只有 True 與 False。 |
| 小數 | Double |
現代硬體優化最完整的 64 位元浮點數,精度穩定。 |
| 字串 | String |
自動管理記憶體的長字串。在現代環境下支援動態長度。 |
| 字元 | Char |
對應 String 的基礎字元單位。 |
實際上 String 和 Char 會因環境而異,無法適用所有字串。建議做法是儘量和 Delphi 對齊。
若需要明確的資料寬度,請自行查閱 Pascal 語言參考手冊。
型態別名
利用 Pascal 的型態別名特性,可以為一些資料型態取更容易記的別名。參考以下宣告:
{$ifndef COMMON_TYPE_INC}
{$define COMMON_TYPE_INC}
Type
Int = Integer;
{$ifdef FPC}
Int8 = Shortint;
Int16 = Smallint;
Int32 = Longint;
{$endif}
(* Int64 is builtin in
both Delphi and Free Pascal. *)
{$ifdef FPC}
UInt8 = Byte;
UInt16 = Word;
UInt32 = Longword;
UInt64 = QWord;
{$endif}
Bool = Boolean;
{$ifndef FPC}
UnicodeChar = WideChar;
{$endif}
{$endif}
在 Type 區塊的上方加入我們寫好的型態宣告:
{$INCLUDE Type.inc}
之後在程式碼中就可以使用比較易記的別名來標註變數型態。
由於新版的 Delphi 已經內建固定寬度整數別名,所以將重覆的部分用 Compiler Directive 包起來,只給 Free Pascal 用。
布林 (Boolean)
布林型態用來表達布林數,其值只有 true 和 false。Pascal 的布林數可再細分為以下數種型態:
- 預設型態
boolean:8 位元
- 相容於帶號整數
boolean8:8 位元boolean16:16 位元boolean32:32 位元boolean64:64 位元 (視系統支援而定)
- 相容於無號整數
bytebool:8 位元wordbool:16 位元longbool:32 位元qwordbool:64 位元 (視系統支援而定)
在一般情形下,使用第一種 boolean 型態來表達布林數即可。後面數種布林型態是為了和 C 相容而設置的。
字元 (Character)
字元型態用來表達單個字母 (letter) 或符號 (symbol)。在 Pascal 中有兩種字元型態:
Char:8 位元,同AnsiCharAnsiChar:8 位元WideChar:16 位元
Char (或 AnsiChar) 使用 ASCII 編碼,適用於純英語環境。而 WideChar 以 UTF-16 編碼,用於多國語言文字。
透過以下程式可取得 Pascal 中的字元型態的寬度:
{$ifdef FPC} {$mode DELPHIUNICODE} {$endif}
{$ifdef WINDOWS} {$apptype CONSOLE} {$endif}
Program Main;
Uses
SysUtils;
{$INCLUDE Type.inc}
Begin
WriteLn(Format('Size of Char: %d', [SizeOf(Char)]));
WriteLn(Format('Size of WideChar: %d', [SizeOf(UnicodeChar)]));
End.
相容性考量
Char 的實際內容會因環境而異。在現代 Delphi 中為 2 變數組的 Unicode;在 FPC 中則依編碼設定而異。
考量相容性,建議將 String / Char 限定在 ASCII 字元,碰到 Unicode 字完時改用 UnicodeString / UnicodeChar。
由於 Delphi 沒有 UnicodeChar 型態,建議參考先前的型態別名,將 UnicodeChar 對應到 WideChar。
我們來看一個用字元接住字串的範例:
{$ifdef FPC} {$mode DELPHIUNICODE} {$endif}
{$ifdef WINDOWS} {$apptype CONSOLE} {$endif}
program Main;
{$INCLUDE Type.inc}
var
s: UnicodeString;
c: UnicodeChar;
begin
s := '你好,世界';
c := s[1];
Assert('你' = c);
{$ifdef DEBUG} readln; {$endif}
end.
Pascal 的字串是 1 index,但 C / C++ 的 0 index 相異。這裡用 c := s[1] 把字元接比來,然後確認字元。
整數 (Integer)
整數型態用來表達整數。在 Pascal 中的整數型態可依是否有帶正負號分為兩大類:
- 無號整數 (unsigned integer)
cardinal:16 位元或 32 位元,依系統而定byte:8 位元word:16 位元longword:32 位元qword:64 位元 (視系統支援而定)
- 帶號整數 (signed integer)
integer:16 位元或 32 位元,依系統而定shortint:8 位元smallint:16 位元longint:32 位元int64:64 位元 (視系統支援而定)
在電腦程式中細分整數型態的目的是節約系統資源,能用寬度小的整數型態就不用寬度大的。早期的電腦運算資源相對昂貴,所以會對運算資源斤斤計較。
此外,Pascal 的整數型態的寬度是固定的,當我們想要使用特定範圍的整數資料時,就可以選取相對應的整數型態。
但對一般使用來說,不需要分那麼細。現在的個人電腦相對於電腦程式來說,運算資源算是相當充沛。無號整數用 cardinal,帶號整數用 integer 即可。
使用預設模式 (fpc mode) 編譯 Pascal 程式時,integer 對應到 smallint 型態。使用 Object Pascal 模式 (objfpc mode) 或 Delphi 模式 (delphi mode) 編譯 Pascal 程式時,integer 對應到 longint 型態。
以下程式可取得系統上的整數型態的寬度:
{$ifdef FPC} {$mode DELPHIUNICODE} {$endif}
{$ifdef WINDOWS} {$apptype CONSOLE} {$endif}
Program Main;
Uses
SysUtils;
{$INCLUDE Type.inc}
Begin
WriteLn(format('Size of Cardinal: %d', [sizeOf(Cardinal)]));
WriteLn(format('Size of Byte: %d', [sizeOf(UInt8)]));
WriteLn(format('Size of Word: %d', [sizeOf(UInt16)]));
WriteLn(format('Size of LongWord: %d', [sizeOf(UInt32)]));
WriteLn(format('Size of Qword: %d', [sizeof(UInt64)]));
WriteLn(''); (* Separator. *)
WriteLn(format('Size of Integer: %d', [sizeOf(Int)]));
WriteLn(format('Size of ShortInt: %d', [sizeOf(Int8)]));
WriteLn(format('Size of SmallInt: %d', [sizeOf(Int16)]));
WriteLn(format('Size of LongInt: %d', [sizeOf(Int32)]));
Writeln(format('Size of Int64: %d', [sizeOf(Int64)]));
End.
以下是筆者在自己的電腦上試跑的結果:
Size of Cardinal: 4
Size of Byte: 1
Size of Word: 2
Size of LongWord: 4
Size of Qword: 8
Size of Integer: 4
Size of ShortInt: 1
Size of SmallInt: 2
Size of LongInt: 4
Size of Int64: 8
在不同系統上得到的整數型態寬度可能相異,不要背誦這個結果。
以下程式可取得整數型態的範圍:
{$ifdef FPC} {$mode DELPHIUNICODE} {$endif}
{$ifdef WINDOWS} {$apptype CONSOLE} {$endif}
Program Main;
Uses
SysUtils;
{$INCLUDE Type.inc}
Begin
WriteLn('Range of Cardinal between ' + IntToStr(low(Cardinal)) + ' and ' + IntToStr(high(Cardinal)
));
WriteLn('Range of Byte between ' + IntToStr(low(UInt8)) + ' and ' + IntToStr(high(UInt8)));
WriteLn('Range of Word between ' + IntToStr(low(UInt16)) + ' and ' + IntToStr(high(UInt16)));
WriteLn('Range of LongWord between ' + IntToStr(low(UInt32)) + ' and ' + IntToStr(high(UInt32)));
WriteLn('Range of Qword between ' + IntToStr(low(UInt64)) + ' and ' + IntToStr(high(UInt64)));
WriteLn(''); (* Separator. *)
WriteLn('Range of Integer between ' + IntToStr(low(Int)) + ' and ' + IntToStr(high(Int)));
WriteLn('Range of ShortInt between ' + IntToStr(low(Int8)) + ' and ' + IntToStr(high(Int8)));
WriteLn('Range of SmallInt between ' + IntToStr(low(Int16)) + ' and ' + IntToStr(high(Int16)));
WriteLn('Range of LongInt between ' + IntToStr(low(Int32)) + ' and ' + IntToStr(high(Int32)));
WriteLn('Range of Int64 between ' + IntToStr(low(Int64)) + ' and ' + IntToStr(high(Int64)));
End.
以下是筆者在自己的電腦上試跑的結果:
Range of Cardinal between 0 and 4294967295
Range of Byte between 0 and 255
Range of Word between 0 and 65535
Range of LongWord between 0 and 4294967295
Range of Qword between 0 and 18446744073709551615
Range of Integer between -2147483648 and 2147483647
Range of ShortInt between -128 and 127
Range of SmallInt between -32768 and 32767
Range of LongInt between -2147483648 and 2147483647
Range of Int64 between -9223372036854775808 and 9223372036854775807
了解整數型態的範圍是重要的,因為電腦和數學還是有一段差距。用電腦進行運算時,會因超出範圍導致溢位 (overflow) 或下溢 (underflow)。Free Pascal 編譯器可開啟邊界檢查 (range check),在整數越界時引發錯誤。
如果需要更大範圍的整數時,就要使用大數運算函式庫。大數運算是以軟體模擬的,不受到硬體的限制,但速度較慢。
浮點數 (Floating Point Number)
浮點數型態用來表達數學上的小數。依據其資料寬度,可分為以下數種:
real:視系統而定single:32 位元double:64 位元extended:80 位元comp:64 位元currency:64 位元
一般情形下,用 real 型態即可。若需要特定精確度才改用其他型態。
以下程式可取得系統上的浮點數型態的寬度:
program main;
uses
SysUtils;
begin
WriteLn(Format('Size of real: %d', [SizeOf(real)]));
WriteLn(Format('Size of single: %d', [SizeOf(single)]));
WriteLn(Format('Size of double: %d', [SizeOf(double)]));
WriteLn(Format('Size of extended: %d', [SizeOf(extended)]));
WriteLn(Format('Size of comp: %d', [SizeOf(comp)]));
end.
以下是在筆者的主機上試跑的結果:
Size of real: 8
Size of single: 4
Size of double: 8
Size of extended: 8
Size of comp: 8
可發現其實好幾個浮點數型態的寬度是相同的。
字串 (String)
Pascal 的字串是字元陣列。又再細分為以下數種型態:
string:依模式而定ShortString:有長度限制,有頁碼 (code page)AnsiString:無長度限制,有頁碼RawByteString:無長度限制,無頁碼UTF8String:無長度限制,指定頁碼為 UTF8
string 是預設的字串型態,實際的型態會依模式而變。若在原始碼中加入編譯器指示詞 {$H+} 時,string 為 AnsiString。反之,在預設模式或加入指示詞 {$H-} 時,string 為 ShortString。
ShortString 是長度最長為 255 的字元陣列。但尾端沒有 null,和 C 字串相異。未指定頁碼時,該型態會自動使用系統預設的頁碼。
AnsiString 長度沒有限制,尾端有 null 字元,類似於 C 字串。未指定頁碼時,該型態也會自動使用系統預設的頁碼。
RawByteString 和 UTF8String 本質上仍是 AnsiString。這兩種型態等同於以下型態宣告:
type
RawByteString = AnsiString(CP_NONE);
UTF8String = AnsiString(CP_UTF8);
自訂範圍型態 (Subrange)
Pascal 可以自訂資料的範圍,像是以下的片段宣告 age 型態,該型態的範圍為 0 至 150:
type
age = 0..150;
自訂範圍型態的好處是讓編譯器幫我們檢查資料是否符合範圍,在開發時期提早發現錯誤。
列舉 (Enumeration)
列舉型態用來表達離散 (discrete)、有限數量的 (finite) 資料。這類資料在程式中當成符號來用,其內部數字不是重點。以下是實例:
type
direction = (NORTH, SOUTH, EASH, WEST);
在這個 Pascal 片段中,我們宣告了列舉型態 direction,該型態有四個值。
Pascal 的列舉型態是獨立的型態,和整數型態是相異的。也就是說,Pascal 的列舉型態具有型態安全 (type safety)。相對來說,C 語言的列舉型態無法和整數型態區分,不具有型態安全。
陣列 (Array)
陣列型態是線性 (linear)、連續 (continuous)、同質的 (homogeneous) 容器 (collection) 或資料結構 (data structure)。陣列中的元素在記憶體中會連續排列,可透過索引值 (index) 快速存取。
以下 Pascal 片段建立一個長度為 10 的一維陣列,該陣列的元素型態為整數,範圍從 1 至 10:
var
arr : array[1..10] of integer;
Pascal 陣列可以自訂範圍,不一定要從 1 開始。
除了一維陣列外,也可以建立多維陣列。以下 Pascal 片段建立二維陣列:
var
mtx : array[0..5, 0..3] of real;
除了以整數為索引值外,還可以用其他的型態。像是以下 Pascal 片段以列舉型態當成陣列的索引值:
type
direction = (NORTH, SOUTH, EASH, WEST);
var
directions : array[direction] of integer;
集合 (Set)
集合型態用來表達數學上的集合。以下的 Pascal 片段建立以 char (字元) 為元素的集合:
var
letters : set of char;
同樣地,我們可以用其他型態來建立集合。像是以下的片段建立以列舉為元素的集合:
type
direction = (NORTH, SOUTH, EASH, WEST);
var
directions : set of direction;
記錄 (Record)
記錄是使用者自訂型態,用來建立無法以純量表達的資料。例如,以下 Pascal 片段建立 TPoint 型態,用來表達平面座標的點 (point):
type
TPoint = record
x : real;
y : real;
end;
程式語言在開發時,無法預先知道所有的需求。讓程式語言的使用者 (即程式設計師) 有自訂型態的彈性是相當重要的。Pascal 算是早期就支援這項特性的語言。
物件 (Object) 和類別 (Class)
Object Pascal 限定
Pascal 在剛問世時,物件導向程式設計的概念還不興盛,所以當時 Pascal 未在語言中加入物件導向相關的特性。雖然我們可以用記錄寫擬物件,和真正的物件導向程式還是有一些差別。
物件和類別是為了讓 Pascal 支援物件導向程式才加進去的型態,兩者的差別是記憶體層級相異。實務上,物件甚少使用,幾乎都是使用類別來寫物件導向程式。
指標 (Pointer)
指標型態的資料用來儲存記憶體的位址 (address)。該型態是用來操作記憶體的語言特性。由於指標在現實生活中沒有相對應的概念,一開始要花一點時間來適應指標的使用方式。
不定型態 (Variant)
不定型態可用來儲存任意的純量或指標。在 Pascal 這類靜態型態語言中,不定型態讓程式更有彈性。但不定型態的資料處理起來會比靜態型態資料來得慢,這是因為 Pascal 程式要在運行期解析資料的真正型態。所以,不應過度使用不定型態。