DELPHI的編譯指令 {$IFDEF WIN32} -- 這可不是批註喔! 應用時機與場合 Delphi中有許許多多的Compiler Directives(編譯器指令)﹐這些編譯指令對於我們的程序發展有何影響呢? 它們又能幫我們什麼忙呢? Compiler Directive 對程序開發的影響與助益, 可以從以下幾個方向來討論: 協助除錯 穩健熟練的程序設計師經常會在開發應用系統的過程中﹐特別加入一些除錯程序或者回饋驗算的程序﹐這些除錯程序對於軟件品質的提升有極其正面的功能。然而開發完成的正式版本中如果不需要這些額外的程序的話﹐要想在一堆程序中找出哪些是除錯用的程序並加以刪除或設定爲批註﹐不僅累人﹐而且容易出錯﹐況且日後維護時這些除錯程序還用得着。 此時如果能夠應用像是$IFDEF的Compiler Directives ﹐就可以輕易的指示Delphi要/不要將某一段程序編進執行文件中。 版本分類 除了上述的除錯版本/正式版本的分類之外﹐對於像是「試用版」「普及版」「專業版」的版本分類﹐也可以經由Compiler Directive的使用﹐爲最後的產品設定不同的使用權限。其它諸如「中文版」「日文版」「國際標準版」等全球版本管理方面﹐同樣也可以視需要指示Delphi特別連結哪些資源檔或者是採用哪些適當的程序。以上的兩則例子中﹐各版本間只需共享同一份程序代碼即可。 Delphi 1.0 與 Delphi 2.0有許多不同之處﹐組件資源文件(.DCR)即是其中一例﹐兩者的檔案格式並不兼容﹐在您讀過本文之後﹐相信可以寫出這樣的程序﹐指示Delphi在不同的版本採用適當的資源文件以利於組件的安裝。 {$IFDEF WIN32} 程序的重用與管理 經過前文的討論後﹐相信你已經不難看出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的特別指示, 再加上其它的開關或參數內容, 最後以右大括號作爲指令的結束, 例如: 同時, 就如同Pascal的變量名稱與保留字一樣, Compiler Directives也是不區分大小寫的。 開關指令(Switch directives) {$A+} 開關型的編譯指令不一定要分行寫, 它們可以組合在同一個編譯指示的批註符號之間, 但必須以逗號連接, 而且中間不可以有空格, 例如: 光標停留在程序編輯器的任一位置時按下Ctrl+O O, 完整的Compiler Directives將會全部列於Unit的最上方。 參數指令(Parameter directives) 條件指令(Conditional directives) 以下是一個條件編譯的例子, 第一與第三列是寫給Compiler看的,指示 Compiler在 __DEBUG這個條件名稱完成定義的情況才編譯ShowMessage()這列程序;反之, 如果 __DEBUG 當時沒有定義的話, 這段程序幾乎與批註無異, Compiler對它將視而不見。 如何從IDE改變Compiler directives設定 從Delphi的IDE程序整合發展環境, 我們很方便的就可以修改各個compiler directives的設定, 方法是: 從Delphi IDE主選單: Project/Options/Compiler, 直接核選/取消各個CheckBox。值得注意的是, 改變一個項目的Compiler directives並不會影響其它的項目, 換言之, 各個項目都保有自己一套編譯指示。 假如您希望其它的項目也採用相同一套的Compiler directives, 在上述ProjectOptions對話盒的左下方有一個「Default」選項, 選取這個CheckBox之後, 雖然對於既有的項目沒有作用, 但未來新的項目都將可以採用這組設定作爲默認值。 透過Delphi的整合環境設定Compiler directives的確十分簡便, 但是許多情況下我們仍然需要將Compiler directive直接加到程序中。至少有兩個原因支持我們這麼作: 局部控制編譯條件 下列這段取自Online Help的程序範例, 即應用了{$I}編譯指令局部控制在發生I/O錯誤時不要舉發例外訊息, 這樣, 我們就可以編譯出一支在這段程序區域中不會產生I/O例外訊息的檔案偵測函數。 function FileExists(FileName: string): Boolean; 程序的可移植性 我們都可能會用到其它公司或個人創作的unit或component, 也可能分享程序給其它人, 換句話說, 單元或程序可能會在不同的機器上編譯, 直接將Compiler directives加入程序, 不僅可以免去程序使用前需要特別更改IDE的麻煩, 更重要的是解決了各個單元間要求不同編譯環境的歧異。 注意事項 Compiler directives的作用與影響範圍 如同變量的可見範圍與生命週期, 在我們使用 Compiler Directives 時也必須注意各個Compiler Directives 的作用範圍. 全域的 區域的 值得一提的是, 在程序中直接加入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); 在我們執行上述程序時, 在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喔! 不應該用來作爲程序執行流程控制 條件編譯的巢套最多可以16層 有些Compiler directives不應寫在Unit中 建議事項 打開全部的偵錯開關 $HINTS ON 此處有一個迷思有待澄清-「加入Dubug信息會不會讓執行文件變大變慢啊?」, 不一定。 對於們像是$D+, $L+, $HINTS ON這些開關, 打開後, Delphi在編譯時的確會額外加入一些除錯信息, 使得.DCU的檔案變大, 對於.EXE的檔案大小並沒有影響; 同時, 程序的執行速度也沒有改變, 還可以應用IDE的除錯器trace我們的程序, 值得應用。 對於像是$Q, $R等Compiler directive, 的確會影響執行文件的大小與速度, 然而這並不動搖我們在研發期間使用它們的決定, 請想想看, 值得爲這一點點的速度放棄程序的正確性嗎? 當然, 程序開發完成後, 正式出貨的版本, 可以關閉這兩個開關。 如果您寫好了一個組件, 而且只預備提供.DCU, 由於沒有.PAS可供Delphi IDE的Debugger追蹤程序, 除錯開關反而應該在組件脫手前關閉並重新編譯.DCU, 否則會引起使用者那邊找不到檔案的例外訊息。 善用{$I} 條件名稱請加入前導符 procedure TForm1.Button1Click(Sender: TObject); 以上的程序編譯與執行都沒有問題, 但條件名稱與變量名稱重複畢意容易讓人混淆, 因此, 假如能適當的爲編譯條件名稱之前加上諸如底線(_TEST), 程序會比較容易閱讀。 設定一致的編譯環境 個別指令說明 {$A+} 字段對齊 {$A+} ShowMessage在{$A+}時顯示的結果是:「8」; 倘若是{$A-}, 那所得的結果是「5」, 按理說, Byte應該只要一個byte就足夠了, 但是考慮到硬件的執行特性, 經過對齊後的record會有比較好的執行速度。 有關這個Compiler Directive要注意的事項是: 不管{$A}的開關是ON或OFF, 使用packed修飾過的記錄宣告, 是一定不會對齊的. 例如: MyRecord = packed record // 不會對齊的記錄宣告方式 {$APPTYPE GDI} 應用程序型態 在.DPR中加入{$APPTYPE CONSOLE} $APPTYPE不能應用在DLL的項目或單一的程序單元(Unit), 它只對.EXE有意義。而且只有寫在.DPR中才有作用。 請看以下的程序: if (Length(sCheckedDateString) <> 8) 假如sCheckedDateString的字符串內容是「85/12/241」(長度9)的話, 以上的if述句, 其實在第一個邏輯判斷時就已經知道結果了, 即使不看後來的邏輯運算結果也知道整個式子會是真值。 假如您希望對整個邏輯表達式進行完整的評估 -- 儘管結果已知, 後來的邏輯運算也不影響整個的結果時仍要全部評估過, 請將這個Compiler directives設爲{$B+}, 反之, 請設爲{$B-}, 系統的默認值是{$B-}。 {$D+} 除錯信息 {$DEFINE條件名稱} 定義條件名稱 經常, 我們會因爲除錯需要﹑區別不同版本等緣故, 希望選擇性的採用或排除某一段程序, 這個時候, 我們就可以先以$DEFINE定義好一個條件名稱(Conditional name), 然後配合{$IFDEF條件名稱}#{$ELSE}#{$ENDIF}指示編譯器按指定的 條件名稱之有無來選擇需要編譯的程序。 以下列的程序片斷來說: {$DEFINE _ProVersion} 編譯器將會選擇編譯上述A的那列程序, 日後, 如果我們需要編譯「簡易版」的程序版本時, 只要: 將{$DEFINE _ProVersion}那列整個刪掉。 使用$DEFINE時的其它注意事項如下: 以{$DEFINE}定義的條件名稱都是區域的。換句話說, 它的作用範圍只在當時所 在的單元纔有效, 即使定義在unit的interface, 由其它的unit以uses參考也沒有 效, 仍然只有在目前的unit有作用。 應用{$DESCRIPTION}可以指定加入一段文字到.EXE或.DLL表頭的模塊描述進入點 (module description entry)中﹐通常我們會用這個Compiler Directive加入應 用程序的名稱與版本編號到.EXE中。 例如: {$DESCRIPTION Dchat Version 1.0} {$X+} 擴充語法 這是爲了與之前的Pascal版本前向兼容的編譯指令, 雖然設定這個開關型的指令仍有作用, 但筆者建議您大可保留系統的默認值{$X+}, 在{$X+}下: 不需要非得準備一個變量接受函數的傳回值, 換句話說, 函數的傳回值可以舍 棄, 此時, 就可以像是呼叫程序一樣, 很方便的呼叫函數。 打關{$HINTS}開關後, Compiler會提示程序設計師注意以下的情況: 變量定義了卻沒有使用 由於程序簡單, 在兩個$HINTS中間的程序, 我們不難看出: for循環不會執行到, I變量也因此不曾用過 J := 3寫了等於白寫 但在程序越寫越長而日趨復雓時, 藉由{$HINTS ON}的協助, 比較容易察覺出程序 的毛病。 {$IFDEF} {$IFNDEF} 請參閱{$DEFINE}的說明, 在此補充說明{$IFNDEF}, 以下列程序來說, 即在指示 Compiler在_Test未定義時, 條件編譯ShowMessage()那列程序: {$IFNDEF _TEST} 換言之, {$IFNDEF}相當於{$IFDEF}的{$ELSE}部分。 {$IFOPT 開關} 到底{$B}是開着或關着呢? 如果我們想要指示Compiler按照某一個編譯開關當時的狀態作我們指定的事, 應該該怎麼做呢? 這時, {$IFOPT}就派得上用場了。例如: {$R+} 這個Compiler directive用來指示.EXE或.DLL加載時的預設地址。例如: {$IMAGEBASE $00400000}。如果指定加載的地址空間之前已經有其它模塊佔用了, Windows會爲.EXE重新配置一個新的加載地址。對於.DLL來說, 如果可以成功配置到我們寫在{$IMAGEBASE}的地址, 由於不需要重新配置內存地址, 不僅加載的速度 較快, 如果有其它程序也參照到這個DLL的話, 也可以減少加載時間與內存的消 耗。 使用這個Compiler directive時需要注意的事項有: 指定的敘述必須是一個大於$00010000的32位整數數值, 同時, 較低位置的16個位必須是零。 以Delphi IDE修改Compiler directives的確相當方便, 但往往我們仍然需要將Compiler directives直接加入程序中, 可是當我們這樣作之後不用多久, 就會發 現要一一重新修改各個單元中的這些Compiler directives時, 實在是既無聊而又 容易出錯的工作。這時候, 假如您一開始就採用{$I文件名稱}, 整件事就會變得很簡單。怎麼做呢? 讓我用一個例子告訴您 -- 先用一般的文書編輯器建好一個MySet.inc的普通文本文件, 內容爲: 在我們的程序中, 加入一列{$I MySet.inc}, 例如: interface implementation 基本動作會了之後, 讓我告訴你多一點有關{$I文件名稱}的事。 一旦應用了{$I文件名稱}, 幾乎等於Compiler在編譯時, 讓Compiler將這個檔 案的內容貼進我們的程序中的那個位置。 {$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} 經過{$L hasptpw.obj}宣告之後, 程序的其它部分就可以直接呼叫原先位於 hasptpw.obj中的hsap這個程序了。 {$L+} 區域符號信息 在{$L+}時, Delphi會額外加入一些區域符號信息, 這使得我們可以應用Delphi IDE中的View/Call Stack, View/Watch在程序執行時檢視變量內容與函式呼叫的 關係。 應用這個Compiler directive的注意事項有: {$D-}時, {$L+}不會有作用。 Delphi 2.0之後, 字符串多了一個更爲好用的長字符串, 不僅沒有數據長度255的限制與C語言慣用的Null-terminated string兼容性也大爲提高。 使用{$H}時的注意事項有: {$H+}的編譯情形下, 以string定義的字符串變量都是長字符串, 請注意, 字符串是否爲長字符串是在字符串定義時決定的, 例如: 由於var前{$H-}的緣故, 雖然在begin後我們立即設定爲{$H+}, 但s仍然是一個短 字符串, 所以, 自然不能像是長字符串一樣, 以pchar強制型別轉換後當作Null-terminated字符串使用。 承上, 不管程序是{$H+}或{$H-}, 只要字符串是以長字符串方式定義的, 即使begin..end;中改成{$H-}, 該字符串的操作仍然具有長字符串的特性。 不論{$H}的狀態如何, 以AnsiString定義的一定是長字符串; 以string[n]或ShortString定義的一定是短字符串。 要改變唯迭(Stack)內存配置大小時, 我們可以有以下兩種選擇: 使用{$MINSTACKSIZE數字}, {$MAXSTACKSIZE數字}, 分別指定最小.最大的Stack大小. 寫在.DPR中才有效果。 這個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+} const不是常數嗎? 爲什麼可以改呢? 在先前的Pascal版本中, 以const VarName: DataType = const value; 定義的具型態常數的確是可以改的, 假如您希望常數就是常數, 它不應該允許修改, 請將這個Compiler directive設爲{$J-} 不論是{$J+}或{$J-}, 以const VarName = const value; 定義的常數(沒有加上 型別宣告), 是一個真正的常數, 其它的程序不可以改變其內容。 其實{$J+}時還有一個妙用, 那就是宣告出類似C語言static的變量, 換句話說, 產生了一個與Application相同生命週期的變量。在這種情形下, 變量只在第一次 使用時纔會建立, 函數或程序結束時, 該變量也不會消滅, 下一次再呼叫到這個 函數或程序時, 我們仍然可以參考到上次執行結束時的值。讓我們試一下這個例子: {$J+} 第一次執行時, 我們分別會看到「0」「1」, 再點一次這個按鈕時, 看到的將是 「1」「2」。 |
DELPHI的編譯指令
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.