編譯指令與說明
作者: 錢達智(Wolfgang Chien) wrote on 1996.12.15-19
E-mail: wolfgang@ms2.hinet.net
WWW: http://www.aaa.hinet.net/delphi/~chien.htm
{$IFDEF WIN32} -- 這可不是註解喔!
對於Delphi來說﹐左右大括號之間的內容是註解﹐然而「{$」
(左括號後緊接著貨幣符號)對於Compiler(編譯器)而言並不是註解﹐
而是寫給Compiler看的特別指示。
應用時機與場合
Delphi中有許許多多的Compiler Directives(編譯器指令)﹐
這些編譯指令對於我們的程式發展有何影響呢? 它們又能幫我們什麼忙呢?
Compiler Directive 對程式開發的影響與助益, 可以從以下幾個方向來討論:
Ø 協助除錯
Ø 版本分類
Ø 程式的重用與管理
Ø 設定統一的執行環境
協助除錯
穩健熟練的程式設計師經常會在開發應用系統的過程中﹐特別加入
一些除錯程式或者回饋驗算的程序﹐這些除錯程式對於軟體品質的
提升有極其正面的功能。然而開發完成的正式版本中如果不需要這
些額外的程式的話﹐要想在一堆程式中找出哪些是除錯用的程式並加以
刪除或設定為註解﹐不僅累人﹐而且容易出錯﹐況且日後維護時這些除錯程式還用得著。
此時如果能夠應用像是$IFDEF的Compiler Directives ﹐就可以輕易的
指示Delphi要/不要將某一段程式編進執行檔中。
同時﹐Compiler本身也提供了一些錯誤檢查的開關﹐
可以預先對程式中可能的問題提醒程式設計師注意﹐同樣有助於撰寫正確的程式。
版本分類
除了上述的除錯版本/正式版本的分類之外﹐對於像是「試用版」「普及版」
「專業版」的版本分類﹐也可以經由Compiler Directive的使用﹐為最後的
產品設定不同的使用權限。其他諸如「中文版」「日文版」「國際標準版」
等全球版本管理方面﹐同樣也可以視需要指示Delphi特別連結哪些資源檔或
者是採用哪些適當的程式。以上的兩則例子中﹐各版本間只需共用同一份程式碼即可。
Delphi 1.0 與 Delphi 2.0有許多不同之處﹐元件資源檔(.DCR)即是其中
一例﹐兩者的檔案格式並不相容﹐在您讀過本文之後﹐相信可以寫出這樣的程式﹐
指示Delphi在不同的版本採用適當的資源檔以利於元件的安裝。
{$IFDEF WIN32}
{$R XXX32.DCR}
{$ELSE}
{$R XXXX16.DCR}
{$EDNIF}
程式的重用與管理
經過前文的討論後﹐相信你已經不難看出Compiler Directives在程式管理上的應用價值。
對於原始程式的重用與管理﹐也是Compiler Directives 使得上力的地方. 舉例來說:
Pascal-Style字串是Delphi 1.0與 Delphi 2.0之間的明顯差異﹐除了原先的短字串之外﹐
Delphi 2.0之後還多了更為方便使用的長字串﹐同時﹐系統也額外提供了像是 Trim()
這樣的字串處理函式。假如您有一個字串處理單元必須要同時應用於Delphi 1.0 與
2.0的專案時﹐編譯指示器可以幫你的忙。
此外﹐透過像是{$I xxxx} 這樣的 Compiler Directives﹐我們也可以適當的含入某些
程式, 同樣有助於切割組合我們的程式或編譯設定。
設定一致的執行環境
專案小組的成員間﹐必須有共同的環境設定﹐我很難預料一個小組成員間彼此有不同的
{$B}{$H}{$X}設定﹐最後子系統在併入主程式時會發生什麼事。
此外, 當您寫好一個元件或單元需要交予第三者使用時, 使用編譯指示器也可以保證元
件使用者與您有相同的編譯環境。
使用Compiler Directives
指令語法
Compiler Directives從外表看起來與註解頗為類似, 與註解不同的是:
Compiler Directives的語法格式都是以「{$」開始, 不空格緊接一個名
稱(或一個字母)表明給Compiler的特別指示, 再加上其他的開關或參數內容,
最後以右大括號作為指令的結束, 例如:
{$B+}
{$R-}
{$R MyCursor.res}
同時, 就如同Pascal的變數名稱與保留字一樣, Compiler Directives也是
不區分大小寫的。
從指令的語法格式來說Compiler Directives﹐可以進一步分類成以下三種格式:
Ø 開關指令(Switch directives)
這類指令都是單一字母以不空格的方式連接「+」或「-」符號; 或者是開關名
稱以一個空格後連接「ON」或「OFF」來表示作用/關閉某一個編譯指示開關。例如:
{$A+}
{$ALIGN ON}
開關型的編譯指令不一定要分行寫, 它們可以組合在同一個編譯指示的註解符號之間,
但必須以逗號連接, 而且中間不可以有空格, 例如:
{$B+,H+,T-,J+}
游標停留在程式編輯器的任一位置時按下Ctrl+O O, 完整的Compiler Directives
將會全部列於Unit的最上方。
Ø 參數指令(Parameter directives)
有些Compiler Directives需要在編譯名稱後面連接自定的參數(檔案名稱或指定的記
憶體大小), 例如: {$R MyCursor.res}, 即在指示Delphi在編譯連結時,
含入「MyCursor.res」這個資源檔。
Ø 條件指令(Conditional directives)
指示Compiler在編譯的過程中, 按我們設定的條件, 選擇性的採用/排除不同區域的
程式碼。
以下是一個條件編譯的例子, 第一與第三列是寫給Compiler看的,指示
Compiler在 __DEBUG這個條件名稱完成定義的情況才編譯ShowMessage()這列程式;
反之, 如果 __DEBUG 當時沒有定義的話, 這段程式幾乎與註解無異, Compiler對
它將視而不見。
{$IFDEF __DEBUG}
ShowMessage(IntToStr(i));
{$ENDIF}
如何從IDE改變Compiler directives設定
從Delphi的IDE程式整合發展環境, 我們很方便的就可以修改各個compiler directives的
設定, 方法是:
從Delphi IDE主選單: Project/Options/Compiler, 直接核選/取消各個CheckBox。
值得注意的是, 改變一個專案的Compiler directives並不會影響其他的專案, 換言之,
各個專案都保有自己一套編譯指示。
假如您希望其他的專案也採用相同一套的Compiler directives, 在上述Project Options
對話盒的左下方有一個「Default」選項, 選取這個CheckBox之後, 雖然對於既有的專案
沒有作用, 但未來新的專案都將可以採用這組設定作為預設值。
將Compiler directives寫入程式
透過Delphi的整合環境設定Compiler directives的確十分簡便, 但是許多情況下我們
仍然需要將Compiler directive直接加到程式中。至少有兩個原因支持我們這麼作:
Ø 局部控制編譯條件
在Project/Options/Compiler中所作的設定, 影響所及是整個專案, 如果某一段程式
要特別使用不同的編譯設定, 就必須直接將編譯指示加到程式中。
下列這段取自Online Help的程式範例, 即應用了{$I}編譯指令局部控制在發生I/O錯誤
時不要舉發例外訊息, 這樣, 我們就可以編譯出一支在這段程式區域中不會產生I/O例外
訊息的檔案偵測函數。
function FileExists(FileName: string): Boolean;
var
F: file;
begin
{$I-}
AssignFile(F, FileName);
FileMode := 0; ( Set file access to read only }
Reset(F);
CloseFile(F);
{$I+}
FileExists := (IOResult = 0) and (FileName <> '');
end; { FileExists }
Ø 程式的可攜性
我們都可能會用到其他公司或個人創作的unit或component, 也可能分享程式給其他人,
換句話說, 單元或程式可能會在不同的機器上編譯, 直接將Compiler directives加入
程式, 不僅可以免去程式使用前需要特別更改IDE的麻煩, 更重要的是解決了各個單元
間要求不同編譯環境的歧異。
注意事項
Compiler directives的作用與影響範圍
如同變數的可見範圍與生命週期, 在我們使用 Compiler Directives 時也必須注意各個
Compiler Directives 的作用範圍.
Compiler Directives的作用範圍可分為以下兩種:
Ø 全域的
全域的Compiler Directives, 影響所及是整個專案; 我們稍早前提到經由Delphi
IDE改變Compiler directives的方式就屬於全域的設定。
Ø 區域的
而區域的Compiler Directives 影響所及只從Compiler Directives 改變的那一行開始,
直到該程式單元(Unit)的結束或另一個相同的Compiler Directives 為止,
對其他的程式單元並沒有影響。
也就是說, 如果在unit中特別加入Compiler directives, Compiler會優先採用區域
的設定, 然後才是屬於專案層級的全域設定。
值得一提的是, 在程式中直接加入Compiler directives的最大作用範圍也只限於當時
那個單元而已, 對其他單元並沒有任何影響, 即使是以uses參考也是一樣。也就是說,
我們可以透過uses參考其他unit公開的變數與函式, 但是各個unit的編譯指令並不會互
相參考。
這項獨立的性質, 使得unit之間編譯環境的設定與關係變得十分簡潔, 例如Delphi
2.0的VCL都是在{$H+}的情況下編譯的, 因此, VCL中的字串都是以長字串的型態編譯
而成的, 有了這項編譯指令獨立的特性, 不論我們Prject中的設定為何, 這些在VCL中
定義過的字串都是長字串。我們的Project也不會因為uses了VCL中的unit而改變了自己
的設定。
因此, 在我們移交程式到網路上時, 大可以放心的在程式中加入必要的Compiler
directives, 別擔心, 即使別的unit以uses參考了我們的程式, 也不影響它自己原來
的設定。
如果我們自行以{$DEFINE _DEBUGVERSION}($DEFINE在稍後的個別指令介紹中將有說明)
定義了一個條件符號, 這個新的條件符號也是區域的, 換句話說, 它只從定義的那一
個單元的那一列之後才成立, 當然, 也只對目前這個單元有效.
由於自訂的條件符號只有區域的作用, 如果有好幾個程式單元都需要參考到某一個條
件符號, 怎麼辦呢? 嗯! 在每一個程式單元開頭處中都加上編譯指示是最直接的方式,
可是略嫌麻煩, 特別是編譯指示有變時, 要一一修正各個單元的設定內容, 很容易因為
疏忽而出錯。
比較簡易可行的作法是從Delphi IDE整合發展環境的主選單-Project / Options /
Directories/Conditional 的 Conditionals 中填入條件名稱。這樣, 相對於專案的
各個unit而言, 就有了一個全域的條件符號。
或者, 您也可以參考本文對於{$I}這個Compiler Directive的說明。 我在那裏指出了
另一個彈性的解決方式。
修改過編譯指令後, 建議Build All過一次程式
請試一試這個程式:
procedure TForm1.Button1Click(Sender: TObject);
begin
// ifopt是用來偵測某一個編譯開關的作用狀態
{$ifopt H+}
ShowMessage('H+');
{$else}
ShowMessage('H-');
{$endif}
end;
在我們執行上述程式時, 在Delphi預設的是$H+時, ShowMessage()會在畫面上會顯示
「H+」, 執行過後, 讓程式與form的內容與位置保留不變, 單純的從主選單:
Project/Options/Compiler, 將Huge Strings的核對方塊清除($H-),
然後按下F9執行, 咦! 怎麼還是看到「H+」?!
那是因為Delphi只會在unit內容經過異動後才會重新將.PAS編譯成.DCU,
在我們的例子中, 程式並沒有變動, .DCU當然也沒有重新產生, 最後.EXE的這個部分
自然也是沒什麼變化。
所以, 要解決這個問題, 只要以Delphi IDE主選單Project/Build All指示Delphi
重新編譯全部的程式即可。因此, 如果您從Delphi IDE修改過Compiler Directives後,
記得要Build All喔!
不應該用來作為程式執行流程控制
在程式中, 我們可以使用if敘述, 根據執行當時的情況控制程式執行時的流程,
但我們不可以用{$IFDEF}來作同樣的事, 為什麼? 從上述的說明, 相信您不難發現,
Compiler directives會對最後.EXE的內容發生直接的影響, 應用像是{$IFDEF}指示
Compiler的結果, 幾乎可以視同授權Compiler在編譯的那個時候自動選用/捨棄程式
到.DCU, .EXE中, 換句話說, 在程式編譯完成時, 會執行到那一段程式已成定局了,
我們自然不能用它來作程式流程的控制。
條件編譯的巢套最多可以16層
在使用{$IFDEF}…{$ENDIF}條件編譯我們的程式時, 一個{$IFDEF}中可以再包含另一
個{$IFDEF}, 但深度最多只能16層, 雖然是個限制, 但以正常的情形來說, 這應該已
經足夠了。
有些Compiler directives不應寫在Unit中
對於像是{$MINSTACKSIZE}{$MAXSTACKSIZE}管理堆疊大小, 或者像是{$APPTYE}
指示程式編譯成圖形/文字模式的Compiler directives, 只能寫在.DPR中, 寫在Unit
中是沒有效果的。
建議事項
確定您瞭解指令的影響
由於編譯指令的影響是如此直接與深遠, 在修改與應用某一個Compiler directive時,
請確定您已經了解其含意與影響。
打開全部的偵錯開關
Delphi有關偵錯的Compiler directives如下:
Ø $HINTS ON
Ø $D+
Ø $L+
Ø $Q+
Ø $R+
Ø $WARNINGS ON
各指令的用法您可以參閱本章稍後對個別指令的說明, 全部打開這些開關吧!
這樣不僅讓您可以使用Delphi IDE的除錯器, 對於程式編譯與執行過程中的問題,
Delphi也會適時的反應, 有助於寫作正確的程式。
此處有一個迷思有待澄清—「加入Dubug資訊會不會讓執行檔變大變慢啊?」, 不一定。
對於們像是$D+, $L+, $HINTS ON這些開關, 打開後, Delphi在編譯時的確會額外加入
一些除錯資訊, 使得.DCU的檔案變大, 對於.EXE的檔案大小並沒有影響; 同時,
程式的執行速度也沒有改變, 還可以應用IDE的除錯器trace我們的程式, 值得應用。
對於像是$Q, $R等Compiler directive, 的確會影響執行檔的大小與速度, 然而這並
不動搖我們在研發期間使用它們的決定, 請想想看, 值得為這一點點的速度放棄程式
的正確性嗎? 當然, 程式開發完成後, 正式出貨的版本, 可以關閉這兩個開關。
如果您寫好了一個元件, 而且只預備提供.DCU, 由於沒有.PAS可供Delphi IDE的Debugger
追蹤程式, 除錯開關反而應該在元件脫手前關閉並重新編譯.DCU, 否則會引起使用者那
邊找不到檔案的例外訊息。
善用{$I}
{$I FileName}是一個非常有用的Compiler directive.應用這個指令, 我們可以彈性的
管理Compiler directive的設定。
條件名稱請加入前置字元
不知道您有沒有這個疑問 -- 如果用{$DEFINE}定義的條件名稱與變數名稱相同時會發
生什麼事?
procedure TForm1.Button1Click(Sender: TObject);
var
TEST: integer;
begin
{$DEFINE TEST}
{$IFDEF TEST}
ShowMessage('Test');
{$ENDIF}
end;
以上的程式編譯與執行都沒有問題, 但條件名稱與變數名稱重覆畢意容易讓人混淆, 因此,
假如能適當的為編譯條件名稱之前加上諸如底線(_TEST), 程式會比較容易閱讀。
設定一致的編譯環境
在您了解了Compiler Directives之後, 請立即開始著手修改您IDE中有關編譯指示
的各個開關並且設為Default, 這樣, 日後您的專案乃至整個研發小組都將擁有
共同一致的編譯環境, 對於寫出來的程式會以何種方式編譯連結都了然於胸,
直接有助於子系統順利併入主系統中。
個別指令說明
有了之前對於Compiler directives的觀念之後, 接下來的這一節我將一一
介紹幾個常用的Compiler Directive的用法與注意事項, 您可以從這一
節中學到更多有關Compiler directives的知識與使用細節。
{$A+} 欄位對齊
在{$A+}(預設值)的情形下, 如果沒有使用 packed 修飾詞宣告的 record
型態, 其欄位會以CPU可以有效存取的方式向 1. 2. 4 等邊界對齊,
以獲取最佳的存取速度。以下列的程式示例來說:
{$A+}
type
MyRecord = record
ByteField: byte;
IntegerField: integer;
end;
…
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage(IntToStr(SizeOf(MyRecord)));
end;
ShowMessage在{$A+}時顯示的結果是:「8」; 倘若是{$A-}, 那所得的結果是「5」,
按理說, Byte應該只要一個byte就足夠了, 但是考慮到硬體的執行特性,
經過對齊後的record會有比較好的執行速度。
有關這個Compiler Directive要注意的事項是: 不管{$A}的開關是ON或OFF,
使用packed修飾過的記錄宣告, 是一定不會對齊的. 例如:
MyRecord = packed record // 不會對齊的記錄宣告方式
{$APPTYPE GDI} 應用程式型態
一般的情形下, Delphi會以{$APPTYPE GUI}的方式產生一個圖形的使用者介面程式,
如果您需要產生一個文字螢幕模式的程式, 那可以經由:
Ø 在.DPR中加入{$APPTYPE CONSOLE}
Ø 從主選單: Project/Options/Linker/EXE and DLL Options, 核取
「Generate Console Application」Check Box。
其他有關這個Compiler Directive的注意事項有:
Ø $APPTYPE不能應用在DLL的專案或單一的程式單元(Unit), 它只對.EXE有意義。
而且只有寫在.DPR中才有作用。
Ø 我們可以應用System程式單元中的IsConsole函數在程式執行時偵測應用程式
的類型。
Ø 參閱Object Pascal手冊第十三章可以知道更多有關Console Mode
Application的資訊。
{$B-} 布林評估
請看以下的程式:
if (Length(sCheckedDateString) <> 8)
or EmptyStr(sCheckedDateString)
or (sCheckedDateString = ' . . ')
or (sCheckedDateString = ' / / ') then
begin
Result := True;
Exit;
end;
假如sCheckedDateString的字串內容是「85/12/241」(長度9)的話, 以上的if述句,
其實在第一個邏輯判斷時就已經知道結果了, 即使不看後來的邏輯運算結果也知道
整個式子會是真值。
假如您希望對整個邏輯運算式進行完整的評估 -- 儘管結果已知, 後來的邏輯運算
也不影響整個的結果時仍要全部評估過, 請將這個Compiler directives設為{$B+},
反之, 請設為{$B-}, 系統的預設值是{$B-}。
{$D+} 除錯資訊
當程式以{$D+}(預設值)編譯時, 我們可以用Delphi整合發展境境的Debugger設定
中斷點, 也可以使用Trace Into或Trace Info追蹤程式的執行過程, 值得注意的是,
以{$D+}編譯的程式, 執行的速度並不會受到影響, 只不過編譯過的DCU的檔案長度會
加大, 但EXE檔的大小不變。
{$DEFINE條件名稱} 定義條件名稱
隨著您對Compiler Directives的瞭解與應用程度的加深, 您會發現這是一個非常實
用的編譯指示。
經常, 我們會因為除錯需要﹑區別不同版本等緣故, 希望選擇性的採用或排除某一
段程式, 這個時候, 我們就可以先以$DEFINE定義好一個條件名稱(Conditional name),
然後配合{$IFDEF條件名稱}…{$ELSE}…{$ENDIF}指示編譯器按指定的條件名稱之有無
來選擇需要編譯的程式。以下列的程式片斷來說:
{$DEFINE _ProVersion}
…
procedure TForm1.Button1Click(Sender: TObject);
begin
{$IFDEF _Proversion}
frmPrint.ShowModal; // A
{$ELSE}
ShowMessage('很抱歉, 試用版不提供列印功能');
{$ENDIF}
end;
編譯器將會選擇編譯上述A的那列程式, 日後, 如果我們需要編譯「簡易版」
的程式版本時, 只要:
Ø 將{$DEFINE _ProVersion}那列整個刪掉。
Ø 或者, 將{$DEFINE _ProVersion}改成{-$DEFINE _ProVersion},
讓它變成普通的註解
Ø 或者, 在{$DEFINE _ProVersion}的下一列加上{$UNDEF _ProVersion},
解除_ProVersion這個條件名稱的定義。
這樣, 由於_ProVersion這個條件名稱未定義的緣故, Compiler就只會選擇
{$ELSE}下的那段程式, 重新編譯一次, 不需費太多力氣, 很容易的就可以製作出
「簡易版」了, 省去了要同時維護兩份程式的麻煩。
使用$DEFINE時的其他注意事項如下:
Ø 以{$DEFINE}定義的條件名稱都是區域的。換句話說, 它的作用範圍只在
當時所在的單元才有效, 即使定義在unit的interface, 由其他的unit以uses參考也沒有效,
仍然只有在目前的unit有作用。
Ø 此外, 它的作用範圍是從定義起, 到unit結尾或者以{$UNDEF}解除為止。
Ø 如果程式單元中已經用{$DEFINE}定義了一個條件名稱, 而且也沒有用
{$UNDEF}解除定義, 重新{$DEFINE}一個同樣名稱並沒有作用, 換句話說, 它們是同一個.
Ø 假如需要一個全域的條件名稱, 您可以:主選單: Project / Options / Directories/
Conditional 的 Conditionals 中填入條件名稱。
Ø 以下的標準條件名稱, 是Delphi 2.0已經預先預備好的, 我們可以直接引用,
同時, 它們都是全域的, 任何Unit都可以參照得到。
Ø VER90: Delphi Object Pascal的版本編號。90表示9.0版, 日後若出現9.5
版時, 也會有VER95的定義。
Ø WIN32: 指出目前是在Win32(95, NT)作業環境
Ø CUP386: 採用386(含)以上的CPU時, 系統會提供本條件名稱。
Ø CONSOLE: 此符號會於應用程式是在螢幕模式下編譯時才定義。
{$DESCRIPTION 描述內容}
應用{$DESCRIPTION}可以指定加入一段文字到.EXE或.DLL表頭的模組描述進入點
(module description entry)中﹐通常我們會用這個Compiler Directive加入應
用程式的名稱與版本編號到.EXE中。例如:
{$DESCRIPTION Dchat Version 1.0}
{$X+} 擴充語法
這是為了與之前的Pascal版本前向相容的編譯指令, 雖然設定這個開關型的指令仍有
作用, 但筆者建議您大可保留系統的預設值{$X+}, 在{$X+}下:
Ø 不需要非得準備一個變數接受函數的傳回值, 換句話說, 函數的傳回值可以
捨棄, 此時, 就可以像是呼叫程序一樣, 很方便的呼叫函數。
Ø 支援Pchar型態與零基的字元陣列作為C語言以Null結尾的字串。
{$HINTS OFF} 提示訊息
打關{$HINTS}開關後, Compiler會提示程式設計師注意以下的情況:
Ø 變數定義了卻沒有使用
Ø 程式流程中不會執行的for或while迴圈
Ø 只有存入沒有取用的指定敘述。意思是說, 指定資料到某一個變數之後,
卻沒有任何的程式參考取用這個變數值。
{$HINTS ON}
procedure MyTest;
const _False = False;
var
I, J: integer;
begin
if _False then
for I := 1 to 3 do ;
J := 3;
end;
{$HINTS OFF}
由於程式簡單, 在兩個$HINTS中間的程式, 我們不難看出:
Ø for迴圈不會執行到, I變數也因此不曾用過
Ø J := 3寫了等於白寫
但在程式越寫越長而日趨複雓時, 藉由{$HINTS ON}的協助, 比較容易察覺出程式的毛病。
{$IFDEF} {$IFNDEF}
請參閱{$DEFINE}的說明, 在此補充說明{$IFNDEF}, 以下列程式來說, 即在指示Compiler
在_Test未定義時, 條件編譯ShowMessage()那列程式:
{$IFNDEF _TEST}
ShowMessage('_TEST not define');
{$ENDIF}
換言之, {$IFNDEF}相當於{$IFDEF}的{$ELSE}部分。
{$IFOPT 開關}
到底{$B}是開著或關著呢? 如果我們想要指示Compiler按照某一個編譯開關當時的狀態作
我們指定的事, 應該該怎麼做呢? 這時, {$IFOPT}就派得上用場了。例如:
{$R+}
{$Q-} // 特別指定為Q-
{$IFOPT R+} // 如果 Range Check 是開啟的話
ShowMessage('程式是在 Range Check 開啟狀態下編譯的');
// 這個 Q+ 也會在 IFOPT R+ 成立時才通知 Compiler
{$Q+}
{$ENDIF}
{$IFOPT Q+}
ShowMessage('Q 也變成開啟狀態了');
{$ENDIF}
ShowMessage() 與 {$Q+}會在$R+ 的情形下才編譯, 因此, 雖然我們事前特別指示為{
$Q-}, 第二個的ShowMessage()在程式執行時也可以看到「Q 也變成開啟狀態了」。
{$IMAGEBASE檔案基礎位址}
這個Compiler directive用來指示.EXE或.DLL載入時的預設位址。例如: {$IMAGEBASE
$00400000}。如果指定載入的位址空間之前已經有其他模組佔用了, Windows會為.EXE重
新配置一個新的載入位址。對於.DLL來說, 如果可以成功配置到我們寫在{$IMAGEBASE}
的位址, 由於不需要重新配置記憶體位址, 不僅載入的速度較快, 如果有其他程式也參
照到這個DLL的話, 也可以減少載入時間與記憶體的消耗。
使用這個Compiler directive時需要注意的事項有:
Ø 指定的敘述必須是一個大於$00010000的32位元整數數值, 同時, 較低位置的
16個位元必須是零。
Ø DLL的建議位址範圍從$40000000到$7FFFFFFF, 該範圍的位址可以同時適用於
Windows 95與Windows NT。
{$I檔案名稱} 含入檔案
以Delphi IDE修改Compiler directives的確相當方便, 但往往我們仍然需要將Compiler
directives直接加入程式中, 可是當我們這樣作之後不用多久, 就會發現要一一重新
修改各個單元中的這些Compiler directives時, 實在是既無聊而又容易出錯的工作。
這時候, 假如您一開始就採用{$I檔案名稱}, 整件事就會變得很簡單。怎麼做呢?
讓我用一個例子告訴您 --
Ø 先用一般的文書編輯器建好一個MySet.inc的普通文字檔案, 內容為:
{$H+}
{$DEFINE _Proversion}
Ø 在我們的程式中, 加入一列{$I MySet.inc}, 例如:
unit Unit1;
{$I MySet.inc}
interface
…
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
{$IFDEF _ProVersion}
ShowMessage('專業版');
{$Else}
ShowMessage('只有專業版才有此功能');
{$ENDIF}
end;
…
這是副程式的觀念嘛! 沒錯, 就是這麼簡單而已, 以後如果有任何變化, 修改MySet.INC,
然後Project/Buile All即可, 實在是夠簡單的了。
基本動作會了之後, 讓我告訴你多一點有關{$I檔案名稱}的事。
Ø 一旦應用了{$I檔案名稱}, 幾乎等於Compiler在編譯時, 讓Compiler將這個檔
案的內容貼進我們的程式中的那個位置。
Ø 如果沒有註明副檔名, Delphi預設這個檔案是.PAS。
Ø 如果在專案的目錄中找不到這個檔案的話, Delphi會陸續搜尋Tools/Options
/Library中的Library Path中的目錄。
另外, 當您寫作了一個DLL, 使用者在使用其中的函數前必須宣告過, 如果能夠一併提
供這些函數的宣告檔, 使用者只要一行{$I xxx}
即可, 是不是很方便呢?
{$I+} EInOutError檢查
在{$I+}(系統預設值)狀態編譯的程式, 一旦發生I/O錯誤時, 將會舉發一個EInOutError
的例外, 假如我們在特定的情況下不希望出現這個例外的訊息時(例如前文提到的偵測檔
案是否存在函數), 可以將這個Compiler directive設為{$I-}, 此時, 程式執行時是否發
生過錯誤,程式設定師必須自行檢查IOResult這個公用變數的值, 如果是零, 表示沒有錯誤,
非零的錯誤代碼含意請詳查Online help。
{$L檔案名稱} 連結目標檔
如果您有一個.OBJ檔要併入Delphi的程式時, 可以在程式中加入:
{$L OTHER.OBJ}
這樣, 就可以使用OTHER.OBJ中的程式了, 值得注意的是, 函數或程序在呼叫前,
仍然必須用external宣告過, 表明這些模組是來自「外部」的函式。
舉例來說, 筆者有一份由Keypro廠商提供的.OBJ檔, 在使用時, 相關的程式如下:
…
{$L hasptpw.obj}
{$F+}
procedure hasp (Service, SeedCode, LptNum, Pass1, Pass2 : word;
var p1,p2,p3,p4 : word); external;
{$F-}
…
經過{$L hasptpw.obj}宣告之後, 程式的其他部分就可以直接呼叫原先位於 hasptpw.obj
中的hsap這個程序了。
{$L+} 區域符號資訊
在{$L+}時, Delphi會額外加入一些區域符號資訊, 這使得我們可以應用Delphi IDE中的
View/Call Stack, View/Watch在程式執行時檢視變數內容與函式呼叫的關係。
應用這個Compiler directive的注意事項有:
Ø {$D-}時, {$L+}不會有作用。
Ø 使用{$L+}, 只會加大.DCU的檔案大小, 對.EXE的大小與執行速度並沒有影響。
{$H+} 長字串宣告
Delphi 2.0之後, 字串多了一個更為好用的長字串, 不僅沒有資料長度255的限制,
與C語言慣用的Null-terminated string相容性也大為提高。
使用{$H}時的注意事項有:
Ø {$H+}的編譯情形下, 以string定義的字串變數都是長字串, 請注意,
字串是否為長字串是在字串定義時決定的, 例如:
procedure TForm1.Button1Click(Sender: TObject);
{$H-}
var
s: string;
begin
{$H+}
s := '測試一下長字串';
Windows.MessageBox(0, pchar(s), '訊息', 64);
end;
由於var前{$H-}的緣故, 雖然在begin後我們立即設定為{$H+}, 但s仍然是一個短字串,
所以, 自然不能像是長字串一樣, 以pchar強制型別轉換後當作Null-terminated字串使
用。
Ø 承上, 不管程式是{$H+}或{$H-}, 只要字串是以長字串方式定義的, 即使
begin..end;中改成{$H-}, 該字串的操作仍然具有長字串的特性。
因此, 由於VCL中的字串都是長字串, 即使我們的程式是{$H-}, 仍然可以拿它們當長
字串來使用。
Ø 不論{$H}的狀態如何, 以AnsiString定義的一定是長字串; 以string[n]或
ShortString定義的一定是短字串。
{$M 16386, 1048576} 記憶體配置大小
要改變唯疊(Stack)記憶體配置大小時, 我們可以有以下兩種選擇:
Ø 使用{$MINSTACKSIZE數字}, {$MAXSTACKSIZE數字}, 分別指定最小.最大的Stack
大小.
Ø 或者使用{$M min, max}, 同時指定最小與最大的值。
使用這些Compiler directive時的注意事項有:
Ø 寫在.DPR中才有效果。
Ø 堆疊的最小數字必須介於1024至21474835647之間。
Ø 堆疊的最大數字必須介於$MINSTACKSIZE至21474835647之間。
Ø 當記憶體不足而無法滿足最小的堆疊大小時, Windows會在啟動這程式時
提出錯誤報告。
Ø 當程式要求的記憶體超過$MINSTACKSIZE的大小時, 將舉發EStackOverflow例外。
{$Z1} 最小列舉大小
這個Compiler directive將影響儲存列舉型態時最小所需的byte數值。如果宣告列舉型
態時, 數值不大於256, 而且也在系統預設的{$Z1}時, 這個列舉型態只佔用一個byte儲
存的。{$Z2}時, 以兩個byte儲存, {$Z4}時, 以四個byte儲存。因為C語言通常以WORD或
DWORD儲存列舉型態, 如果您的程式需要與C、C++溝通時,{$Z2}{$Z4}就很管用了
{$Z+}, 與{$Z-}分別對應到{$Z1}和{$Z4}。
{$P+} 開放字串參數
在程序與函數宣告時, 其中的字串引數, 在{$P+}時表示是Open string; {$P-}時,
只是一般的字串變數而已。這個Compiler directive只在{$H-}時有作用。
{$O+} 最佳化開關
建議您維持{$O+}的系統預設值。開啟這個Compiler directive, Delphi會自動進行
最佳化處理, 程式可以因此跑得快一些, 您可以放心的打開這個編譯開關, Delphi
不會進行不安全的最佳化而使您的程式執行時發生錯誤。
{$Q-} 滿溢檢查, {$R-} 範圍檢查
{$Q}與{$R}是一組搭配使用的Compiler directive, 它們將檢查數值或陣列的
操作是否在安全的邊界中, {$Q}會檢查整數運算(如+, -, Abs, Sqr, Pred,
Succ等), 而{$R}則檢查字串與陣列的存取是否超出合理邊界範圍等問題。
使用這兩個Compiler directives會因為這些檢查動作而降低程式執行的速度,
通常我們會在除錯時開啟這兩個編譯開關。
{$U-} Pentium CPU浮點運算安全檢查
還記得早期Pentium CPU浮點運算不正確的事吧? 這批CPU應該回收得差不多了,
但如果您仍然不確定程式會不會意外的遇到漏網之魚或黑心牌經銷商的話, 請將這個
Compiler directives設為{$U+}。
根據Borland手冊的說明, 如果CPU是沒有暇疵的, 設定{$U+}對於執行速度只有輕微
的影響; 但如果是問題CPU, 浮點的除法速度會因此慢上三倍, 是否要打開這個開關,
您心中應該已有取捨。
{$R檔案名稱} 資源檔
在您還沒有開始學習Compiler directives之前, 這個指令就已經出現在您的程式中
了,每次開出一個新的form時, Delphi自動在Implement開頭部分中加入{$R *.DFM},
在Project/Source中看到的.DPR程式中也有{$R *.RES}, 這些是什麼意思呢? 意思是說,
在編譯連結時, 含入與專案主檔名同名的.RES, 以及與form unit檔案同名的.DFM等資源檔。
如果您需要在程式中使用額外的資源(例如: 自訂滑鼠指標), 請注意不要自行以Resouse
WorkShop或Image Editor等資源編輯器更改這些與Project或Form同名的資源檔,
改變這些同名的檔案不僅無效, 可能還有不可預期的錯誤。因些,
您應該在另外一個資源檔中存放這些資源, 並於{$R}中寫明檔案的名稱將其連結進來, 例如:
{$R MyCursor.res}
{$T-} @指標型態檢查
應用@運算元可以取得變數的位址, 在{$T-}時, 以@取得是一個無型別的指標(Pointer)。
反過來說, 在{$T+}時, 是有型別的指標, 假定I是一個integer的變數, @I所得到的即是
相當於^Integer(Pointer of Integer)的指標。
{$WARNINGS ON} 編譯器警告
這個Compiler directive與{$HINTS}的作用類似, 同樣會對程式的可能問題提出警告。
不同的是, 在{$WARNINGS ON}時, Compiler會對未初始化的變數、沒有傳回值的函數、
建構抽象物件等情況提出警告。
{$J-} 型態常數唯讀
從前筆者曾經對以下的程式產生過疑惑:
{$J+}
procedure TForm1.Button1Click(Sender: TObject);
const
VarConst: integer = 4;
begin
VarConst := 5;
ShowMessage(IntToStr(VarConst));
end;
const不是常數嗎? 為什麼可以改呢? 在先前的Pascal版本中, 以
const VarName: DataType = const value;
定義的具型態常數的確是可以改的, 假如您希望常數就是常數, 它不應該允許修改,
請將這個Compiler directive設為{$J-}
不論是{$J+}或{$J-}, 以const VarName = const value; 定義的常數(
沒有加上型別宣告), 是一個真正的常數, 其他的程式不可以改變其內容。
其實{$J+}時還有一個妙用, 那就是宣告出類似C語言static的變數, 換句話說,
產生了一個與Application相同生命週期的變數。在這種情形下, 變數只在第一
次使用時才會建立, 函數或程序結束時, 該變數也不會消滅, 下一次再呼叫到這個函數
或程序時, 我們仍然可以參考到上次執行結束時的值。讓我們試一下這個例子:
{$J+}
procedure TForm1.Button1Click(Sender: TObject);
const
i: integer = 0;
begin
ShowMessage(IntToStr(i));
Inc(i);
ShowMessage(IntToStr(i));
end;
第一次執行時, 我們分別會看到「0」「1」, 再點一次這個按鈕時, 看到的將是「1」「2」。