編譯指令與說明
作者: 錢達智(Wolfgang Chien)  wrote on 1996.12.15-19
E-mail: [email protected]
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 }
&Oslash; 程式的可攜性
我們都可能會用到其他公司或個人創作的unit或component, 也可能分享程式給其他人, 
換句話說, 單元或程式可能會在不同的機器上編譯, 直接將Compiler directives加入
程式, 不僅可以免去程式使用前需要特別更改IDE的麻煩, 更重要的是解決了各個單元
間要求不同編譯環境的歧異。
注意事項
Compiler directives的作用與影響範圍
如同變數的可見範圍與生命週期, 在我們使用 Compiler Directives 時也必須注意各個
Compiler Directives 的作用範圍.
Compiler Directives的作用範圍可分為以下兩種:
&Oslash; 全域的
全域的Compiler Directives, 影響所及是整個專案; 我們稍早前提到經由Delphi 
IDE改變Compiler directives的方式就屬於全域的設定。
&Oslash; 區域的
而區域的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而改變了自己
的設定。

解决方案 »

  1.   

    因此, 在我們移交程式到網路上時, 大可以放心的在程式中加入必要的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如下:
    &Oslash; $HINTS ON
    &Oslash; $D+
    &Oslash; $L+
    &Oslash; $Q+
    &Oslash; $R+
    &Oslash; $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}的方式產生一個圖形的使用者介面程式, 
    如果您需要產生一個文字螢幕模式的程式, 那可以經由:
    &Oslash; 在.DPR中加入{$APPTYPE CONSOLE}
    &Oslash; 從主選單: Project/Options/Linker/EXE and DLL Options, 核取
    「Generate Console Application」Check Box。
    其他有關這個Compiler Directive的注意事項有:
    &Oslash; $APPTYPE不能應用在DLL的專案或單一的程式單元(Unit), 它只對.EXE有意義。
    而且只有寫在.DPR中才有作用。
    &Oslash; 我們可以應用System程式單元中的IsConsole函數在程式執行時偵測應用程式
    的類型。
    &Oslash; 參閱Object Pascal手冊第十三章可以知道更多有關Console Mode 
    Application的資訊。
      

  2.   

    {$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的那列程式, 日後, 如果我們需要編譯「簡易版」
    的程式版本時, 只要:
    &Oslash; 將{$DEFINE _ProVersion}那列整個刪掉。
    &Oslash; 或者, 將{$DEFINE _ProVersion}改成{-$DEFINE _ProVersion}, 
    讓它變成普通的註解
    &Oslash; 或者, 在{$DEFINE _ProVersion}的下一列加上{$UNDEF _ProVersion}, 
    解除_ProVersion這個條件名稱的定義。
    這樣, 由於_ProVersion這個條件名稱未定義的緣故, Compiler就只會選擇
    {$ELSE}下的那段程式, 重新編譯一次, 不需費太多力氣, 很容易的就可以製作出
    「簡易版」了, 省去了要同時維護兩份程式的麻煩。
    使用$DEFINE時的其他注意事項如下:
    &Oslash; 以{$DEFINE}定義的條件名稱都是區域的。換句話說, 它的作用範圍只在
    當時所在的單元才有效, 即使定義在unit的interface, 由其他的unit以uses參考也沒有效, 
    仍然只有在目前的unit有作用。
    &Oslash; 此外, 它的作用範圍是從定義起, 到unit結尾或者以{$UNDEF}解除為止。
    &Oslash; 如果程式單元中已經用{$DEFINE}定義了一個條件名稱, 而且也沒有用
    {$UNDEF}解除定義, 重新{$DEFINE}一個同樣名稱並沒有作用, 換句話說, 它們是同一個.
    &Oslash; 假如需要一個全域的條件名稱, 您可以:主選單: Project / Options / Directories/
    Conditional 的 Conditionals 中填入條件名稱。
    &Oslash; 以下的標準條件名稱, 是Delphi 2.0已經預先預備好的, 我們可以直接引用, 
    同時, 它們都是全域的, 任何Unit都可以參照得到。
    &Oslash; VER90: Delphi Object Pascal的版本編號。90表示9.0版, 日後若出現9.5
    版時, 也會有VER95的定義。
    &Oslash; WIN32: 指出目前是在Win32(95, NT)作業環境
    &Oslash; CUP386: 採用386(含)以上的CPU時, 系統會提供本條件名稱。
    &Oslash; CONSOLE: 此符號會於應用程式是在螢幕模式下編譯時才定義。