作者:思多雅[天行健] 2008-09-25釋出
在本專題中,我們将一起對C#的編譯、方法及預處理進行探讨。
一、C#的編譯
在談及C#的編譯之前,我們了解一點:計算機不能直接了解進階語言,隻能直接了解機器語言,是以必須要把進階語言翻譯成機器語言,計算機才能執行進階語言編寫的程式。翻譯的方式有兩種,一個是編譯,一個是解釋。
兩種方式隻是翻譯的時間不同。編譯型語言寫的程式執行之前,需要一個專門的編譯過程,把程式編譯成為機器語言的檔案,比如exe(com)檔案,以後要運作的話就不用重新翻譯了,直接使用編譯的結果就行了(exe檔案)。
C#的編譯階段如下:
C#的編譯程式把一個C#源程式翻譯成目标程式的工作過程分為五個階段:1、詞法分析;2、文法分析;3、中間代碼生成;4、代碼優化;5、目标代碼生成。主要是進行詞法分析和文法分析,又稱為源程式分析,分析過程中發現有文法錯誤,給出提示資訊。
1、詞法分析
詞法分析的任務是對由字元組成的單詞進行處理,從左至右逐個字元地對源程式進行掃描,産生一個個的單詞符号,把作為字元串的源程式改造成為單詞符号串的中間程式。執行詞法分析的程式稱為詞法分析程式或掃描器。
源程式中的單詞符号經掃描器分析,一般産生二進制式:單詞種别;單詞自身的值。單詞種别通常用整數編碼,如果一個種别隻含一個單詞符号,那麼對這個單詞符号,種别編碼就完全代表它自身的值了。若一個種别含有許多個單詞符号,那麼,對于它的每個單詞符号,除了給出種别編碼以外,還應給出自身的值。
詞法分析器一般來說有兩種方法構造:手工構造和自動生成。手工構造可使用狀态圖進行工作,自動生成使用确定的有限自動機來實作。
2、文法分析
編譯程式的文法分析器以單詞符号作為輸入,分析單詞符号串是否形成符合文法規則的文法機關,如表達式、指派、循環等,最後看是否構成一個符合要求的程式,按該語言使用的文法規則分析檢查每條語句是否有正确的邏輯結構,程式是最終的一個文法機關。編譯程式的文法規則可用上下文無關文法來刻畫。
文法分析的方法分為兩種:自上而下分析法和自下而上分析法。自上而下就是從文法的開始符号出發,向下推導,推出句子。而自下而上分析法采用的是移進歸約法,基本思想是:用一個寄存符号的先進後出棧,把輸入符号一個一個地移進棧裡,當棧頂形成某個産生式的一個候選式時,即把棧頂的這一部分歸約成該産生式的左鄰符号。
3、中間代碼生成
中間代碼是源程式的一種内部表示,或稱中間語言。中間代碼的作用是可使編譯程式的結構在邏輯上更為簡單明确,特别是可使目标代碼的優化比較容易實作。中間代碼即為中間語言程式,中間語言的複雜性介于源程式語言和機器語言之間。中間語言有多種形式,常見的有逆波蘭記号、四元式、三元式和樹。
4、代碼優化
代碼優化是指對程式進行多種等價變換,使得從變換後的程式出發,能生成更有效的目标代碼。所謂等價,是指不改變程式的運作結果。所謂有效,主要指目标代碼運作時間較短,以及占用的存儲空間較小。這種變換稱為優化。
有兩類優化:一類是對文法分析後的中間代碼進行優化,它不依賴于具體的計算機;另一類是在生成目标代碼時進行的,它在很大程度上依賴于具體的計算機。對于前一類優化,根據它所涉及的程式範圍可分為局部優化、循環優化和全局優化三個不同的級别。
5、目标代碼生成
目标代碼生成是編譯的最後一個階段。目标代碼生成器把文法分析後或優化後的中間代碼變換成目标代碼。
目标代碼生成階段應考慮直接影響到目标代碼速度的三個問題:一是如何生成較短的目标代碼;二是如何充分利用計算機中的寄存器,減少目标代碼通路存儲單元的次數;三是如何充分利用計算機指令系統的特點,以提高目标代碼的品質。
6、表格管理程式
編譯過程中源程式的各種資訊被保留在種種不同的表格,編譯各階段的工作都涉及到構造、查找、或更新有關的表格。
7、出錯處理程式
如果編譯過程中發現源程式有錯誤,編譯程式應報告錯誤的性質和錯誤的發生的地點,并且将錯誤所造成的影響限制在盡可能小的範圍内,使得源程式的其餘部分能繼續被編譯下去。
這也是一般的編譯語言的編譯過程,但要注意的是C#有一個特殊的地方,那就是C#可以先編譯成TL檔案,将代碼編譯成中間代碼(IL)既不是源程式也不是cpu指令,程式運作時JIT将IL翻譯成本地cpu指令再執行,由于編譯的是中間語言,是以速度比一般的解釋性語言要快。
JAVA也有這個特性,java程式也需要編譯,但是沒有直接編譯稱為機器語言,而是編譯稱為位元組碼,然後用解釋方式執行位元組碼。
是以,也有學者把認為這二個語言既不是傳統的編譯語言,也不是傳統解釋語言。
-------思多雅[天行健]版權所有,首發太平洋論論壇,轉載請注明-------
二、C#的文法符号
一個C#程式由一個或多個源檔案組成。一個源檔案是一個統一字元編碼的字元的有序序列。源檔案通常和檔案系統中的檔案有一一對應關系,但是這個對應關系并不需要。
C#的詞彙和句子的文法散布在整個文章中。詞彙文法定義如能把字元組合為形式标記;句子的文法定義了如何把标記組合為C#程式。
文法生成包括無詞尾符号和有詞尾符号。在文法生成當中,無詞尾符号用意大利體表示,而有詞尾符号用定寬字型。每一個無詞尾符号定義為一系列産品(production)。這一系列産品的第一行是無詞尾符号的名稱,接下來是一個冒号。對于一個産品,每個連續的鋸齒狀的行的右手邊同左手邊類似是無詞尾符号。
例子:
nonsense:
terminal1
terminal2
定義了一個名為nonsense 的無詞尾符号,有兩個産品,一個在右手邊是terminal1,一個在左手邊是 terminal2。
選項通常列為單獨的一行,雖然有時有很多選項,短語“one of”會在選項前面。這裡有一個對把每個選項都列在單獨一行的簡單縮寫的方法。
例子
letter: one of
A B C a b c
簡寫為:
letter: one of
A
B
C
a
b
c
如identifier opt ,一個寫在下方的字首 “opt”用來作為簡寫來指明一個可選的符号。例子
whole:
first-part second-partopt last-par t
是下面的縮寫:
whole:
fir st-part last-part
fir st-part second-part last-part
-------思多雅[天行健]版權所有,首發太平洋論論壇,轉載請注明-------
三、C#的預處理
預處理階段是一個文本到文本的轉換階段,在預處理過程中,使能進行代碼的條件包含和排除。
pp-un it:
pp-gro up opt
pp-gro up:
pp-gro up-part
pp-gro up pp-group-part
pp-gro up-part:
pp-tokensopt new-line
pp-de claration
pp-if -section
pp-con trol-line
pp-l ine-number
pp-tokens:
pp-token
pp-tokens pp-token
pp-token:
identifi er
keyword
literal
operator-or-punctuator
new-line:
The carriage return character (U+000D)
The line feed character (U+000A)
The carriage return character followed by a line feed character
The line separator character (U+2028)
The paragraph separator character (U+2029)
1、預處理聲明
在預處理過程中,為了使用名稱可以被定義和取消定義。#define 定義一個辨別符。#undef “反定義”一個辨別符,如果一個辨別符在以前已經被定義了,那麼它就變成了不明确的。如果一個辨別符已經被定義了,它的語意就等同于true ;如果一個辨別符沒有意義,那麼它的語意等同于false。
pp-de claration:
#define pp-identifier
#undef pp-identifier
來看看這個例子:
#define A
#undef B
class C
{
#if A
void F()
#else
void G()
#endif
#if B
void H()
#else
void I()
#endif
}
變為:
class C
{
void F()
void I()
}
如果有一個pp-unit, 聲明就必須用pp- token 元素進行。換句話說,#define 和#undef 必須在檔案中任何 “真正代碼”前聲明,否則在編譯時會發生錯誤。是以,也許會像下面的例子一樣散布#if 和#define:
#define A
#if A
#define B
#endif
namespace N
{
#if B
class Class1
#endif
}
因為#define 放在了真實代碼後面,是以下面的例子是非法的:
#define A
namespace N
{
#define B
#if B
class Class1
#endif
}
一個#undef 也許會“反定義”一個沒有定義的名稱。下面的例子中定義了一個名字并且對它進行了兩次反定義,第二個#undef 沒有效果,但還是合法的。
#define A
#undef A
#undef A
2、 #if, #elif, #else, #endif
pp-if -section 用來對程式文本的一部件進行有條件地包括和排除。
pp-if -section:
pp-if -group pp-elif-groupsopt pp-else-groupopt pp-endif-line
pp-if -group:
#if pp-expression new-line pp -group opt
pp-e lif -groups
pp-e lif -group
pp-e lif -groups pp-elif-group
pp-e lif -group:
#elif pp-expression new-line group opt
pp-e lse-group:
#else new-line group opt
pp-end if -line
#endif new-line
舉個例子:
#define Debug
class Class1
{
#if Debug
void Trace(string s)
#endif
}
變成:
class Class1
{
void Trace(string s)
}
如果這部分可以嵌套。
來看看例子:
#define Debug // Debugging on
#undef Trace // Tracing off
class PurchaseTransaction
{
void Commit() {
#if Debug
CheckConsistency();
#if Trace
WriteToLog(this.ToString());
#endif
#endif
CommitHelper();
}
}
3、預處理控制行
特性#error和#warning使得代碼可以把警告和錯誤的條件報告給編譯程式,來查出标準的編譯時的警告和錯誤。
pp-con trol-line:
#error pp-message
#warning pp-message
pp-message:
pp-tokensopt
舉個例子幫助大家了解
#warning Code review needed before check-in
#define DEBUG
#if DEBUG && RETAIL
#error A build can't be both debug and retail!
#endif
class Class1
{…}
這将總是産生警告(“Code review needed before check-in"),并且如果予處理修飾符DEBUG 和RETAIL 都被定義,還會産生錯誤。
4、 #line
#line 的特點使得開發者可以改變行的數量和編譯器輸出時使用的源檔案名稱,例如警告和錯誤。如果沒有行訓示符,那麼行的數量和檔案名稱就會自動由編譯器定義。#line訓示符通常用于程式設計後的工具,它從其它文本輸入産生C#源代碼。
pp-l ine-number:
#line integer-literal
#line integer-literal string-literal
pp-in teger-literal:
decimal-digit
decimal-digits decimal-digit
pp-s tring-literal:
" pp-string-literal-characters "
pp -string-literal-characters:
pp-s tring-literal-character
pp-s tring-literal-characters pp-string-literal-character
pp-s tring-literal-character:
Any character except " (U+0022), and white-space
5、預處理辨別符
預處理辨別符使用和規則C#辨別符文法相似的文法:
pp -identifi er:
pp-ava ilable-identifier
pp-ava ilable-identifi er:
A pp-identif ier-or-keyword that is not true or false
pp-id entif ier-or-keyword:
identifi er-start-character identif ier-part-characters opt
true 和false 符号不是合法的預定義訓示符,是以不能用于#define 的定義和#undef 的反定義。
6、預處理表達式
操作符!, ==, !=, && 和||是允許的預定義表達式。在預定義表達式中,圓括号可以用來分組。
pp-expression:
pp-equality-expression
pp-pr imary-expression:
true
false
pp -identifi er
( pp-expression )
pp-unary-expression:
pp-pr imary-expression
! pp-unary-expression
pp-equality-expression:
pp-equality-expression == pp-logical-and-expression
pp-equality-expression != pp-logical-and-expression
pp-logical-and-expression:
pp-unary-expression
pp-logical-and-expression && pp-unary-expression
pp-logical-or-expression:
pp-logical-and-expression
pp-logical-or-expression || pp-logical-and-expression
7、與空白互動作用
條件編譯辨別符必須在一行的第一個非空白位置。
一個單行注釋可以跟在條件編譯訓示符或pp-c ontrol-line 辨別符後面。
例如:
#define Debug // Defined if the build is a debug build
對于pp-control-line 辨別符,一行的剩餘組成pp-message,獨立于此行的注釋。
看看例子:
#warning // TODO: Add a better warning
會有一個注釋為"// TODO: Add a better warning"的警告。
一個多行注釋的起始和結束可以不在同一行中,就像條件編譯辨別符。
就像這個例子:
#define Debug
#define
Retail
#define A
#define B
結果将是編譯時錯誤。
可以形成一個條件編譯辨別符的資料符号可能會隐含在注釋中。
看看這個例子:
// This entire line is a commment. #define Debug
不包含任何條件編譯辨別符,然而完全由空白組成。
《未完待續:C#專題之文法和句法分析》
-------思多雅[天行健]版權所有,首發太平洋論論壇,轉載請注明-------
小知識:編譯語言與解釋語言的優缺點對比
1、運作效率:
編譯語言需要編譯一次,運作直接執行、不需要翻譯,是以編譯型語言的程式執行效率高。而解釋語言則不同,解釋型語言的程式不需要編譯,省了道工序,解釋性語言在運作程式的時候才翻譯,比如解釋型basic語言,專門有一個解釋器能夠直接執行basic程式,每個語句都是執行的時候才翻譯。這樣解釋性語言每執行一次就要翻譯一次,效率比較低。
解釋執行的語言因為解釋器不需要直接同機器碼打交道是以實作起來較為簡單、而且便于在不同的平台上面移植,這一點從現在的程式設計語言解釋執行的居多就能看出來,如 Visual Basic、Visual Foxpro、Power Builder、Java...等。編譯執行的語言因為要直接同CPU 的指令集打交道,具有很強的指令依賴性和系統依賴性,但編譯後的程式執行效率要比解釋語言要高的多,象現在的 Visual C/C++、Delphi 等都是很好的編譯語言。
2、代碼安全性
對于解釋語言與編譯語言所編制出來的代碼安全性上而言,可以說是各有優缺點。曾經在 Windows 下跟蹤調式過 VB 程式的朋友一般都知道,程式代碼 99% 的時間裡都是在 VBRUNxx 裡轉來轉去,根本看不出一個是以然來。 這是因為你跟蹤的是 VB 的解釋器,要從解釋器中看出代碼的目的是什麼是相當困難的。但解釋語言有一個緻命的弱點,那就是解釋語言的程式代碼都是以僞碼的方式存放的,一旦被人找到了僞碼與源碼之間的對應關系,就很容易做出一個反編譯器出來,你的源程式等于被公開了一樣。而編譯語言因為直接把使用者程式 編譯成機器碼,再經過優化程式的優化,很難從程式傳回到你的源程式的狀态, 但對于熟悉彙編語言的解密者來說,也很容易通過跟蹤你的代碼來确定某些代碼的用途。
看到這裡,可别打花花腸子去搞什麼破解,這個跟偶可沒什麼關聯的哦。