亚洲日本免费-啊轻点灬太粗太长了三男一女-麻豆av电影在线观看-日韩一级片毛片|www.grbbt.com

如何解析表達(dá)式之二

  關(guān)于賦值算符在表達(dá)式中的使用

  其實,大家一般不會在一個表達(dá)式中使用多個賦值算符,除非不太正常,但其實很多語言的編譯或解釋系統(tǒng)都允許這樣做,關(guān)鍵是如何使用。賦值算符和其它算符明顯不同的一點還在于:其它算符均是自左至右進(jìn)行解析,而賦值算符則恰恰相反!

  我們來看如下一個簡單的例子,請你給出判斷:這個表達(dá)式到底是合法的還是非法的:

  a=3+a*2+b=5

  看到上面這個表達(dá)式,我們的第一反應(yīng)就是書寫這個表達(dá)式的人可能腦子有問題,怎么能這些用呢?事實上這個表達(dá)式也是錯誤的(大家可以試試,一般系統(tǒng)都會報告類似“無法給常量賦值”等的錯誤),如果我們改一下:

  a=3+a*2+(b=5)

  這樣,這個表達(dá)式就是合法的了!為什么?

  因為按算符的優(yōu)先程度,上式中a*2最先被規(guī)約,然后是3+a*2,最后是先對變量b進(jìn)行賦值,最后規(guī)約的效果實際上就相當(dāng)于:

  a=3+a*2+5

  臨時變量的約簡

  在上一章的最后部分,提到了一個比較復(fù)雜的表達(dá)式的解析,即:

  b > c+a < a || a < 3 && a == b

  最終,經(jīng)過處理的中間代碼如下所示:

  @@tmp0=%sstack[3]+%sstack[1]

  @@tmp1=%sstack[2]>@@tmp0

  @@tmp2=@@tmp1<%sstack[1]

  @@tmp3=%sstack[1]<3

  @@tmp4=%sstack[1]==%sstack[2]

  @@tmp5=@@tmp3&&@@tmp4

  @@tmp6=@@tmp2||@@tmp5

  在上一章也提到,這樣生成的中間代碼并不是十分優(yōu)雅、簡約,雖然我們不需要研究到底如何對代碼進(jìn)行優(yōu)化,甚至直接使用寄存器來替代臨時變量(那是真正編譯器干的事,不是腳本翻譯研究的對象,時刻記住我們只研究腳本的解析或解釋,至于如何使用累加寄存器、邊界對齊、機(jī)器代碼的翻譯、運行時內(nèi)存優(yōu)化等“高深”課題至少現(xiàn)在不是我們所關(guān)心的),但還是有十分簡單又比較有效的方法來減少臨時變量的數(shù)量:如果被規(guī)約的表達(dá)式中包含了臨時變量,那么就使用序號最大的臨時變量作為規(guī)約的目標(biāo)。

  經(jīng)過一點點優(yōu)化,上述的中間代碼就被變換為如下形式:

  @@tmp0=%sstack[3]+%sstack[1]

  @@tmp0=%sstack[2]>@@tmp0

  @@tmp0=@@tmp0<%sstack[1]

  @@tmp1=%sstack[1]<3

  @@tmp2=%sstack[1]==%sstack[2]

  @@tmp2=@@tmp1&&@@tmp2

  @@tmp2=@@tmp0||@@tmp2

  通過上面的例子可以看出,我們僅用了三個臨時變量(@@tmp0-@@tmp2)就替代了原來需要七個臨時變量(@@tmp0-@@tmp6)的方案,而且效果相同。

  好了,其實本章的主要目的并不是對表達(dá)式解析的優(yōu)化進(jìn)行深入地探討,而是主要解決如下兩個問題:

  1.表達(dá)式中包含了數(shù)組元素的操作;

  2.表達(dá)式中包含了函數(shù)的調(diào)用。

  如需要解決上述兩個問題,我們就要使用計算機(jī)界最為恐怖和逆天的方法,也是計算機(jī)科學(xué)中無處不在的核心內(nèi)容之一——遞歸(Recursive)。

  關(guān)于遞歸,當(dāng)然有許多非常高級的議題來討論或研究它,比如令人無法捉摸的而又十分高深的理論課程——《遞歸論》,或者在《數(shù)據(jù)結(jié)構(gòu)》中無所不在的基于遞歸的數(shù)據(jù)結(jié)果或遞歸算法(如二叉樹的遍歷、B樹的刪除等等),抑或是在《編譯原理》中的什么左右遞歸文法!

  好了,本文的目的不是來探討這些理論問題的,我們還是看看如何使用遞歸來解決表達(dá)式解析的。

  為什么需要在表達(dá)式的解析中專門提到數(shù)組的處理?在上一章中提到關(guān)于算符優(yōu)先級時,關(guān)于符號“[]”就是實際上和數(shù)組相關(guān)的。

  幾乎所有語言的,無論是一般高級語言還是一些通用的腳本語言都會將“[]”作為數(shù)組下標(biāo)來使用。“[]”中不僅可以使用整數(shù)常量來存取數(shù)組中的元素,而且其中可以使用表達(dá)式,表達(dá)式即可以是常量表達(dá)式也可以是帶變量的表達(dá)式甚至其中再嵌套其它數(shù)組的調(diào)用或者函數(shù)調(diào)用,對于這種復(fù)雜的情況我們只有也只能求助于遞歸。

  需要說明的一點是,我們實現(xiàn)的解釋系統(tǒng)中,支持一維數(shù)組,而不支持多維數(shù)組,其原因很簡單——一維數(shù)組已經(jīng)夠用了!其實,在C語言中實際上也是如此,Perl也是類似的,即使你想定義一個多維數(shù)組也不可得。

  言歸正傳,下面看一個含有數(shù)組的簡單表達(dá)式的解析情況:

  array[0]+a+b

  它生成的中間代碼如下:

  @@tmp0=%sstack[9][0]+%sstack[1]

  @@tmp0=@@tmp0+%sstack[2]

  可以看出,這里數(shù)組array中的第一個元素是從下標(biāo)0開始的,這也是我們的以及大多數(shù)語言約定的,而且這個數(shù)組變量是其所在函數(shù)符號棧的第十個元素(我們約定棧中即可以保存普通的標(biāo)量,也能夠保存向量)。這個例子比較簡單,因數(shù)組下標(biāo)是常量,完全沒有使用到遞歸,故其生成的中間代碼和普通表達(dá)式幾乎沒有區(qū)別,唯有小小的不同點就在于符號%sstack[9][0]表明了這是一個數(shù)組,可以理解%sstack[9]存放的不是一般變量的值,而僅僅是數(shù)組變量array的首地址而已(第一個元素的地址),其元素的值是存放在其它地方的,這里還不能說明(佛曰:說不得?。?,得以后在解釋腳本是如何運行時再講。

  以下,再給出一個稍微復(fù)雜點的例子:

  array[12+a+b]+a+b

  在這個例子中,我們可以看到其數(shù)組下標(biāo)中含有一個子表達(dá)式“12+a+b”,我們知道其實“[]”中所包含的表達(dá)式應(yīng)該是優(yōu)先被規(guī)約的,因為其優(yōu)先級高,所以如遇到此類數(shù)組下標(biāo)應(yīng)遞歸調(diào)用表達(dá)式解析例程,這樣上述表達(dá)式的解析結(jié)果大致如下所示:

  @@tmp0=12+%sstack[1]

  @@tmp0=@@tmp0+%sstack[2]

  @@tmp1=%sstack[9][@@tmp0]+%sstack[1]

  @@tmp1=@@tmp1+%sstack[2]

  通過上面生成的中間代碼可以看出,系統(tǒng)首先是對表達(dá)式“12+a+b”進(jìn)行了規(guī)約,并將結(jié)果保存在臨時變量@@tmp0中,至于后續(xù)的解析就不用過多分析了,讀者一看就知道大概了。

  表達(dá)式中的函數(shù)調(diào)用

  其實,表達(dá)式中包含了函數(shù)調(diào)用是非常正常的用法,我們在處理這種情況時和處理數(shù)組中的表達(dá)式其實比較類似,但存在如下幾點不同:

  1.函數(shù)調(diào)用的合法性檢查與數(shù)組處理存在一定差異;

  2.在處理函數(shù)調(diào)用時,需要注意處理堆棧中的輸入?yún)?shù)、返回值以及返回地址;

  3.在處理函數(shù)調(diào)用時,需注意處理到底是傳值還是傳引用。

  下面我們就上述三個需要注意的方面分別介紹。

  函數(shù)調(diào)用的合法性檢查

  在系統(tǒng)遇到函數(shù)調(diào)用時,需要進(jìn)行合法性檢查,其主要檢查的方面無非是:

  1.函數(shù)是否定義?

  2.函數(shù)參數(shù)的類型是否匹配?

  對于第一個方面,函數(shù)是否定義的問題,我們可以這樣的順序查找一個函數(shù)是否定義:

  1.該函數(shù)是否是系統(tǒng)內(nèi)置函數(shù)?

  2.該函數(shù)是否是本腳本或程序文件定義函數(shù)?

  3.該函數(shù)是否在其它庫文件中被定義?(腳本中需要使用相關(guān)的語句進(jìn)行引用,例如“use xxx”)

  如果根據(jù)上述查找順序仍不能查到,則報錯,否則將查到的函數(shù)地址(這個地址實際上是中間代碼的行號,后面在闡述多文件解析時會給出如何翻譯)。

  對于參數(shù)類型的檢查,幸好我們所做的不是強(qiáng)類型語言,故在這方面我們只要把握好參數(shù)是標(biāo)量還是向量即可,其它的可以推遲到運行階段(例如如果某個函數(shù)的參數(shù)要求必須是數(shù)值,但你卻傳遞的是字符串,那么在解釋或編譯時倒是不一定非要進(jìn)行檢查,待到運行時給出異常或者像Perl一樣給出“undef”也可)。

  函數(shù)調(diào)用與堆棧使用

  一般而言,系統(tǒng)都是利用堆棧來傳遞輸入?yún)?shù)、返回值并且保存返回地址的,但也不排除在一定情況下,直接使用寄存器來保存上述這些內(nèi)容,但在我們的實現(xiàn)中,只考慮模擬堆棧來解決函數(shù)的調(diào)用和返回。

  在很多高級語言中,在將參數(shù)壓入堆棧時,只有兩種做法(也只能有兩種),一種是將參數(shù)自左至右壓入堆棧,而另外一種則正相反,即將參數(shù)自右至左壓入堆棧;在高級語言中,Pascal是采用前者,而采用后者的則更多,利于C/C++(當(dāng)然也可以使用第一種參數(shù)入棧方式,但需要特殊聲明)。

  需要說明的一點是,我們打算單獨使用堆棧來處理函數(shù)的調(diào)用,這和一般的高級語言的處理方式還是存在一定的出入。

  傳值還是引用

  我們知道,在Java的函數(shù)調(diào)用中,一般都是傳遞的對于變量的引用,這樣做的好處就是可以獲得多個返回值,而且可以節(jié)省堆棧空間。

  所以,在本文所提到的函數(shù)調(diào)用中,一般參數(shù)也都是引用方式(當(dāng)然,如果參數(shù)只是變量就可以使用引用,但如果它是普通表達(dá)式或者常量就沒法使用引用了)。

  那么,下面就給出例子來說明如何來處理是傳值還是傳遞引用。

  函數(shù)調(diào)用解析

  好了,經(jīng)過上面的一串說明,我們終于可以處理函數(shù)調(diào)用了,下面給出一個例子來說明函數(shù)調(diào)用在表達(dá)式中的處理以及如何翻譯成中間代碼:

  a    +(22+array[12+a+b])+25.2+fun(f(a+b),sin(a),b,c,log(d))+(x+(y+z))

  上面是一個較為復(fù)雜的表達(dá)式,它既含有普通的算符,又包含了數(shù)組和函數(shù)調(diào)用,而且數(shù)組下標(biāo)中也是表達(dá)式,而函數(shù)調(diào)用的參數(shù)中既包括普通的變量,也包括了表達(dá)式以及嵌套的函數(shù)調(diào)用。

  我們假定fun、f是用戶自定義函數(shù),而sin、log是系統(tǒng)內(nèi)置函數(shù),a、b、c、d、x、y和z都是用戶定義的變量,25.2是個浮點類型的常數(shù),最終得到的中間代碼可能是這樣的:

  @@tmp0=12+%sstack[1]

  @@tmp0=@@tmp0+%sstack[2]

  @@tmp1=22+%sstack[9][@@tmp0]

  @@tmp1=%sstack[1]+@@tmp1

  @@tmp1=@@tmp1+25.2

  @@tmp2=%sstack[1]+%sstack[2]

  call,f,@@tmp3,@@tmp2

  call,sin,@@tmp4,%sstack[1]

  call,log,@@tmp5,%sstack[4]

  call,fun,@@tmp6,@@tmp3,@@tmp4,%sstack[2],%sstack[3],@@tmp5

  @@tmp6=@@tmp1+@@tmp6

  @@tmp7=%sstack[6]+%sstack[7]

  @@tmp7=%sstack[5]+@@tmp7

  @@tmp7=@@tmp6+@@tmp7

  在上述生成的中間代碼中,我們可以但到,如果數(shù)組下標(biāo)或者函數(shù)參數(shù)中包含了表達(dá)式,則首先回遞歸調(diào)用表達(dá)式解析過程進(jìn)行處理;我們將函數(shù)調(diào)用翻譯成如下的中間代碼形式:

  call,function name,returnedvalue,argument0,…,argumentn

  當(dāng)然,我們可以將function name替換成所需要的函數(shù)地址,如下:

  call,function address,returnedvalue,argument0,…,argumentn

  其中,如果函數(shù)是系統(tǒng)內(nèi)置、其它庫函數(shù)或者用戶自定義函數(shù),則其地址可以增加一個前綴來表示,例如“###System::”;使用“###”就是告訴腳本解析器這個函數(shù)需要從其它庫中來尋找,而不是本腳本文件定義的函數(shù);另外,與其它腳本語言或者高級語言類似,用戶也需要注意庫的搜索路徑,這個問題會在腳本的運行階段來解釋。

  如果不過考慮的中間代碼的兼容性,其實只使用名字來查找也是不錯的,至少這樣看來更加直觀(這個理念更加接近于動態(tài)庫的概念,即無論庫的版本如何修改,只要這個函數(shù)仍然存在我就可以使用),只不過這樣做的話會略微降低一點腳本在運行時的效率(需要根據(jù)庫文件的函數(shù)名與函數(shù)地址表進(jìn)行查找)。

  另外,需要說明的是通過觀察生成的中間代碼,我們也可以輕易地區(qū)分出,哪些參數(shù)需要傳值,哪些需要傳遞引用,如上述中間代碼中的一行:

  最后,如果函數(shù)的參數(shù)是可變的(類似C語言中的printf函數(shù)),那么我們也需要小心處理這種樣式。

  表達(dá)式解析的完整算法

  以下是表達(dá)式解析完整算法的偽代碼;其中AnalysisStack就是在表達(dá)式分析時所使用的堆棧,expression是待分析的表達(dá)式串,GetNextToken是從輸入串中獲取算符或者操作數(shù)(變量、常量或者是函數(shù)調(diào)用):

  Initializethe AnalysisStack;

  Push ‘#’into the AnalysisStack;

  Add ‘#’to the end of expression;

  While(NotEof(expression) )

  {

  Token = GetNextToken();

  If ( Token is a common operand )

  {

  Checkvalidation of operand(if it is a valid constant or defined variable);

  Push Token into the AnalysisStack;

  }

  Else

  If ( Token isa function call )

  {

  Check thevalidation of function call;

  Resolve thearguments;

  Result = Recursivecalling the expression parser according to each argument;

  Push Result into the AnalysisStack;

  }

  Else

  If ( Tokencontains array )

  {

  Check validation of array;

  Result = Recursivecalling the expression parser according to its index;

  PushResult into the AnalysisStack;

  }

  Else

  {

  #The tokenis an operator

  PreOperator= Get the operator at the top of the AnalysisStack;

  If ( GetPriority(Token)<= GetPriority(PreOperator) )

  {

  Right =Pop from the AnalysisStack;

  PreOperator = Pop from the AnalysisStack;

  Left = Popfrom the AnalysisStack;

  Generatethe intermediate code, like tmpvar = Left PreOperator Right;

  Push tmpvar into the AnalysisStack;

  }

  Push Tokeninto the AnalysisStack;

  }

  }

  If (SizeOf(AnalysisStack) != 2 )

  {

  Error;

  }

  Else

  {

  Return (Pop from the AnalysisStack);

  }

  關(guān)于上述算法,有幾點值得注意:

  1.關(guān)于字符串的處理:需要將字符串作為整體的Token來處理;由于字符串中可能包含任何字符,故需小心處理,包括對于字符串中也包含雙引號(本文約定雙引號是字符常量的界符)的情況;

  2.關(guān)于注釋的處理:本節(jié)暫不做討論,在后續(xù)章節(jié)中討論復(fù)合語句解析時再進(jìn)行;但值得注意的一點是注釋可以出現(xiàn)在任何的算符之間或者算符和操作數(shù)之間,而不能橫亙在操作數(shù)內(nèi),否則會認(rèn)為注釋是常量或變量的一部分,從而導(dǎo)致錯誤;

  3.關(guān)于回車換行的處理:回車換行不會作為腳本解析的分隔符號;其它與注釋類似,即回車換行可以出現(xiàn)在任何的算符之間或者算符和操作數(shù)之間;

  4.關(guān)于臨時變量的生成和存儲:本節(jié)暫不作討論(既可以存放在符號棧上也可以單獨存儲),待后續(xù)章節(jié)再繼續(xù)闡述這個話題。

  小結(jié)

  通過上述章節(jié)的講解和示例,我們基本上掌握了如何解析或解釋一個較為復(fù)雜的表達(dá)式,其實其核心也無非如下兩點:第一需要搞清楚各種算符的優(yōu)先級別,當(dāng)然也可以自己發(fā)明算符;第二善于使用遞歸的方法將復(fù)雜的問題簡單化、重復(fù)化。

  關(guān)于遞歸,在后續(xù)的章節(jié)還需要經(jīng)常出場,包括對于復(fù)合語句的解析等等,故希望能熟練掌握并多加練習(xí)(當(dāng)然不要搞得老是堆棧溢出),大家可以盡量考慮用遞歸來改寫一些循環(huán)語句做的工作。

  另外,值得注意的一點是在有些語言中,可以在表達(dá)式中直接聲明變量,但我們并不打算這樣做。

  最后,對于逗號運算符(在一般高級語言或腳本語言中,與它的優(yōu)先級實際上是低于賦值運算符的),我們也并不打算支持,但讀者應(yīng)該知道在這種情況下,逗號運算符是可以將多個子表達(dá)式分隔開,整個表達(dá)式的值就是最后一個表達(dá)式的值,如下:

  假定:b=2,c=7,d=5,

  a1=(++b,c–,d+3);

  a2=++b,c–,d+3;

  那么,a1等于8,而a2卻等于3。

 

上一篇:安卓防火墻 PS DroidWall

下一篇:日志審計系統(tǒng)的全部不見得都是審計