賦值語句
前面已經說明,要訪問内存,就需要相應的地址以表明訪問哪塊内存,而變量是一個映射,因此變量名就相當于一個地址。對于内存的操作,在一般情況下就隻有讀取内存中的數值和将數值寫入内存(不考慮分配和釋放内存),在C 中,為了将一數值寫入某變量對應的地址所标識的内存中(出于簡便,以後稱變量a對應的地址為變量a的地址,而直接稱變量a的地址所标識的内存為變量a),隻需先書寫變量名,後接“=”,再接欲寫入的數字以及分号。如下:
a = 10.0f; b = 34;
由于接的是數字,因此就可以接表達式并由編譯器生成計算相應表達式所需的代碼,也就可如下:
c = a / b * 120.4f;
上句編譯器将會生成進行除法和乘法計算的CPU指令,在計算完畢後(也就是求得表達式a / b * 120.4f的值了後),也會同時生成将計算結果放到變量c中去的CPU指令,這就是語句的基本作用(對于語句,在《C 從零開始(六)》中會詳細說明)。
上面在書寫賦值語句時,應該确保此語句之前已經将使用到的變量定義過,這樣編譯器才能在生成賦值用的CPU指令時查找到相應變量的地址,進而完成CPU指令的生成。如上面的a和b,就需要在書寫上面語句前先書寫類似下面的變量定義:
float a; long b;
直接書寫變量名也是一條語句,其導緻編譯器生成一條讀取相應變量的内容的語句。即可以如下書寫:a;
上面将生成一條讀取内存的語句,即使從内存中讀出來的數字沒有任何應用(當然,如果編譯器開了優化選項,則上面的語句将不會生成任何代碼)。從這一點以及上面的c = a / b * 120.4f;語句中,都可以看出一點——變量是可以返回數字的。而變量返回的數字就是按照變量的類型來解釋變量對應内存中的内容所得到的數字。這句話也許不是那麼容易理解,在看過後面的類型轉換一節後應該就可以理解了。
因此為了将數據寫入一塊内存,使用賦值語句(即等号);要讀取一塊内存,書寫标識内存的變量名。所以就可以這樣書寫:a = a 3;
假設a原來的值為1,則上面的賦值語句将a的值取出來,加上3,得到結果4,将4再寫入a中去。由于C 使用“=”來代表賦值語句,很容易使人和數學中的等号混淆起來,這點應注意。
而如上的float a;語句,當還未對變量進行任何賦值操作時,a的值是什麼?上帝才知道。當時的a的内容是什麼(對于VC編譯器,在開啟了調試選項時,将會用0xCCCCCCCC填充這些未初始化内存),就用IEEE的real*4格式來解釋它并得到相應的一個數字,也就是a的值。因此應在變量定義的時候就進行賦值(但是會有性能上的影響,不過很小),以初始化變量而防止出現莫名其妙的值,如:float a = 0.0f;。
賦值操作符
上面的a = a 3;的意思就是讓a的值增加3。在C 中,對于這種情況給出了一種簡寫方案,即前面的語句可以寫成:a = 3;。應當注意這兩條語句從邏輯上講都是使變量a的值增3,但是它們實際是有區别的,後者可以被編譯成優化的代碼,因為其意思是使某一塊内存的值增加一定數量,而前者是将一個數字寫入到某塊内存中。所以如果可能,應盡量使用後者,即a = 3;。這種語句可以讓編譯器進行一定的優化(但由于現在的編譯器都非常智能,能夠發現a = a 3;是對一塊内存的增值操作而不是一塊内存的賦值操作,因此上面兩條語句實際上可以認為完全相同,僅僅隻具有簡寫的功能了)。
對于上面的情況,也可以應用在減法、乘法等二元非邏輯操作符(不是邏輯值操作符,即不能a &&= 3;)上,如:a *= 3; a -= 4; a |= 34; a >>= 3;等。
除了上面的簡寫外,C 還提供了一種簡寫方式,即a ;,其邏輯上等同于a = 1;。同上,在電腦編程中,加一和減一是經常用到的,因此CPU專門提供了兩條指令來進行加一和減一操作(轉成彙編語言就是Inc和Dec),但速度比直接通過加法或減法指令來執行要快得多。為此C 中也就提供了“ ”和“—”操作符來對應Inc和Dec。所以a ;雖然邏輯上和a = a 1;等效,實際由于編譯器可能做出的優化處理而不同,但還是如上,由于編譯器的智能化,其是有可能看出a = a 1;可以編譯成Inc指令進而即使沒有使用a ;卻也依然可以得到優化的代碼,這樣a ;将隻剩下簡寫的意義而已。
應當注意一點,a = 3;這句語句也将返回一個數字,也就是在a被賦完值後a的值。由于其可以返回數字,按照《C 從零開始(二)》中所說,“=”就屬于操作符,也就可以如下書寫:
c = 4 ( a = 3 );
之所以打括号是因為“=”的優先級較“ ”低,而更常見和正常的應用是:c = a = 3;
應該注意上面并不是将c和a賦值為3,而是在a被賦值為3後再将a賦值給c,雖然最後結果和c、a都賦值為3是一樣的,但不應該這樣理解。由于a ;表示的就是a = 1;就是a = a 1;,因此a ;也将返回一個數字。也由于這個原因,C 又提供了另一個簡寫方式, a;。
假設a為1,則a ;将先返回a的值,1,然後再将a的值加一;而 a;先将a的值加一,再返回a的值,2。而a—和—a也是如此,隻不過是減一罷了。
上面的變量a按照最上面的變量定義,是float類型的變量,對它使用 操作符并不能得到預想的優化,因為float類型是浮點類型,其是使用IEEE的real*4格式來表示數字的,而不是二進制原碼或補碼,而前面提到的Inc和Dec指令都是出于二進制的表示優點來進行快速增一和減一,所以如果對浮點類型的變量運用“ ”操作符,将完全隻是簡寫,沒有任何的優化效果(當然,如果CPU提供了新的指令集,如MMX等,以對real*4格式進行快速增一和減一操作,且編譯器支持相應指令集,則還是可以産生優化效果的)。
賦值操作符的返回值
在進一步了解 a和a 的區别前,先來了解何謂操作符的計算(Evaluate)。操作符就是将給定的數字做一些處理,然後返回一個數字。而操作符的計算也就是執行操作符的處理,并返回值。前面已經知道,操作符是個符号,其一側或兩側都可以接數字,也就是再接其他操作符,而又由于賦值操作符也屬于一種操作符,因此操作符的執行順序變得相當重要。
對于a b c,将先執行a b,再執行( a b ) c的操作。你可能覺得沒什麼,那麼如下,假設a之前為1:
c = ( a *= 2 ) ( a = 3 );
上句執行後a為5。而c = ( a = 3 ) ( a *= 2 );執行後,a就是8了。那麼c呢?結果可能會大大的出乎你的意料。前者的c為10,而後者的c為16。
上面其實是一個障眼法,其中的“ ”沒有任何意義,即之所以會從左向右執行并不是因為“ ”的緣故,而是因為( a *= 2 )和( a = 3 )的優先級相同,而按照“()”的計算順序,是從左向右來計算的。但為什麼c的值不是預想的2 5和4 8呢?因為賦值操作符的返回值的關系。
賦值操作符返回的數字不是變量的值,而是變量對應的地址。這很重要。前面說過,光寫一個變量名就會返回相應變量的值,那是因為變量是一個映射,變量名就等同于一個地址。C 中将數字看作一個很特殊的操作符,即任何一個數字都是一個操作符。而地址就和長整型、單精度浮點數這類一樣,是數字的一種類型。當一個數字是地址類型時,作為操作符,其沒有要操作的數字,僅僅返回将此數字看作地址而标識的内存中的内容(用這個地址的類型來解釋)。地址可以通過多種途徑得到,如上面光寫一個變量名就可以得到其對應的地址,而得到的地址的類型也就是相應的變量的類型。如果這句話不能理解,在看過下面的類型轉換一節後應該就能了解了。
所以前面的c = ( a = 3 ) ( a *= 2 );,由于“()”的參與改變了優先級而先執行了兩個賦值操作符,然後兩個賦值操作符都返回a的地址,然後計算“ ”的值,分别計算兩邊的數字——a的地址(a的地址也是一個操作符),也就是已經執行過兩次賦值操作的a的值,得8,故最後的c為16。而另一個也由于同樣的原因使得c為10。
現在考慮操作符的計算順序。當同時出現了幾個優先級相同的操作符時,不同的操作符具有不同的計算順序。前面的“()”以及“-”、“*”等這類二元操作符的計算順序都是從左向右計算,而“!”、負号“-”等前面介紹過的一元操作符都是從右向左計算的,如:!-!!a;,假設a為3。先計算從左朝右數第三個“!”的值,導緻計算a的地址的值,得3;然後邏輯取反得0,接着再計算第二個“!”的值,邏輯取反後得1,再計算負号“-”的值,得-1,最後計算第一個“!”的值,得0。
賦值操作符都是從右向左計算的,除了後綴“ ”和後綴“—”(即上面的a 和a--)。因此上面的c = a = 3;,因為兩個“=”優先級相同,從右向左計算,先計算a = 3的值,返回a對應的地址,然後計算返回的地址而得到值3,再計算c = ( a = 3 ),将3寫入c。而不是從左向右計算,即先計算c = a,返回c的地址,然後再計算第二個“=”,将3寫入c,這樣a就沒有被賦值而出現問題。又:
a = 1; c = 2; c *= a = 4;
由于“*=”和“ =”的優先級相同,從右向左計算先計算a = 4,得a為5,然後返回a的地址,再計算a的地址得a的值5,計算“*=”以使得c的值為10。
因此按照前面所說, a将返回a的地址,而a 也因為是賦值操作符而必須返回一個地址,但很明顯地不能是a的地址了,因此編譯器将編寫代碼以從棧中分配一塊和a同樣大小的内存,并将a的值複制到這塊臨時内存中,然後返回這塊臨時内存的地址。由于這塊臨時内存是因為編譯器的需要而分配的,與程序員完全沒有關系,因此程序員是不應該也不能寫這塊臨時内存的(因為編譯器負責編譯代碼,如果程序員欲訪問這塊内存,編譯器将報錯),但可以讀取它的值,這也是返回地址的主要目的。所以如下的語句沒有問題:
( a ) = a = 34;
但( a ) = a = 34;就會在編譯時報錯,因為a 返回的地址所标識的内存隻能由編譯器負責處理,程序員隻能獲得其值而已。
a 的意思是先返回a的值,也就是上面說的臨時内存的地址,然後再将變量的值加一。如果同時出現多個a ,那麼每個a 都需要分配一塊臨時内存(注意前面c = ( a = 3 ) ( a *= 2 );的說明),那麼将有點糟糕,而且a 的意思是先返回a的值,那麼到底是什麼時候的a的值呢?在VC中,當表達式中出現後綴“ ”或後綴“—”時,隻分配一塊臨時内存,然後所有的後綴“ ”或後綴“—”都返回這個臨時内存的地址,然後在所有的可以計算的其他操作符的值計算完畢後,再将對應變量的值寫入到臨時内存中,計算表達式的值,最後将對應變量的值加一或減一。
因此:a = 1; c = ( a ) ( a );執行後,c的值為2,而a的值為3。而如下:
a = 1; b = 1; c = ( a ) ( a ) ( b *= a ) ( a *= 2 ) ( a *= a );
執行時,先分配臨時内存,然後由于5個“()”,其計算順序是從左向右,
計算 a的值,返回增一後的a的地址,a的值為2
計算a 的值,返回臨時内存的地址,a的值仍為2
計算b *= a 中的a ,返回臨時内存的地址,a的值仍為2
計算b *= a 中的“*=”,将a的值寫入臨時内存,計算得b的值為2,返回b的地址
計算a *= 2的值,返回a的地址,a的值為4
計算a *= a 中的a ,返回臨時内存的地址,a的值仍為4
計算a *= a 中的“*=”,将a的值寫入臨時内存,返回a的地址,a的值為16
計算剩下的“ ”,為了進行計算,将a的值寫入臨時内存,得值16 16 2 16 16為66,寫入c中
計算三個a 欠下的加一,a最後變為19。
上面說了那麼多,無非隻是想告誡你——在表達式中運用賦值操作符是不被推崇的。因為其不符合平常的數學表達式的習慣,且計算順序很容易搞混。如果有多個“ ”操作符,最好還是将表達式分開,否則很容易導緻錯誤的計算順序而計算錯誤。并且導緻計算順序混亂的還不止上面的a 就完了,為了讓你更加地重視前面的紅字,下面将介紹更令人火大的東西,如果你已經同意上面的紅字,則下面這一節完全可以跳過,其對編程來講可以認為根本沒有任何意義(要不是為了寫這篇文章,我都不知道它的存在)。
序列點(Sequence Point)和附加效果(Side Effect)
在計算c = a 時,當c的值計算(Evaluate)出來時,a的值也增加了一,a的值加一就是計算前面表達式的附加效果。有什麼問題?它可能影響表達式的計算結果。
對于a = 0; b = 1; ( a *= 2 ) && ( b = 2 );,由于兩個“()”優先級相同,從左向右計算,計算“*=”而返回a的地址,再計算“ =”而返回b的地址,最後由于a的值為0而返回邏輯假。很正常,但效率低了點。
如果“&&”左邊的數字已經是0了,則不再需要計算右邊的式子。同樣,如果“||”左邊的數字已經非零了,也不需要再計算右邊的數字。因為“&&”和“||”都是數學上的,數學上不管先計算加号左邊的值還是右邊的值,結果都不會改變,因此“&&”和“||”才會做剛才的解釋。這也是C 保證的,既滿足數學的定義,又能提供優化的途徑(“&&”和“||”右邊的數字不用計算了)。
因此上面的式子就會被解釋成——如果a在自乘了2後的值為0,則b就不用再自增2了。這很明顯地違背了我們的初衷,認為b無論如何都會被自增2的。但是C 卻這樣保證,不僅僅是因為數學的定義,還由于代碼生成的優化。但是按照操作符的優先級進行計算,上面的b = 2依舊會被執行的(這也正是我們會書寫上面代碼的原因)。為了實現當a為0時b = 2不會被計算,C 提出了序列點的概念。
序列點是一些特殊位置,由C 強行定義(C 并未給出序列點的定義,因此不同的編譯器可能給出不同的序列點定義,VC是按照C語言定義的序列點)。當在進行操作符的計算時,如果遇到序列點,則序列點處的值必須被優先計算,以保證一些特殊用途,如上面的保證當a為0時不計算b = 2,并且序列點相關的操作符(如前面的“&&”和“||”)也将被計算完畢,然後才恢複正常的計算。
“&&”的左邊數字的計算就是一個序列點,而“||”的左邊數字的計算也是。C 定義了多個序列點,包括條件語句、函數參數等條件下的表達式計算,在此,不需要具體了解有哪些序列點,隻需要知道由于序列點的存在而可能導緻賦值操作符的計算出乎意料。下面就來分析一個例子:
a = 0; b = 1; ( a *= 2 ) && ( b = a );
按照優先級的順序,編譯器發現要先計算a *= 2,再計算 a,接着“ =”,最後計算“&&”。然後編譯器發現這個計算過程中,出現了“&&”左邊的數字這個序列點,其要保證被優先計算,這樣就有可能不用計算b = a了。所以編譯器先計算“&&”的數字,通過上面的計算過程,編譯器發現就要計算a *= 2才能得到“&&”左邊的數字,因此将先計算a *= 2,返回a的地址,然後計算“&&”左邊的數字,得a的值為0,因此就不計算b = a了。而不是最開始想象的由于優先級的關系先将a加一後再進行a的計算,以返回1。所以上面計算完畢後,a為0,b為1,返回0,表示邏輯假。
因此序列點的出現是為了保證一些特殊規則的出現,如上面的“&&”和“||”。再考慮“,”操作符,其操作是計算兩邊的值,然後返回右邊的數字,即:a, b 3将返回b 3的值,但是a依舊會被計算。由于“,”的優先級是最低的(但高于前面提到的“數字”操作符),因此如果a = 3, 4;,那麼a将為3而不是4,因為先計算“=”,返回a的地址後再計算“,”。又:
a = 1; b = 0; b = ( a = 2 ) ( ( a *= 2, b = a - 1 ) && ( c = a ) );
由于“&&”左邊數字是一個序列點,因此先計算a *= 2, b的值,但根據“,”的返回值定義,其隻返回右邊的數字,因此不計算a *= 2而直接計算b = a – 1得0,“&&”就返回了,但是a *= 2就沒有被計算而導緻a的值依舊為1,這違背了“,”的定義。為了消除這一點(當然可能還有其他應用“,”的情況),C 也将“,”的左邊數字定為了序列點,即一定會優先執行“,”左邊的數字以保證“,”的定義——計算兩邊的數字。所以上面就由于“,”左邊數字這個序列點而導緻a *= 2被優先執行,并導緻b為1,因此由于“&&”是序列點且其左邊數字非零而必須計算完右邊數字後才恢複正常優先級,而計算c = a,得2,最後才恢複正常優先級順序,執行a = 2和“ ”。結果就a為4,c為2,b為5。
所以前面的a = 3, 4;其實就應該是編譯器先發現“,”這個序列點,而發現要計算“,”左邊的值,必須先計算出a = 3,因此才先計算a = 3以至于感覺序列點好像沒有發生作用。下面的式子請自行分析,執行後a為4,但如果将其中的“,”換成“&&”,a為2。
a = 1; b = ( a *= 2 ) ( ( a *= 3 ), ( a -= 2 ) );
如果上面你看得很暈,沒關系,因為上面的内容根本可以認為毫無意義,寫在這裡也隻是為了進一步向你證明,在表達式中運用賦值運算符是不好的,即使它可能讓你寫出看起來簡練的語句,但它也使代碼的可維護性降低。
類型轉換
數字可以是浮點數或是整型數或其他,也就是說數字是具有類型的。注意《C 從零開始(三)》中對類型的解釋,類型隻是說明如何解釋狀态,而在前面已經說過,出于方便,使用二進制數來表示狀态,因此可以說類型是用于告訴編譯器如何解釋二進制數的。
所以,一個長整型數字是告訴編譯器将得到的二進制數表示的狀态按照二進制補碼的格式來解釋以得到一個數值,而一個單精度浮點數就是告訴編譯器将得到的二進制數表示的狀态按照IEEE的real*4的格式來解釋以得到一個是小數的數值。很明顯,同樣的二進制數表示的狀态,按照不同的類型進行解釋将得到不同的數值,那麼編譯器如何知道應該使用什麼類型來進行二進制數的解釋?
前面已經說過,數字是一種很特殊的操作符,其沒有操作數,僅僅返回由其類型而定的二進制數表示的狀态(以後為了方便,将“二進制數表示的狀态”稱作“二進制數”)。而操作符就是執行指令并返回數字,因此所有的操作符到最後一定執行的是返回一個二進制數。這點很重要,對于後面指針的理解有着重要的意義。
先看15;,這是一條語句,因為15是一個數字。所以15被認為是char類型的數字(因為其小于128,沒超出char的表示範圍),将返回一個8位長的二進制數,此二進制數按照補碼格式編寫,為00001111。
再看15.0f,同上,其由于接了“f”這個後綴而被認為是float類型的數字,将返回一個32位長的二進制數,此二進制數按照IEEE的real*4格式編寫,為1000001011100000000000000000000。
雖然上面15和15.0f的數值相等,但由于是不同的類型導緻了使用不同的格式來表示,甚至連表示用的二進制數的長度都不相同。因此如果書寫15.0f == 15;将返回0,表示邏輯假。但實際卻返回1,為什麼?
上面既然15和15.0f被表示成完全不同的兩個二進制數,但我們又認為15和15.0f是相等的,但它們的二進制表示不同,怎麼辦?将表示15.0f的二進制數用IEEE的real*4格式解釋出15這個數值,然後再将其按8位二進制補碼格式編寫出二進制數,再與原來的表示15的二進制數比較。
為了實現上面的操作,C 提供了類型轉換操作符——“()”。其看起來和括号操作符一樣,但是格式不同:(<類型名>)<數字>或<類型名>(<數字>)。
上面類型轉換操作符的<類型名>不是數字,因此其将不會被操作,而是作為一個參數來控制其如何操作後面的<數字>。<類型名>是一個标識符,其唯一标識一個類型,如char、float等。類型轉換操作符的返回值就如其名字所示,将<數字>按照<類型名>标識的類型來解釋,返回類型是<類型名>的數字。因此,上面的例子我們就需要如下編寫:15 == ( char )15.0f;,現在其就可以返回1,表示邏輯真了。但是即使不寫( char ),前面的語句也返回1。這是編譯器出于方便的緣故而幫我們在15前添加了( float ),所以依然返回1。這被稱作隐式類型轉換,在後面說明類的時候,還将提到它。
某個類型可以完全代替另一個類型時,編譯器就會進行上面的隐式類型轉換,自動添加類型轉換操作符。如:char隻能表示-128到127的整數,而float很明顯地能夠表示這些數字,因此編譯器進行了隐式類型轉換。應當注意,這個隐式轉換是由操作符要求的,即前面的“==”要求兩面的數字類型一緻,結果發現兩邊不同,結果編譯器将char轉成float,然後再執行“==”的操作。注意:在這種情況下,編譯器總是将較差的類型(如前面的char)轉成較好的類型(如前面的float),以保證不會發生數值截斷問題。如:-41 == 3543;,左邊是char,右邊是short,由于short相對于char來顯得更優(short能完全替代char),故實際為:( short )-41 == 3543;,返回0。而如果是-41 == ( char )3543;,由于char不能表示3543,則3543以補碼轉成二進制數0000110111010111,然後取其低8位,而導緻高8位的00001101被丢棄,此被稱為截斷。結果( char )3543的返回值就是類型為char的二進制數11010111,為-41,結果-41 == ( char )3543;的返回值将為1,表示邏輯真,很明顯地錯誤。因此前面的15 == 15.0f;實際将為( float )15 == 15.0f;。
注意前面之所以會朝好的方向發展(即char轉成float),完全是因為“==”的緣故,其要求這麼做。下面考慮“=”:short b = 3543; char a = b;。因為b的值是short類型,而“=”的要求就是一定要将“=”右邊的數字轉成和左邊一樣,這樣才能進行正确的内存的寫入(簡單地将右邊數字返回的二進制數複制到左邊的地址所表示的内存中)。因此a将為-41。但是上面是編譯器按照“=”的要求自行進行了隐式轉換,可能是由于程序員的疏忽而沒有發現這個錯誤(以為b的值一定在-128到127的範圍内),因此編譯器将對上面的情況給出一個警告,說b的值可能被截斷。為了消除編譯器的疑慮,如下:char a = ( char )b;。這樣稱為顯示類型轉換,其告訴編譯器——“我知道可能發生數據截斷,但是我保證不會截斷”。因此編譯器将不再發出警告。但是如下:char a = ( char )3543;,由于編譯器可以肯定3543一定會被截斷而導緻錯誤的返回值,因此編譯器将給出警告,說明3543将被截斷,而不管前面的類型轉換操作符是否存在。
現在應該可以推出——15 15.0f;返回的是一個float類型的數字。因此如果如下:char a = 15 15.0f;,編譯器将發出警告,說數據可能被截斷。因此改成如下:char a = ( char )15 15.0f;,但類型轉換操作符“()”的優先級比“ ”高,結果就是15先被轉換為char然後再由于“ ”的要求而被隐式轉成float,最後返回float給“=”而導緻編譯器依舊發出警告。為此,就需要提高“ ”的優先級,如下:char a = ( char )( 15 15.0f );就沒事了(或char( 15 15.0f )),其表示我保證15 15.0f不會導緻數據截斷。
應該注意類型轉換操作符“()”和前綴“ ”、“!”、負号“-”等的優先級一樣,并且是從右向左計算的,因此( char )-34;将會先計算-34的值,然後再計算( char )的值,這也正好符合人的習慣。
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!