程序員C語言快速上手——基礎篇(三)

小拓展: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。

來總結一下

  1. ++作爲後綴時,自增表達式整體的值等於該變量初始值。如上例中int r = i++;,表達式整體的返回值r 等於i的初始值,而i未做自增運算前的初始值是0,所以r就是0。但是要注意,表達式一旦運行,i的值就會立刻發生變化,因此(i++)中,i的值是1

  2. ++作爲前綴時,自增表達式整體的返回值等於該變量運算之後的值。如上例中int r = ++i;r的值等於(++i)表達式運算之後i的實際值。

因此,遇到複雜的自增運算符時,只需要問自己兩個問題,自增變量的值是幾?表達式整體的返回值又是幾?下面我們看一個很常見的問題,問ij打印的值各是幾?

    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;
}

在這裏插入圖片描述
這次生成的彙編代碼稍複雜,簡單說明一下,edxeax都是32位通用寄存器,rax則是64位寄存器,在此處,可以把raxeax等同,可以看做是同一個寄存器。那麼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語言中,存在左值右值的概念。簡單說,=左邊的叫左值,右邊的叫右值。左值只能是計算機內存中的對象,而不能是常量或計算的結果。例如變量可以成爲左值,而像5i + 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. 先執行“表達式1”。
  2. 再執行“表達式2”,如果它的值爲真(非0),則執行循環體,否則結束循環。
  3. 執行完循環體後再執行“表達式3”。
  4. 重複執行步驟 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;

控制循環

在適當的時候,我們需要退出循環或跳過本次循環,這時候就需要控制循環。
控制循環通常使用breakcontinue關鍵字。

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);
}

歡迎關注我的公衆號:編程之路從0到1

編程之路從0到1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章