C語言—運算子、表達式和語句

2020-08-11 18:05:45

基本運算子

C的基本運算子包括:+、-、*和/,C沒有指數運算子,但是C的標準數學庫中提供了一個pow()函數用於指數運算,例如pow(3.5,2.2)返回3.5的2.2次冪。
基本運算子這裏講一些注意點
這裏先說幾個術語:數據物件、左值、右值和運算子

  • 用於儲存值的數據儲存區域統稱爲 數據物件(data object)
  • 左值是C語言的術語,用於表示特定數據物件的名稱或表達式。因此,物件指的是實際的數據儲存,而左值是用於標識或定位儲存位置的標籤。對於早期C語言,提到左值意味着:
    1.它指定一個物件,可以參照記憶體中的地址。
    2.它可用在賦值運算子的左側。
    但是後來,標準中新增了const限定符。用const建立的變數不可修改。因此const識別符號滿足以上的第一項而不滿足第二項。爲此,C標準新增了一個術語:可修改的左值(modifiable lvalue),用於標識可修改的物件
  • 右值指的是能賦值給可修改左值的量,且本身不是左值

除法運算子:/

C語言中,整數除法的小數部分會被捨棄,這一過程稱爲截斷
混合整數和浮點數計算的結果是浮點數。計算機不能真正用浮點數處以整數,編譯器會把兩個物件轉換成相同類型,例如printf("7./4 is %1.2f \n",7./4)中,在進行除法運算前,整數會被轉換成浮點數`。

運算子優先順序

優先順序從高到低

運算子 結合律
() 從左往右
±(一元) 從右往左
* / 從左往右
± (二元) 從左往右
= 從右往左

考慮以下語句:
y = 6 * 12 + 5 * 20;
當不同運算子共用同一個運算物件時,優先順序決定了求值順序,即先進行兩個乘法。但優先順序並未規定先進行哪一個乘法,計算機根據不同的硬體來決定先計算前者還是後者,可能在一種硬體上採用某種方案的效率更高,而在另一種硬體上採用另一種方案效率更高。但無論採用哪種方案,表達式都是簡化成 72 + 100,雖然這並不影響結果,但是讀者可能誤用乘法從左往右的結合律來解釋這一點。 結合律只適用於同樣優先順序運算子共用同一運算物件的時候,例如表達式:12 / 3 * 2中,/ 和 * 的優先順序相同,共用物件爲3,因此結合律在這種情況起作用。

其它運算子

siezeof運算子和size_t型別

C標頭檔案系統中使用typedef 把 size_t 作爲 unsigned int 或 unsigned double 的別名。這樣在使用size_t 型別時,編譯器會根據不同的系統替換標準型別(有的系統sizeof()返回的是unsigned int型別,有的返回的是unsigned long 型別,參照size_t 統一返回值)。C99做了進一步調整,新增%zd 轉換說明用於printf()顯示size_t 型別的值。若系統不支援,可用%u 或 %lu替代 %zd。

求模運算子:%

  • 求模運算子只能用於整數,不能用於浮點數。
    求模運算子常用於控製程式流,例如,假設你正在設計一個賬單預算系統,每3個月要加進一筆額外費用,這種情況可以在程式中對月份求模3,並檢查結果是否爲0。若爲0,則加進額外費用。
    除法運算子得出的商的正負號和我們平常正常計算中的規則一致,而求模運算子得出的值的正負號與被除數一致。例如:
    11 % 5 得1 ;
    -11 % -5 得-1;
    -11% 5 得 -1

遞增運算子:++

shoe = 3.0;
while (shoe < 18.5)
{
	foot = SCALE * size + ADJUST;
	printf("%10.1f  %20.2f inches\n",shoe,foot);
	++shoe;
}
//可將複合語句中的自增放到while條件語句中去,
//即while(++shoe<18.5),並改寫shoe的初始值爲2.0。

這麼做的好處一是緊湊的程式碼能讓程式更爲簡潔,可讀性更高,更重要的是它把控制回圈的兩個過程集中在一個地方,這樣就不容易忘記更新回圈

遞減運算子:- -

優先順序

遞增運算子和遞減運算子都有很高的結合優先順序,只有圓括號的優先順序比它們高。因此,x * y++表示的是(x) * (y++),而不是(x * y)++,但是後者無效,因爲遞增和遞減運算子只能影響一個變數(即只能影響一個可修改的左值),而組合x*y本身是不可修改的左值

不要自作聰明

建議不要一次性複雜地使用太多遞增遞減運算子。
例如在函數中:
printf("%10d,%10d\n",num,num*num++);
C語言中,編譯器可以自行選擇先對函數中的哪個參數求值,如果在函數參數表中使用遞增遞減運算子,可能造成不必要的麻煩,例如以上程式只能在某些系統中正常執行。該程式的問題是,當printf()獲取待列印的值時,可能先對最後一個參數(num * num++)求值,這樣在獲取其它參數的值前就先遞增了num,得到結果6,25;但是它也有可能從右往左執行,對最右邊的num(++作用的num)使用5,然後遞增num,對第2個num和最左邊的num使用6,結果得到:6,30。
類似這樣的語句也會導致一些麻煩:
ans = num/2 + 5*(1 + num ++);
編譯器可能不會像你所想的先計算第1項(num/2)接着計算第2項(5 * (1 + num ++)),而是先計算第二項,遞增num,然後再num/2中使用num遞增後的新值。
遵循以下規則,可以避免類似的問題。

  • 如果一個變數出現在一個函數的多個參數中,不要對該變數使用遞增或遞減運算子。
  • 如果一個變數多次出現在一個表達式中,也不要對該變數使用遞增或遞減運算子。

表達式和語句

  • 每個表達式都有一個值
語句

語句是C程式的基本構建塊。一條語句相當於一條完整的計算機指令,但並不是所有的指令都是語句。
例如: x = 6 + (y = 5)
該語句中子表達式 y = 5 是一條完整的指令,但它只是語句的一部分而不是一條語句。

副作用和序列點

副作用是對數據物件或檔案的修改。如語句: states = 50; 它的副作用是將變數的值設定爲50。副作用?這似乎更像是主要目的!但從C語言的角度看,主要目的是對錶達式求值。跟賦值運算子一樣,遞增遞減運算子也有副作用,但使用它們的主要目的就是使用其副作用。
序列點是程式的執行點,在該點上,所有的副作用都在進入下一之前發生。在C語言中,語句中的分號標記了一個序列點。意思就是,在一條語句中,賦值、自增、自減運算子對運算物件做的改變必須在程式執行下一條語句之前完成。

型別轉換

1.當型別轉換出現在表達式時,無論是unsigned int 還是 signed的 char 和 short 都會被自動轉換成int ,必要時轉換成unsigned int (如果 short 與 int的大小相同時,unsigned short 就比int 大,這種情況下 unsigned short被轉換成unsigned int)。由於都是從較小型別轉換成較大型別,所以這種轉換被稱爲升級
2.涉及兩種運算型別的運算,兩個值會被分別轉換成兩種型別的更高級別。
3.型別的級別從高到低依次是: long double>double>float?unsigned long long>long long>unsigned long >long?unsigned int >int。
4.賦值表達式語句中,計算的最終結果會被轉換成被賦值變數的型別。這個過程可能導致型別升級降級
5.當作爲函數參數傳遞時,char和short被轉換成int,float被轉換成double。

型別升級通常不會有什麼問題,但是型別降級會導致真正的麻煩。因爲較低型別可能放不下較高型別的整個數位。例如一個8位元的char變數儲存101沒問題,但是存不下22334.
如果待轉換的值與目標型別不匹配怎麼辦?
1.目標型別是無符號整形,且待賦的值是整數時,額外的位會被忽略。例如,目標型別是8位元的unsigned char,待賦的值是原始值求模256。
2.如果目標型別是一個有符號的整形,且待賦的值是整數,結果因實現而異。
3.如果目標型別是一個整型,且待賦的值是浮點數,該行爲未定義。
強制型別轉換符
格式:(目標型別)待轉換的量

int mice;
mice = 1.6 + 1.7;
mice = (int)1.6 + (int)1.7;

第一行使用自動型別轉換,首先1.6+1.7得3.3,然後爲了匹配int型別的變數,3.3被型別轉換截斷爲整數3.
第二行,1.6和1.7在相加之前都被轉換成整數1,所以把1+1的和賦給變數mice。

帶參數的函數

假設自定義一個函數 void pound(int n){…},在主函數前宣告函數原型 void pound(int n),向函數傳遞一個char型別的‘!’,因爲原型的存在,編譯器會把該參數轉換成int型別,在ASCII中‘!’的數值是33,在系統中,該參數從1位元組的33變成4位元組的33以滿足函數的要求。同樣地,向函數傳遞一個浮點數也會因爲原型的存在而被轉換成合適的型別。
而在ANSI C之前,C語言使用的是函數宣告而不是函數原型,函數宣告只指明瞭函數名和返回型別,沒有指明參數型別。即使缺少函數宣告,char和short型別也會被自動升級爲int型別,但是float會被自動升級爲double型別傳輸給函數,從而導致輸出的內容不正確。