標籤:c編譯器 c指標算術 語義檢查
在前文對語義檢查進行簡介時,我們已初步介紹過用於對二元運算子運算式進行語義檢查的函數CheckBinaryExpression,為了閱讀方便,這裡我們再次給出圖4.2.2。在本小節中,我們準備對第1126至1144行中的各個函數進行討論。
圖4.2.2 CheckBinaryExpression()
對於形如a+b的二元運算運算式,我們要通過在前面章節中介紹的函數CommonRealType來求得整個運算式a+b的類型,如果a為int型且b為double型,則運算式a+b的類型為double。在圖4.2.40中給出的宏定義PERFORM_ARITH_CONVERSION中,第14行調用了CommonRealType函數用來求公用類型,而第15和第16行則分別對二元運算子的左運算元和右運算元進行必要的轉型操作。第6行的宏SWAP_KIDS用於交換左右運算元,例如當遇到形如i + ptr的運算式,其中i為整數,而ptr為指標,我們如果希望左運算元為指標類型,而右運算元為整數類型,則可使用宏SWAP_KIDS來交換左右運算元,進而得到ptr+i。第19行的REPORT_OP_ERROR用於報錯,表示遇到了非法的運算子。
圖4.2.40 宏PERFORM_ARITH_CONVERSION
接下來我們來分析一4.2.2第1126至1143行中列出的CheckEqualityOP等函數,其代碼4.2.41所示。第2行我們給出了用於判斷是否為算術類型的宏IsArithType,其定義在ucl\type.h,第3行的IsScalarType用於判斷類型是否為標量類型。結合第3行的POINTER等枚舉常量及其在type.h中的枚舉定義,不難讀懂type.h中的其他幾個宏,如BothScalarType等,這裡不再囉嗦。圖4.2.41第5至24行的代碼用於對形如a==b或a!=b的運算式進行語義檢查,如果運算元a和b都是算術類型(即整型和浮點類型),則在第12用宏PERFORM_ARITH_CONVERSION來完成必要的類型轉換。運算式a==b或a!=b的結果為真或假,在C語言中,我們用int類型來表示布爾值,所以第13行置運算式的類型為int。第14行調用FoldConstant函數進行必要的常量摺疊,例如對於3 == 2這樣的運算式,沒有必要到運行時再求值,編譯時間我們就可以知道運算式3 == 2的結果為0。
圖4.2.41 CheckEqualityOP()
圖4.2.41第16至21行則是按照C標準ucc\ansi.c.txt第3.3.9節” Equality operators”中規定的語義規則進行編寫的,如下所示:
(1) both operands are pointers to qualified or unqualified versions ofcompatible types;
兩個指標是類型相容的指標,我們在前面的章節中介紹過“相容類型”的概念。對應第16行。
(2)one operand is apointer to an object or incomplete type and the other is a qualified orunqualified version of void ; 一個指標是指向資料對象的指標,另一個是void *。對應第17和18行。
(3)one operand is apointer and the other is a null pointer constant. 一個是指標類型,而另一個為NULL。對應第19和20行。
圖4.2.41第25至31行用於處理a&b、a|b和a^b這樣的位元運算,這些運算子要求運算元為整型;而第32至39行則用於處理形如a && b和a||b這樣的短路運算,第34行的if條件要求兩個運算元都為標量類型(即整型、浮點型和指標類型等);第41至54行的代碼用於對a/b、a*b和a%b進行語義檢查,取餘運算%要求兩個運算元都為整型,而乘除運算則可以是整型或浮點型;第55至61行的函數則用於檢查形如a<<b和a>>b這樣的移位元運算,這裡需要注意的是,我們並沒有使用宏PERFORM_ARITH_CONVERSION來求公用類型,其原因在於運算式a>>b是進行算術右移還是邏輯右移,要取決於“左運算元a為有符號整數還是不帶正負號的整數”,所以在第60行我們置運算式a>>b的類型為第一個運算元a的類型。圖4.2.41中的代碼不算複雜,我們就不再囉嗦。
接下來,我們來分析一下CheckAddOP等函數,4.2.42所示。第1至26行的代碼用於處理形如a+b的運算式,而第27至48行的代碼則對形如a-b的運算式進行語義檢查。對於a+b而言,第9至11行用於處理兩個運算元都為算術類型的情況;當一個運算元為指標類型,而另一個運算元為整型,運算式ptr+i進行的是C語言的指標加法運算,在彙編代碼層次真正執行的加法為ptr + k,其中k為i*sizeof(*ptr),第21行調用的ScalePointerOffset函數用於構造進行乘法運算的文法樹結點。而對於a-b來說,第30至33行用於處理減法的兩個運算元都是算術類型的情況,第34至39行用於處理形如ptr-i的指標運算,跟ptr+i的指標運算類似,我們需要在第37行調用ScalePointerOffset函數來構造i*sizeof(*ptr)的乘法運算。第41至48行則用於處理形如ptr1-ptr2的指標減法運算,例如,對於intarr[3]而言,&arr[2]-&arr[0]的含義是&arr[2]與&arr[0]之間相差幾個int整數,而不是它們的地址相差幾個位元組,因此在彙編代碼層次,我們真正執行的運算為(ptr2-ptr1)/sizeof(*arr),第44行調用的函數PointerDifference用於構造相應的除法運算結點。第49至60行給出了該函數的代碼,第53的CREATE_AST_NODE用於建立文法樹結點,第55行置其運算子為OP_DIV,即除法。
圖4.2.42 CheckAddOP()
下面,我們來討論賦值運算運算式的語義檢查。按照C的語義,在運算式a+=b中,a只被計算一次,而在a = a+b中,a卻要被計算兩次。例如,對以下代碼而言,在運算式*f()+= 3中,函數f()只被調用一次,而在運算式*f() += *f() + 3中,函數f()需要被調用兩次。因此,當我們把a += b轉換為a = a + b來處理,也要保持這樣的語義不變。
int * f(void){
static int number;
printf("int *f(void) \n");
return &number;
}
int main(){
*f() += 3;
*f() += *f() + 3;
return 0;
}
對於在C原始碼中就寫為a = a + b的運算式來說(為表述方便,不妨記為a1 = a2 +b),在文法樹上,有兩個結點與a對應,即a1和a2各對應一個文法樹結點;但對a+=b而言,只有一個文法樹結點與a對應,語義檢查時,我們把a+=b轉換為a = a’ + b來處理後,並不會為a構造新的文法樹結點。為了與在C原始碼中就寫為a=a+b的運算式有所區別,我們不妨把語義檢查後由a+=b得來的運算式寫為 a= a’+b,其中a和a’對應的是同一個文法樹結點。
有了這個基礎後,讓我們來分析一下對賦值運算運算式進行語義檢查的函數CheckAssignmentExpression。賦值運算子左側的運算元必須是左值,且該運算元在聲明時不應有限定符const,如果左側運算元是結構體對象,則在該結構體的定義中,所有的成員域都不應有const限定符,圖4.2.43第38至44行的CanModify()函數完成了這些判斷。第13行調用CanModify()函數來檢查一下左運算元是否為可寫的左值。第18至27行用於把形如 a += b的運算式轉換為a = a’+b來處理,我們只在第20行建立了一個新的文法樹結點用來存放運算子+,而a和a’始終對應同一個文法樹結點。對於a = a’+b來說,第26行實際上是調用CheckAddOP函數來對加法運算進行語義檢查。第29行調用的CanAssign函數用於檢測賦值運算子兩側的運算元在類型上是否匹配,我們在前面的章節中分析過CanAssign函數。
圖4.2.43 CheckAssignmentExpression()
圖4.2.44給出了運算式a+=b在文法分析後和語義檢查後的文法樹,由該圖我們可以很清楚地看到,在把a+=b轉換為a=a’+b後,文法樹上a對應的結點仍舊只有一個。語義檢查後,我們還會在文法樹上添加各結點的類型資訊,此處為簡單起見,我們忽略了這些資訊。
圖4.2.44 a+=b的文法樹
然後,我們來討論一下對形如a?b:c的條件運算式的語義檢查,與其相關的代碼4.2.45所示。條件運算式實際上可被當作是三元運算子,即有a、b和c這3個運算元。圖4.2.45第6至12行用於對第一個運算元a進行語義檢查,第9行要求第一個運算元a為標量類型,第13至18行遞迴地調用CheckExpression函數,來對第2個運算元b和第3個運算元c分別進行語義檢查。
圖4.2.45 CheckConditionalExpression()
圖4.2.45第19至50行的代碼,用於對形如a?b:c中的運算元b和c的類型進行檢查,這些代碼是按照C標準ucc\ansi.c.txt的第“3.3.15 Conditional operator”節的語義規則編寫的,如下所示:
(1)both operands have arithmetic type,即b和c的類型都為算術類型,對應第
22至28行,此時整個條件運算式的類型為b和c的公用類型。
(2)both operands have compatible structure or union types,即b和c的類型為相容
的結構體或聯合體類型,對應第29至30行。
(3)both operands have void type,即b和c的類型都為void,對應第31至32
行,此時整個條件運算式的類型就為void。
(4)both operands are pointers to qualified or unqualified versions ofcompatible types,
即b和c為相容的指標類型,對應第33至36行,此時整個條件運算式的類型為b和c類型的合成類型。
(5)one operand is a pointer and the other is a null pointer constant,即一個為指標類
型,另一個為NULL,對應第37至42行。
(6)one operand is a pointer to an object or incomplete type and the otheris a pointer
to aqualified or unqualified version of void,即一個是指向資料對象的指標(即不是指向函數的指標),另一個是void *。
除了以上這6種情況外,b和c的其他類型都被視為非法的,圖4.2.45第48行會進行報錯。圖4.2.45中調用的CommonRealType和CompositeType等函數,我們都已在前面的章節中做過討論。至此,我們完成了對C語言運算式的語義檢查,相關的代碼主要在exprchk.c中,可以發現尾碼運算式和一元運算式的語義是相對比較複雜的,例如,尾碼運算子[]和一元運算子*都涉及到了記憶體定址。對這些運算子的語義檢查,有助於我們更深入地理解C語言。
C編譯器剖析_4.2 語義檢查_運算式的語義檢查(7)_二元運算子_賦值運算_條件運算式