C語言運算符的優先級
一、運算符的優先級表
C 語言的符號眾多,由這些符號又組合成了各種各樣的運算符。既然是運算符就一定有其特定的優先級,下表就是C 語言運算符的優先級表:




上表不容易記住。其實也用不著死記,用得多,看得多自然就記得了。也有人說不用記這些東西,只要記住乘除法的優先級比加減法高就行了,別的地方一律加上括號。這在你自己寫代碼的時候,確實可以,但如果是你去閱讀和理解別人的代碼呢?別人不一定都加上括號了吧?所以,記住這個表,我個人認為還是很有必要的。
二、一些容易出錯的優先級問題
上表中,優先級同為1 的幾種運算符如果同時出現,那怎么確定表達式的優先級呢?這是很多初學者迷糊的地方。下表就整理了這些容易出錯的情況:
這些容易出錯的情況,希望讀者好好在編譯器上調試調試,這樣印象會深一些。一定要多調試,光靠看代碼,水平是很難提上來的。調試代碼才是最長水平的。
2/(-2)的值是多少?
除法運算在小學就掌握了的,這里還要討論什么呢?別急,先計算下面這個例子:2/(-2)的值為多少?2%(-2)的值呢?如果與你想象的結果不一致,不要驚訝。我們先看看下面這些規則:
假定我們讓a 除以b,商為q,余數為r:
? ?q = a/b;
? ?r = a%b;
這里不妨先假定b 大于0。
我們希望a、b、q、r 之間維持什么樣的關系呢?
1,最重要的一點,我們希望q*b + r == a,因為這是定義余數的關系。
2,如果我們改變a 的正負號,我們希望q 的符號也隨之改變,但q 的絕對值不會變。
3,當b>0 時,我們希望保證r>=0 且r<b。
這三條性質是我們認為整數除法和余數操作所應該具備的。但是,很不幸,它們不可能同時成立。
先考慮一個簡單的例子:3/2,商為1,余數也為1。此時,第一條性質得到了滿足。好,把例子稍微改寫一下:(-3)/2 的值應該是多少呢?如果要滿足第二條性質,答案應該是-1。但是,如果是這樣,余數就必定是-1,這樣第三條性質就無法滿足了。如果我們首先滿足第三條性質,即余數是1,這種情況下根據第一條性質,商應該為-2,那么第二條性質又無法滿足了。
上面的矛盾似乎無法解決。因此,C 語言或者其他語言在實現整數除法截斷運算時,必須放棄上述三條性質中的至少一條。大多數編程語言選擇了放棄第三條,而改為要求余數與被除數的正負號相同。這樣性質1 和性質2 就可以得到滿足。大多數C 語言編譯器也都是如此。
但是,C 語言的定義只保證了性質1,以及當a>=0 且b>0 時,保證|r|<|b|以及r>=0。后面部分的保證與性質2 或性質3 比較起來,限制性要弱得多。通過上面的解釋,你是否能準確算出2/(-2)和2%(-2)的值呢?
C語言++、--操作符
這絕對是一對讓人頭疼的兄弟。先來點簡單的:? ?int i = 3;
? ?(++i)+(++i)+(++i);
表達式的值為多少?15 嗎?16 嗎?18 嗎?其實對于這種情況,C語言標準并沒有作出規定。有點編譯器計算出來為18,因為i 經過3 次自加后變為6,然后3 個6 相加得18;而有的編譯器計算出來為16(比如Visual C++6.0),先計算前兩個i 的和,這時候i 自加兩次,2 個i 的和為10,然后再加上第三次自加的i 得16。其實這些沒有必要辯論,用到哪個編譯器寫句代碼測試就行了。但不會計算出15 的結果來的。
++、--作為前綴,我們知道是先自加或自減,然后再做別的運算;但是作為后綴時,到底什么時候自加、自減?這是很多初學者迷糊的地方。假設i=0,看例子:
A)
j =(i++,i++,i++);
B)
for(i=0;i<10;i++)
{
? ?//code
}
C)
k = (i++)+ (i++)+ (i++);
你可以試著計算他們的結果。
A) 例子為逗號表達式,i 在遇到每個逗號后,認為本計算單位已經結束,i 這時候自加。關于逗號表達式與“++”或“--”的連用,還有一個比較好的例子:
int x;
int i = 3;
x = (++i, i++, i+10);
問x 的值為多少?i 的值為多少?
按照上面的講解,可以很清楚的知道,逗號表達式中,i 在遇到每個逗號后,認為本計算單位已經結束,i 這時候自加。所以,本例子計算完后,i的值為5,x的值為15。
B) 例子i 與10 進行比較之后,認為本計算單位已經結束,i 這時候自加。
C) 例子i 遇到分號才認為本計算單位已經結束,i 這時候自加。
也就是說后綴運算是在本計算單位計算結束之后再自加或自減。C 語言里的計算單位大體分為以上3 類。
留一個問題:
for(i=0,printf(“First=%d”,i);
i<10,printf(“Second=%d”,i);
i++,printf(“Third=%d”,i))
{
? ?printf(“Fourth=%d”,i);
}
打印出什么結果?
一、++i+++i+++i
上面的例子很簡單,那我們把括號去掉看看:int i = 3;
++i+++i+++i;
天啦!這到底是什么東西?好,我們先看看這個:a+++b 和下面哪個表達式想當:
A)
a++ +b;
B)a+ ++b;
二、貪心法
C 語言有這樣一個規則:每一個符號應該包含盡可能多的字符。也就是說,編譯器將程序分解成符號的方法是,從左到右一個一個字符地讀入,如果該字符可能組成一個符號,那么再讀入下一個字符,判斷已經讀入的兩個字符組成的字符串是否可能是一個符號的組成部分;如果可能,繼續讀入下一個字符,重復上述判斷,直到讀入的字符組成的字符串已不再可能組成一個有意義的符號。 這個處理的策略被稱為“貪心法”。需要注意到是,除了字符串與字符常量,符號的中間不能嵌有空白(空格、制表符、換行符等)。比如:==是單個符號,而= =是兩個等號。按照這個規則可能很輕松的判斷a+++b 表達式與a++ +b 一致。那++i+++i+++i;會被解析成什么樣子呢?希望讀者好好研究研究。另外還可以考慮一下這個表達式的意思:a+++++b;
C語言花括號{}
花括號每個人都見過,很簡單吧。但曾經有一個學生問過我如下問題:? ?char a[10] = {“abcde”};
他不理解為什么這個表達式正確。我讓他繼續改一下這個例子:
? ?char a[10] { = “abcde”};
問他這樣行不行。那讀者以為呢?為什么?
花括號的作用是什么呢?我們平時寫函數,if、while、for、switch 語句等都用到了它,但有時又省略掉了它。簡單來說花括號的作用就是打包。你想想以前用花括號是不是為了把一些語句或代碼打個包包起來,使之形成一個整體,并與外界絕緣。這樣理解的話,上面的問題就不是問題了。
C語言位運算符
C 語言中位運算包括下面幾種:- & 按位與
- | 按位或
- ^ 按位異或
- ~ 取反
- << 左移
- >> 右移
? ?a ^= b; b ^= a;a ^= b;但并不推薦這么做,因為這樣的代碼讀起來很費勁。
一、左移和右移
- 左移運算符“<<”是雙目運算符。其功能把“<< ”左邊的運算數的各二進位全部左移若干位,由“<<”右邊的數指定移動的位數,高位丟棄,低位補0。
- 右移運算符“>>”是雙目運算符。其功能是把“>> ”左邊的運算數的各二進位全部右移若干位,“>>”右邊的數指定移動的位數。但注意:對于有符號數,在右移時,符號位將隨同移動。當為正數時, 最高位補0;而為負數時,符號位為1,最高位是補0 或是補1 取決于編譯系統的規定。Turbo C 和很多系統規定為補1。
二、0x01<<2+3 的值為多少?
再看看下面的例子:? ?0x01<<2+3;
結果為7 嗎?測試一下。結果為32?別驚訝,32 才是正確答案。因為“+”號的優先級比移位運算符的優先級高(關于運算符的優先級,我并不想在這里做過多的討論,你幾乎可以在任何一本C 語言書上找到)。好,在32 位系統下,再把這個例子改寫一下:
? ?0x01<<2+30;或0x01<<2-3;
這樣行嗎?不行。一個整型數長度為32 位,左移32 位發生了什么事情?溢出!左移-1位呢?反過來移?所以,左移和右移的位數是有講究的。左移和右移的位數不能大于數據的長度,不能小于0。
C語言邏輯運算符||和&
||和&&是我們經常用到的邏輯運算符,與按位運算符|和&是兩碼事。下一節會介紹按位運算符。雖然簡單,但畢竟容易犯錯。看例子:int i=0;
int j=0;
if((++i>0)||(++j>0))
{
? ?//打印出i 和j 的值。
}
結果:i=1;j=0。
不要驚訝。邏輯運算符||兩邊的條件只要有一個為真,其結果就為真;只要有一個結果為假,其結果就為假。if((++i>0)||(++j>0))語句中,先計算(++i>0),發現其結果為真,后面的(++j>0)便不再計算。同樣&&運算符也要注意這種情況。這是很容易出錯的地方,希望讀者注意。
C語言單引號、雙引號
我們知道雙引號引起來的都是字符串常量,單引號引起來的都是字符常量。但初學者還是容易弄錯這兩點。比如:‘a’和“a”完全不一樣,在內存里前者占1 個byte,后者占2個byte。關于字符串常量在指針與數組那章將有更多的討論。這兩個列子還好理解,再看看這三個:
? ?1,‘1‘,“1”。
第一個是整形常數,32 位系統下占4 個byte;
第二個是字符常量,占1 個byte;
第三個是字符串常量,占2 個byte。
三者表示的意義完全不一樣,所占的內存大小也不一樣,初學者往往弄錯。字符在內存里是以ASCAII 碼存儲的,所以字符常量可以與整形常量或變量進行運算。如:‘A‘ + 1。
C語言接續符和轉義符
C 語言里以反斜杠(\)表示斷行。編譯器會將反斜杠剔除掉,跟在反斜杠后面的字符自動接續到前一行。但是注意:反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格。當然你可以測試一下加了空格之后的效果。我們看看下面的例子://這是一條合法的\
單行注釋
/\
/這是一條合法的單行注釋
#def\
ine MAC\
RO 這是一條合法的\
宏定義
cha\
r* s="這是一個合法的\\
n 字符串";
反斜杠除了可以被用作接續符,還能被用作轉義字符的開始標識。常用的轉義字符及其含義:
\n 回車換行
\t 橫向跳到下一制表位置
\v 豎向跳格
\b 退格
\r 回車
\f 走紙換頁
\\ 反斜扛符"\"
\' 單引號符
\a 鳴鈴
\ddd 1~3 位八進制數所代表的字符
\xhh 1~2 位十六進制數所代表的字符
廣義地講,C 語言字符集中的任何一個字符均可用轉義字符來表示。表中的\ddd 和\xhh正是為此而提出的。ddd 和hh 分別為八進制和十六進制的ASCII 代碼。如\102 表示字母"B",\134 表示反斜線,\X0A 表示換行等。
C語言注釋符號
一、幾個似非而是的注釋問題
C 語言的注釋可以出現在C 語言代碼的任何地方。這句話對不對?這是我當學生時我老師問的一個問題。我當時回答是不對。好,那我們就看看下面的例子:A)
int/*...*/i;
B)
char* s="abcdefgh //hijklmn";
C)
//Is it a \
valid comment?
D)
in/*…*/t i;
我們知道C 語言里可以有兩種注釋方式:/* */ 和//。那上面3 條注釋對不對呢?建議你親自在編譯器中測試一下。上述前3條注釋都是正確的,最后一條不正確。
A),有人認為編譯器剔除掉注釋后代碼會被解析成inti,所以不正確。編譯器的確會將注釋剔除,但不是簡單的剔除,而是用空格代替原來的注釋。再看一個例子:
? ?/*這是*/#/*一條*/define/*合法的*/ID/*預處理*/replacement/*指*/list/*令*/
你可以用編譯器試試。
B),我們知道雙引號引起來的都是字符串常量,那雙斜杠也不例外。
C),這是一條合法的注釋,因為\是一個接續符。關于接續符,下面還有更多討論。
D), 前面說過注釋會被空格替換,那這條注釋不正確就很好理解了。
現在你可以回答前面的問題了吧?但注意:/*…*/這種形式的注釋不能嵌套,如:
? ?/*這是/*非法的*/*/
因為/*總是與離它最近的*/匹配。
二、y = x/*p
y = x/*p,這是表示x 除以p 指向的內存里的值,把結果賦值為y?我們可以在編譯器上測試一下,編譯器提示出錯。實際上,編譯器把/*當作是一段注釋的開始,把/*后面的內容都當作注釋內容,直到出現*/為止。這個表達式其實只是表示把x 的值賦給y,/*后面的內容都當作注釋。但是,由于沒有找到*/,所以提示出錯。
我們可以把上面的表達式修改一下:
? ?y = x/ *p
或者
? ?y = x/(*p)
這樣的話,表達式的意思就是x 除以p 指向的內存里的值,把結果賦值為y 了。
也就是說只要斜杠(/)和星號(*)之間沒有空格,都會被當作注釋的開始。這一點一定要注意。
三、怎樣才能寫出出色的注釋
注釋寫得出色非常不容易,但是寫得糟糕卻是人人可為之。糟糕的注釋只會幫倒忙。1、安息吧,路德維希.凡.貝多芬
在《Code Complete》這本書中,作者記錄了這樣一個故事:有位負責維護的程序員半夜被叫起來,去修復一個出了問題的程序。但是程序的原作者已經離職,沒有辦法聯系上他。這個程序員從未接觸過這個程序。在仔細檢查所有的說明后,他只發現了一條注釋,如下:
? ?MOV AX 723h ;R.I.P.L.V.B.
這個維護程序員通宵研究這個程序,還是對注釋百思不得其解。雖然最后他還是把程序的問題成功排除了,但這個神秘的注釋讓他耿耿于懷。說明一點:匯編程序的注釋是以分號開頭。
幾個月后,這名程序員在一個會議上遇到了注釋的原作者。經過請教后,才明白這條注釋的意思:安息吧,路德維希.凡.貝多芬(Rest in peace, Ludwig Van Neethoven)。貝多芬于1827 年逝世,而1827 的十六進制正是723。這真是讓人哭笑不得!
2、windows 大師們用注釋討論天氣問題
還有個例子:前些日子windows 的源代碼曾經泄漏過一部分。人們在看這部分大師的經典作品時,卻發現很多與代碼毫無關系的注釋!有的注釋在討論天氣,有的在討論明天吃什么,還有的在罵公司和老板。這些注釋雖然與代碼無關,但總比上面那個讓貝多芬安息的注釋要強些的。至少不會讓你抓狂。不過這種事情只有大師們才可以做,你可千萬別用注釋
討論天氣。
3、出色注釋的基本要求
- 注釋應當準確、易懂,防止有二義性。錯誤的注釋不但無益反而有害。
- 邊寫代碼邊注釋,修改代碼同時修改相應的注釋,以保證注釋與代碼的一致性。不再有用的注釋要及時刪除。
- 注釋是對代碼的“提示”,而不是文檔。程序中的注釋應當簡單明了,注釋太多了會讓人眼花繚亂。
- 一目了然的語句不加注釋。例如:i++; /* i 加1 */——多余的注釋
- 對于全局數據(全局變量、常量定義等)必須要加注釋。
- 注釋采用英文,盡量避免在注釋中使用縮寫,特別是不常用縮寫。因為不一定所有的編譯器都能顯示中文,別人打開你的代碼,你的注釋也許是一團亂碼。還有,你的代碼不一定是懂中文的人閱讀。
- 注釋的位置應與被描述的代碼相鄰,可以與語句在同一行,也可以在上行,但不可放在下方。同一結構中不同域的注釋要對齊。
- 當代碼比較長,特別是有多重嵌套時,應當在一些段落的結束處加注釋,便于閱讀。
- 注釋的縮進要與代碼的縮進一致。
- 注釋代碼段時應注重“為何做(why)”,而不是“怎么做(how)”。說明怎么做的注釋一般停留在編程語言的層次,而不是為了說明問題。盡力闡述“怎么做”的注釋一般沒有告訴我們操作的意圖,而指明“怎么做”的注釋通常是冗余的。
- 數值的單位一定要注釋。注釋應該說明某數值的單位到底是什么意思。比如:關于長度的必須說明單位是毫米,米,還是千米等;關于時間的必須說明單位是時,分,秒,還是毫秒等。
- 對變量的范圍給出注釋。
- 對一系列的數字編號給出注釋,尤其在編寫底層驅動程序的時候(比如管腳編號)。
- 對于函數的入口出口數據給出注釋。
關于函數的注釋在函數那章有更詳細的討論。
C語言符號有哪些
符號有什么好說的呢?確實,符號可說的內容要少些,但總還是有些可以嘮叨地方。有一次上課,我問學生:‘/’這個符號在C 語言里都用在哪些地方?沒有一個人能答完整。這說明C 語言的基礎掌握不牢靠,如果真正掌握了C 語言,你就能很輕易的回答上來。這個問題就請讀者試著回答一下吧。本章不會像關鍵字一樣一個一個深入討論,只是將容易出錯的地方討論一下。
表(2.1)標準C 語言的基本符號


你也許聽說過“國際C 語言亂碼大賽(IOCCC)”,能獲獎的人毫無疑問是世界頂級C程序員。這是他們利用C 語言的特點極限挖掘的結果。下面這個例子就是網上廣為流傳的一個經典作品:
#i nclude <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?main(-79,-13,a+main(-87,1-_,
main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \}'+}##(!!/"):t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1):0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m.vpbks,fxntdCeghiry"),a+1);}
還沒發狂?看來你抵抗力夠強的。這是IOCCC 1988 年獲獎作品,作者是Ian Phillipps。
毫無疑問,Ian Phillipps 是世界上最頂級的C 語言程序員之一。你可以數數這里面用了多少個符號。當然這里我并不會討論這段代碼,也并不是鼓勵你也去寫這樣的代碼(關于這段代碼的分析,你可以上網查詢)。恰恰相反,我要告訴你的是:大師把代碼寫成這樣是經典,你把代碼寫成這樣是垃圾!所以在垃圾和經典之間,你需要做一個抉擇。