小拓展:C語言中int的正確使用姿勢
上一節已經講過,由於C語言中,整型的實際長度和範圍不固定的問題,會導致C語言存跨平臺移植的兼容問題,因此,C99標準中引入了stdint.h
頭文件,有效的解決了該問題。
#include<stdio.h>
#include<stdint.h>
int main(void){
// 使用stdint.h中定義的類型表示整數
int8_t a = 0;
int16_t b = 0;
int32_t c = 0;
int64_t d = 0;
// 前面加u,表示unsigned,無符號
uint32_t e = 0;
printf("int8 size is %d\n",sizeof(int8_t));
printf("int16 size is %d\n",sizeof(int16_t));
printf("int32 size is %d\n",sizeof(int32_t));
printf("int64 size is %d\n",sizeof(int64_t));
printf("uint32 size is %d\n",sizeof(uint32_t));
}
打印結果:
int8 size is 1
int16 size is 2
int32 size is 4
int64 size is 8
uint32 size is 4
int8_t
即表示8位整型,同理,int64_t
就是64位整型,類型定義明確清晰,且能兼容多種平臺。以上代碼,使用32位編譯器,編譯成32位系統下的程序後,運行得到的結果依然不變。這裏一定會有朋友質疑,爲什麼32位的系統下,還能表示並使用int64
這種64位的整型?這當然就是stdint.h
庫給我們帶來的便利了,簡單說一下原理,如果當前平臺的是32位的,那麼經過組合,我們可以使用兩個32位拼起來,不就能表示64位了嗎?同理,即使是8位的CPU,經過這種拼合思路,照樣能表示64位!當然,聰明人一眼就看出了弊端,使用這種拼合的方式,數據需要經過組合轉換,處理也更加複雜,同時還會帶來性能的損失,但是C99標準庫已經爲我們處理好了一切,雖然付出了一定的性能損失,但是成功的實現了C語言整型的跨平臺兼容,這樣的損失是完全值得的。
由於stdint.h
頭文件是C99標準引入的新特性,前面也說過微軟的VC編譯器不支持C99,那是不是VC就不能用了呢?好東西,當然人人眼饞,微軟雖然表面上說不支持C99,但是這種有用的特性還是會引入,因此VS2010也引入了stdint.h
頭文件,在VS2010及其以後的版本中,可以放心使用。但是要注意,只是引入了這個新特性,而不是支持C99。這裏就要吐槽了,目前還在使用VC6.0教學的,還是上個世紀的人麼?說和工具沒關係的這些人,害人匪淺。
語法基礎
表達式
與其他編程語言不同,C語言強調表達式而不是語句。表達式就如同計算值的公式,通過運算符把變量和常量組合起來。
算術運算符
主要包括加減乘除
+
、-
、*
、/
求餘數,即取模運算 %
二元的算術運算還包括自增和自減
++
、--
自增和自減運算符可以作爲前綴或後綴使用,如下
int i = 0;
i++;
++i;
那麼i++
和++i
的區別是什麼呢?
關於這兩者的區別,某些教材和網上一些資料是這樣解釋的,++
做前綴,是先讓i
加1,做後綴則後加1,既在下一行代碼前i
被加1。類似這種說法其實是不準確的,甚至是錯誤的,理解太過於表面,只是對現象的概括而已。這裏咱們就一次把這個問題徹底搞明白,永不犯迷糊。
前面已經說了,C語言強調的是表達式而不是語句,那麼表達式和語句有什麼區別呢?我個人認爲其中一個區別就是表達式整體一定有一個值,而語句可以沒有返回值。有其他編程基礎的朋友一定清楚所謂返回值的概念,那麼就是說表達式一定有一個返回值,或者應該說是表達式整體的值。
i++
作爲一個表達式,那麼他的表達式的值是什麼呢?其實我們可以用一個變量來保存表達式的值int r = i++;
int i = 0;
int r = i++;
printf("r=%d\n",r);
可以看到,表達式的r
值爲0。這個例子就很清楚了,所謂表達式的值,其實就是(i++)
整體的一個值,它是一個獨立的值。再運行下面的例子
int i = 0;
int r = ++i;
printf("r=%d\n",r);
可以看到,此時,表達式(++i)
整體的值r
變成了1。
來總結一下
-
當
++
作爲後綴時,自增表達式整體的值等於該變量初始值。如上例中int r = i++;
,表達式整體的返回值r
等於i
的初始值,而i
未做自增運算前的初始值是0,所以r
就是0。但是要注意,表達式一旦運行,i
的值就會立刻發生變化,因此(i++)
中,i
的值是1 -
當
++
作爲前綴時,自增表達式整體的返回值等於該變量運算之後的值。如上例中int r = ++i;
,r
的值等於(++i)
表達式運算之後i
的實際值。
因此,遇到複雜的自增運算符時,只需要問自己兩個問題,自增變量的值是幾?表達式整體的返回值又是幾?下面我們看一個很常見的問題,問i
和j
打印的值各是幾?
int i = 0;
int j = i++ + ++i;
printf("i=%d, j=%d\n",i,j);
按照我們上面講的知識來分解,先把式子拆分成(i++) + (++i);
,(i++)
這個表達式整體的值是0,但此時i
的值已經變成1了。而在(++i)
這個表達式中,i
的值則是1 + 1
,所以執行(++i)
後,i
的值爲2,那麼j
的值也就是0 + 2
大家千萬要記住,不管是i++
也好,++i
也罷,變量i
的值都會立刻增加,所以只看i
的值,這兩者是沒有區別的,它的區別在我們說的另一個概念上,也就是所謂的表達式的返回值。
好了,授人以魚不如授人以漁,如何證明我說的就是對的,別人的是錯誤的呢?C語言就是有一個好處,一切紛繁複雜的表象都能迴歸事物的本質。因爲C語言與彙編語言是一一對應的,因此我們只需要查看C語言翻譯成彙編語言後,在計算機內部到底發生了什麼就能掌握真理,而無需人云亦云。
爲了讓生成的彙編語言更簡單,我們去除頭文件,編寫最簡單的代碼test.c
int main(void){
int i = 0;
i++ + ++i;
return 0;
}
打開cmd命令行,使用gcc命令生成彙編源碼,這裏學習一個新的gcc參數-S
gcc -S test.c
打開生成的test.s
文件,這裏截取關鍵部分如下:
call __main
movl $0, -4(%rbp)
movl -4(%rbp), %eax
addl $1, %eax
movl %eax, -4(%rbp)
addl $1, -4(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
ret
這裏call __main
相當於main函數入口,ret
相當於return 0
,這之間一段也就對應我們的兩行C語言代碼。特別說明一下,這裏使用的gnu的工具鏈生成的是AT&T的x86-64彙編代碼,而非大家熟悉的intel 80386彙編。高校教的彙編語言都是intel x86的32位彙編,因此學過彙編的人可能也會感覺非常陌生。實際上這段彙編非常簡單,並不需要有什麼彙編基礎。
簡單解釋一下指令
movl
對應80386彙編中的mov
指令,是單詞move
的縮寫,表示傳遞數據,addl
則對應add
指令,表示加法器。這裏的-4(%rbp)
表示的是一個內存地址,eax
則是32位對應的8個寄存器中的第一個。
movl $0, -4(%rbp)
這句表示把一個常量0存到一個內存地址中,對應int i = 0;
此後,-4(%rbp)
這個地址就代指變量i
movl -4(%rbp), %eax
這句表示將變量i
中的值取出來放到一個名叫eax
的寄存器中。addl $1, %eax
則對應i++
,表示將常量1與寄存器eax
的值相加,然後存到eax
中,那麼此時eax
的值就是1。緊接着movl %eax, -4(%rbp)
,表示將寄存器eax
的值刷新到變量i
中,故而i++
後,i
的值立刻發生改變。
然後是addl $1, -4(%rbp)
,這句對應的C語言代碼是++i
,它表示將常量1直接與變量i
的值相加,結果仍然保存到變量i
中,那麼此時就是1+1
,故而變量i
最後等於2。
到這裏,其實彙編代碼就結束了,並沒有將(i++)
的整體結果與(++i)
的整體結果做最後的求和,這是因爲我們沒有用一個 變量來保存他們的和,所以編譯器對C語言代碼進行了優化,既然我們不需要結果,它乾脆就不計算了。
現在修改代碼,並再次生成彙編代碼
int main(void){
int i = 0;
int j = i++ + ++i;
return 0;
}
這次生成的彙編代碼稍複雜,簡單說明一下,edx
、eax
都是32位通用寄存器,rax
則是64位寄存器,在此處,可以把rax
和eax
等同,可以看做是同一個寄存器。那麼leal 1(%rax), %edx
則表示,將寄存器rax(即eax)
中的值加1,然後存到edx
寄存器中。-4(%rbp)
和-8(%rbp)
分別是變量i
和變量j
的內存地址,可以指代這兩個變量。
通過上述彙編代碼,我們可以清晰的發現,無論是i++
還是++i
,變量i
的值都會立刻被改變。
最後,關於i++
和++i
的闢謠:
有一些陳舊的資料中指出,++i
的性能要比i++
更好,因爲它是直接在內存中加1,在for
循環中,推薦使用++i
。讓我們再次編寫C代碼,生成彙編代碼來驗證這個觀點
int main(void){
int i = 0;
int j = 0;
i++;
++j;
}
彙編代碼
call __main
movl $0, -4(%rbp)
movl $0, -8(%rbp)
addl $1, -4(%rbp)
addl $1, -8(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
ret
可以看到,i++;
和++j;
生成的彙編代碼一模一樣,不存在誰性能更好的說法。現代編譯器中,都已做了優化處理,因此你喜歡寫那種風格都沒問題。
關係運算符
用於比大小的一些運算,其中==
表示兩者相等
<
、<=
、>
、>=
、==
邏輯運算符
這是任何一種編程語言都具備的,如下,表示邏輯與或非
&&
、||
、!
賦值運算符
=
表示賦值運算符,在C語言中,存在左值和右值的概念。簡單說,=
左邊的叫左值,右邊的叫右值。左值只能是計算機內存中的對象,而不能是常量或計算的結果。例如變量可以成爲左值,而像5
、i + 2
這樣的不能做左值。
注意,重點來了,C語言中=
運算符存在賦值陷阱!
首先看C語言的連環賦值語法
int i,j,k;
i = j = k = 0;
=
遵循右結合,所有它等價於i = (j = (k = 0))
,也就是說0先賦值給k
,然後k
的值再賦值給j
,以此類推。Ok,這樣是沒問題的。
再看如下代碼
int i;
float j;
j = i = 6.1f;
則j
最終的值變成了6.0
,這就是賦值陷阱。也就是說=
存在類型自動轉換的問題,值傳遞給i
時,自動轉化爲int型,丟棄了小數部分。
除此外,賦值運算符還存在複合用法如下
int8_t a = 0;
int16_t b = 0;
int32_t c = 0;
int64_t d = 0;
a += 1; // 等價於 a = a + 1
b -=1; // 等價於 a = a - 1
c *= 1; // 等價於 a = a * 1
d /=2; // 等價於 a = a / 1
d %=2; // 等價於 a = a % 1
運算符優先級
這裏給出一個簡單常見的優先級順序
優先級 | 類型 | 符號 |
---|---|---|
1 | 自增自減(後綴) | i++ 或i-- |
2 | 自增自減(前綴) | ++i 或--i |
3 | 乘法類 | * / % |
4 | 加法類 | + - |
5 | 賦值類 | = += -= …… |
分支與循環
條件分支
C語言的條件分支與其他語言相似
if-else
分支,如下結構,這是Linux C語言推薦的代碼範式,即將一個花括號緊跟小括號之後,寫在同一行。
if (1 > 0){
// do something
}else{
// do something
}
if
後面的條件表達式中存在陷阱,在C語言中沒有布爾類型,使用0和非0來表示false和true。因此很多人會想當然的以爲0是false,大於0就是true,實際上,-1
也是true,要注意,是一切非0值,包括小數也是true。
當if-else
中只有一句時,語法上是可以省略花括號的,但是不建議這樣,尤其包含嵌套的if語句時。C語言語法比較自由,正是如此,才更應該遵守規範。始終寫上花括號,養成良好的編程規範,使代碼易於閱讀和維護。
if(a>b) max=a;
else max=b;
// 或者放兩行
if(a>b)
max=a;
else
max=b;
多重條件的複合判斷
if(/*條件1*/){
//語句塊1
} else if(/*條件2*/){
//語句塊2
} else if(/*條件3*/){
//語句塊3
}else{
//語句塊n
}
當複合的條件過多時,直接使用if - else if - else
會顯得代碼冗長,因此C語言也提供了另一種語法編寫選擇分支,與Java、JavaScript等語言的switch相同
int a = 1;
switch(a){
case 1:
printf("Monday\n");
break;
case 2:
printf("Tuesday\n");
break;
case 3:
printf("Wednesday\n");
break;
case 4:
printf("Thursday\n");
break;
case 5:
printf("Friday\n");
break;
case 6:
printf("Saturday\n");
break;
case 7:
printf("Sunday\n");
break;
default:
printf("error\n");
break;
}
需要注意,case
後面必須是一個整數,或者是結果爲整數的表達式,但不能包含任何變量。
循環
while
最簡單的循環當是while
循環
while(/*表達式*/){
//語句塊
}
int i=1, sum=0;
while( i<=100 ){
sum+=i;
i++;
}
除此外,還存在while
循環的變體,do - while
循環
do{
//語句塊
}while(/*表達式*/);
//---------------------------------
int i=1, sum=0;
do{
sum+=i;
i++;
}while(i<=100);
do-while
循環與while
循環的不同在於,它會先執行“語句塊”,然後再判斷表達式是否爲真,如果爲真則繼續循環;如果爲假,則終止循環。因此,do-while 循環至少要執行一次“語句塊”。再使用do-while
循環時,要記住,while(i<=100);
的小括號後面必須跟一個分號。
for
C語言中更常用的可能是for
循環
for 循環的一般形式
for(表達式1; 表達式2; 表達式3){
語句塊
}
- 先執行“表達式1”。
- 再執行“表達式2”,如果它的值爲真(非0),則執行循環體,否則結束循環。
- 執行完循環體後再執行“表達式3”。
- 重複執行步驟 2 和 3,直到“表達式2”的值爲假,就結束循環。
// 使用for循環,進行等差數列求和
int sum=0;
for(int i=1; i<=100; i++){
sum+=i;
}
printf("%d\n",sum);
for 循環中的三個表達式都是可選項,都可以省略,但分號必須保留。
int i = 1, sum = 0;
for( ; i<=100; i++){
sum+=i;
}
// 省略兩個
for( ; i<=100 ; ){
sum=sum+i;
i++;
}
// 全部省略,表示死循環,等同於while(1){}
for( ; ; ){
// do something
}
實際上,for循環的靈活用法,完全可以替代while循環。另外,for循環中也能使用逗號表達式,當循環體只有一行時,亦可省略花括號
//表達式1 和 表達式3都是一個逗號表達式,即用逗號連接了兩個表達式。
for( i=0,j=100; i<=100; i++,j-- ) k=i+j;
控制循環
在適當的時候,我們需要退出循環或跳過本次循環,這時候就需要控制循環。
控制循環通常使用break
和continue
關鍵字。
當break
關鍵字用於 while、for 循環時,會終止循環而執行整個循環體後面的代碼。break 關鍵字通常和 if 語句一起使用,即滿足條件時便跳出循環
int i=1, sum=0;
while(1){ //死循環
sum+=i;
i++;
if(i>100) break; //滿足條件退出循環
}
continue
的作用是跳過本次循環中剩餘的語句而強制進入下一次循環。它只用在 while、for 循環中,常與 if 條件語句一起使用
// 打印奇數
for(int i=1; i<=100; i++){
if(i%2 == 0){ // 遇到偶數時跳過
continue;
}
printf("%d\n",i);
}