8.【C語言詳解】操作符
操作符分類
算術操作符
移位操作符
位操作符
賦值操作符
單目操作符
關係操作符
邏輯操作符
條件操作符
逗號表示式
下標引用、函式呼叫和結構成員
算術操作符
+ - * / %
- % 為取餘也叫模,它的運算元只能是整數,而其他的操作符的運算元既可以為整數也可以為浮點數。
- 運算元有浮點數時,則進行算術轉換進行浮點數運算。
移位操作符
左移操作符 <<
右移操作符 >>
移位操作符對應的是在計算機種的儲存形式:補碼 (因此移位操作符的運算元僅僅只能是整數。)
左移操作符
對相應的二進位制數進行操作。
規則:高位捨去,低位補零。
拓展
左移意味著每一個二進位制位的權重會增加1
例如:整形char
假設一個char型別的資料為 6
那麼它的二進位制表示為
00000110 //6
左移一位後:
00001100 //12
顯然任何二進位制數都可以表示為:
$Σ|\sum\limits_{i=0}^{n}$ k * 2i
(k為0或者1)
那麼左移一位後,在沒有高位溢位的情況下,每一位的權重都增加了1
那麼做簡單變形:
$Σ|\sum\limits_{i=1}^{n+1}$ k * 2i 根據分配律可得:$Σ|\sum\limits_{i=0}^{n}$ (k * 2i ) * 2
即乘2操作;
注意:僅僅是在無溢位的情況下
右移操作符
移位規則:
- 算數右移
左邊用符號位填充,右邊捨棄。
- 邏輯右移
左邊用0填充,右邊捨棄。
一般來說我們只針對邏輯右移來討論:
與左移不同的是,右移操作符在移位的過程中會經常低位非零而被捨棄的情況。
例如:將3右移1位
//邏輯右移
00000011
3 >> 1
00000001
假設 char a = n (n為任何數)
我們準備將 a 右移 k 個位置,可以將其分解為 k 次向右移動一個位置(二進位制位)
那麼每次分為兩種情況
- 最低位為1
- 最低位為0
最低位為1,即先捨去最低位,那麼對應到二進位制數則可通過 -1操作來完成,然後在移位。
最低位為0,那麼則直接移位,不涉及捨棄有效數問題
通過歸納總結:
可以得到 右移操作:
-
如果運算元為奇數 則先 -1 ,再右移。
-
如果為偶數,則直接右移。
通過左移操作的推到同理可得,右移可以是 除以2的操作,即與除以2的結果取整等效。
則右移與整除相同,都是結果取整。
如果是對負數進行移位操作呢?
在資料未溢位的情況下,結論是相同的。
左移 <==> 乘2
右移 <==> 除以2(整除)
思考:如果對-1,進行右移 n 位那麼結果如何?
11111111111111111111111111111111
//右移1位
11111111111111111111111111111111
可以看到是未變化的,通過上面得出的結論:
實際上 -1 右移可看作如下步驟:
- -1為奇數,因此先-1得到-2;
- 進行除以2的操作得到 -1
重複上述過程。
注意:對於移位運算子,移動負數位是標準為定義的。
位操作符
& 按位與
| 按位或
^ 按位異或
注意:運算元必須為整數
位操作符可以做很多有趣的事情。
對於位操作,我們通常只用進行某一位上對應的二進位制值,來獲得規律;
異或記住口訣:
相同為0, 相異為1.
可以試想兩個個極端情況:
- 一個數二進位制全為0
- 兩個數二進位制相同
其中一個數全為0的情況:
0 ^ 1 -> 1
0 ^ 0 -> 0
非零數的每一個二進位制都沒變
那麼可以輕鬆得出異或的值還是等於另一個非零的值
兩個數完全一樣的情況:
0 ^ 0 -> 0
1 ^ 1 -> 0
無論如何,最終二進位制位全為0,
即兩個相同值異或會得到 0 。
利用這個特性:
思考:如何不使用第三個變數交換兩個變數的值。
通常我們會寫出如下程式碼:
void swap(int* num1, int* num2)
{
int tmp = *num1;
*num1 = *num2;
*num2 = tmp;
}
但是這是不符合題意的,但通過位運算,我們可以做到。
void swap(int* n1, int* n2)
{
*n1 = *n1 ^ *n2;
*n2 = *n1 ^ *n2;
*n1 = *n1 ^ *n2;
}
拿a和b來舉例:
設未經過任何修改的a,b的值分別等於_a, _b的值
int main()
{
int a = 10;
int b = 20;
a = a ^ b; //(1)
b = a ^ b; //(2)
a = a ^ b; //(3)
printf("a = %d b = %d\n", a, b);
return 0;
}
經過(1)後,a的值實際上是 _a ^ _b
經過(2)後,b的值實際上是 _a ^ _b ^ _b == _a
經過(3)後,a的值實際上是 _a ^ _b ^ _a == _b
顯然目的已經達成。
思考:如何統計一個數它的二進位制位的1 的個數
試想,如果有一種操作可以每次消除掉一個二進位制的1,消除n次,那麼這個n就為1的個數
int numof1(int k)
{
int cnt = 0;
while (k)
{
k = k & (k - 1);//消除二進位制最後一位1
++cnt;
}
return cnt;
}
賦值操作符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
很簡單,=不再是數學種的判斷,而是賦值。
而像這樣 += 的複合操作符,可以寫成:
例如:
a = a + b <==> a += b;
int main()
{
int a = 10;
a += 20;
printf("%d\n", a);
return 0;
}
單目操作符
! 邏輯反操作
- 負值
+ 正值
& 取地址
sizeof 運算元的型別長度(以位元組為單位)
~ 對一個數的二進位制按位取反
-- 前置、後置--
++ 前置、後置++
* 間接訪問操作符(解引用操作符)
(型別) 強制型別轉換
int main()
{
int a = 10;
printf("%d", a++);
a = 10;
printf("%d", ++a);
return 0;
}
前置和後置++
++a稱作前置加加,a++稱作後置加加。
簡單記憶:
前置先++再使用,後置先使用再++。
實質上這裡的前後置++在C++中是可以通過函式過載來完成的,前置++返回的是加1後的值,而後置++是返回的加一前的值的一份拷貝。
sizeof
sizeof實際也是操作符,型別為size_t,可以是計算型別的大小,也可以是變數。
注意:sizeof(表示式),表示式種的值不會真的去執行,可以看作sizeof是去推導表示式的最終型別,而不會去執行表示式的內容。
int main()
{
int a = 10;
a += 1.1;
printf("%d\n", sizeof(a += 1));
printf("%d", a);
return 0;
}
思考:int a = 10; a += 1.1;最終a的值為多少?型別呢?
a += 1.1; <==> a = a + 1.1;
a + 1.1實際上會進行算數轉換,a會產生一個型別為double臨時拷貝,因此計算結果是 double的11.1,但是在賦值給a時,進行隱式型別轉換為int型別,得到11,賦值給a。
前置 -- 和後置 -- 同理。
如何驗證呢?
int main()
{
int a = 10;
printf("位元組數:%d\n", sizeof(a += 1.1));
printf("a:%d", a);
return 0;
}
int main()
{
int a = 10;
printf("位元組數:%d\n", sizeof(a + 1.1));
printf("a:%d", a);
return 0;
}
可以看到,這裡驗證了上面所說的情況。
a的值並沒有變,並且 a + 1.1的結果時double型別佔有8個位元組。
陣列 和 sizeof
陣列名普遍被認為是一個指標,可情況實際如此嗎?
int main()
{
int arr[10] = { 0 };
printf("%d", sizeof(arr));
return 0;
}
如果arr是個指標,那麼輸出應該為 4 或者 8
輸出:
輸出結果是40,顯然不是我們預想的。
實際上,陣列名是一個陣列型別。
例如:int arr[10] = {0};
那麼arr的型別實際上是 int[10];
我們通過typedef來驗證一下:
typedef struct A
{
int a;
}Type[10];
int main()
{
Type a;
printf("%d", sizeof(Type));
return 0;
}
輸出為:40
這裡的Type就是基於struct A[10]複合型別所定義出的一個新型別。
它的型別和我們struct A arr[10]創建出來的陣列是同一個型別,與Type arr等效。
可以看出這裡的arr實際上是Type型別,即struct A [10]型別。
所以,雖然通常情況下我們把陣列名當作指標來使用,但實際上它的型別並不是指標。
目前我們經常接觸到的只有兩個場景是將陣列名當作一個數組型別而不是指標。
- sizeof(arr)
- &arr
其他的情況我們視作其為指標是沒問題的。
void test(int arr[10])
{
printf("傳參後:%d\n", sizeof(arr));
}
int main()
{
int arr[10] = { 0 };
printf("傳參前:%d\n", sizeof(arr));
test(arr);
return 0;
}
怎麼陣列名作為實參傳遞至函式大小就不同了呢?
雖然我們的形參形式寫的是 int arr[10],但是其本質是int*型別,試想如果函式傳參拷貝每次都傳一整個陣列,那太浪費資源了,因此陣列名傳參,實際上形參是一個指標,指向陣列首元素。
這也印證了上面所說的兩種情況下,陣列名是陣列型別外,其他時候都視作為指標。
關係操作符
>
>=
<
<=
!= 用於測試“不相等”
== 用於測試“相等”
這些關係運算符比較簡單,沒什麼可講的,但是我們要注意一些運算子使用時候的陷阱。
注意:
在程式設計的過程中== 和=不小心寫錯,導致的錯誤。
邏輯操作符
&& 邏輯與
|| 邏輯或
牢記:
&& 只要有一個為假,結果就為假。
||只要有一個為真,結果就為真。
並且一旦可以推斷出結果真假,就不再執行後面的邏輯表示式。(短路)
int main()
{
int a = 0, b = 2, c = 3;
int i = 0;
i = a++ && b++ && ++c; (1)
printf("i==%d a==%d c==%d d==%d\n", i, a, b, c);
a = 0, b = 2, c = 3;
i = a++ || b++ || ++c; (2)
printf("i==%d a==%d c==%d d==%d\n", i, a, b, c);
return 0;
}
兩組測試c的值並不同,實際上是因為在(1)中,a++的值為假,就不在繼續執行後續表示式。
而d的值相同,則是在(2)中b++的值已經為真了,因此不再繼續執行後續表示式。
三目操作符
exp1 ? exp2 : exp3
例如:
int main()
{
int a = 0;
int ret = a == 0 ? 10 : 100;
printf("%d", ret);
}
簡言之就是如果exp1為真則執行exp2,否則執行exp3.
等效於:
int main()
{
int a = 0;
if (a == 0)
ret = 10;
else
ret = 100;
return 0;
}
我們可以利用三目操作符寫出簡潔的取兩數較大值的程式碼:
int ret = a < b ? b : a;
逗號表示式
exp1, exp2, exp3, …expN
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗號表示式
return 0;
}
c的值為13
逗號表示式,就是用逗號隔開的多個表示式。
逗號表示式,從左向右依次執行。整個表示式的結果是最後一個表示式的結果。
下標引用、函式呼叫和結構成員
- []下標引用操作符
其為雙目操作符,運算元為一個整形索引值和一個數組名或指標。
int main()
{
int arr[10] = { 0 };
arr[9];
return 0;
}
arr[9] == *(arr+9)== *(9 + arr) == 9[arr];
很奇怪的寫法,但確實存在。
- ( ) 函式呼叫操作符
接受一個或者多個運算元:第一個運算元是函式名,剩餘的運算元就是傳遞給函式的引數。
int Add(int n1, int n2)
{
return n1 + n2;
}
int main()
{
int a = 1, b = 2;
Add(a, b);
return 0;
}
運算元為Add和n1,n2.
- 訪問一個結構的成員
. 結構體.成員名
-> 結構體指標->成員名
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu)
{
stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//結構成員訪問
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//結構成員訪問
stu.age = 20;//結構成員訪問
set_age1(stu);
pStu->age = 20;//結構成員訪問
set_age2(pStu);
return 0;
}
表示式求值
表示式求值的順序一部分是由操作符的優先順序和結合性決定。同樣,有些表示式的運算元在求值的過程中可能需要轉換為其他型別。
隱式型別轉換
C的整型算術運算總是至少以預設整型型別的精度來進行的。
為了獲得這個精度,表示式中的字元和短整型運算元在使用之前被轉換為普通整型,這種轉換稱為整型提升。
為什麼有整形提升?
表示式的整型運算要在CPU的相應運算器件內執行,CPU內整型運算器(ALU)的運算元的位元組長度一般就是int的位元組長度,同時也是CPU的通用暫存器的長度。因此,即使兩個char型別的相加,在CPU執行時實際上也要先轉換為CPU內整型運算元的標準長度。
通用CPU(general-purpose CPU)是難以直接實現兩個8位元位元組直接相加運算(雖然機器指令中可能有這種位元組相加指令)。所以,表示式中各種長度可能小於int長度的整型值,都必須先轉換為int或unsigned int,然後才能送入CPU去執行運算。
int main()
{
char a, b = 2, c = 3;
printf("%d\n", sizeof(b + c));
a = b + c;
printf("%d", sizeof(a));
return 0;
}
輸出:
4
1
b + c的大小為4個位元組,即int的大小,而a為char。
過程:b和c的值被提升為普通整型,然後再執行加法運算,得到結果後將被截斷為char儲存到a。
如何進行整型提升
整形提升是按照變數的資料型別的符號位來提升的
-
有符號數
整型提升高位補充符號位
- 正數 高位補0
- 負數 高位補1
- 無符號數
高位直接補0
//負數的整形提升
char c1 = -1;
變數c1的二進位制位(補碼)中只有8個位元位:
1111111
因為 char 為有符號的 char
所以整形提升的時候,高位補充符號位,即為1
提升之後的結果是:
11111111111111111111111111111111
//正數的整形提升
char c2 = 1;
變數c2的二進位制位(補碼)中只有8個位元位:
00000001
因為 char 為有符號的 char
所以整形提升的時候,高位補充符號位,即為0
提升之後的結果是:
00000000000000000000000000000001
//無符號整形提升,高位補0
例子:
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
先轉換為二進位制位:
a:1011 0110 整形提升--》1111 1111 1111 1111 1111 1111 1011 0110
b:1011 0110 0000 0000 整型提升--》1111 1111 1111 1111 1011 0110 0000 0000
c:1011 0110 0000 0000 0000 0000 0000 0000 無需整形提升。
a,b整形提升之後,變成了負數,所以表示式 a == 0xb6 , b == 0xb600 的結果是假,但是c不發生整形提升,則表示式 c==0xb6000000 的結果是真.
因此整型提升後:a != 0xb6, b != 0xb600
只要參與運算就會嘗試整型提升。
判斷賦值等一切操作都被當做運算。
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
輸出:
1
4
4
因為+c和-c也被認為是執行了運算。
c只要參與表示式運算,就會發生整形提升,表示式 +c ,就會發生提升,所以 sizeof(+c) 是4個位元組.
表示式 -c 也會發生整形提升,所以 sizeof(-c) 是4個位元組,但是 sizeof(c) ,就是1個位元組.
算術轉換
如果某個操作符的各個運算元屬於不同的型別,那麼除非其中一個運算元的轉換為另一個運算元的型別,否則操作就無法進行。下面的層次體系稱為尋常算術轉換。
long double
double
float
unsigned long int
long int
unsigned int
int
一般來說按照上表從下至上進行轉換(向精度更高的型別去轉換)
如:1 + 1.1 則這裡的1會被轉換成double型別。
但是算術轉換要合理,要不然會有一些潛在的問題。
float f = 3.14;//賦值也是一種運算
int num = f;//隱式轉換,會有精度丟失
其實像這樣的轉換是不太合理但又不可避免的,通常會伴隨著精度的丟失,相當於強制轉換為了精度較低的型別。
int main()
{
size_t a = 0;
int b = -1;
if (b < a)
{
printf("<");
}
else
{
printf(">");
}
return 0;
}
輸出:
>
-1 > 0,這顯然不是這道題的解釋。
注意到 a為無符號整形,判斷 b < a時 a會算術轉換為 size_t;
-1的補碼為全1,而當其被當作無符號整形時就是一個非常大的數,然後再與0進行比較。
其實在整形家族的型別轉換中,無非就是符號位的不同識別方式,以及各種截斷,而浮點數型別想要和整形互相轉換,那麼由於儲存的方式的巨大差異,編譯器就不僅僅是截斷或者改變識別方式那麼簡單了,而是涉及一些複雜的處理,當然我們不必關心。
操作符的屬性
複雜表示式的求值有三個影響的因素。
-
操作符的優先順序
-
操作符的結合性(優先順序相同,決定從左向右運算還是從右向左)
-
是否控制求值順序。(&& , || , ? :,以及逗號表示式)
兩個相鄰的操作符先執行哪個?取決於他們的優先順序。如果兩者的優先順序相同,取決於他們的結合性。
先看優先順序,只有相鄰的操作符間才能通過優先順序確定計算順序。
操作符優先順序
優先順序不建議背,可以使用括號解決優先順序順序問題。
//表示式的求值部分由操作符的優先順序決定。
//表示式1
a*b + c*d + e*f
計算上表達式時,由於 * 比+的優先順序高,只能保證* 的計算是比+早,但是優先順序並不能決定第三個*比第一個+早執行。
所以表示式的執行順序可能是:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
//表示式2
c + --c;
註釋:同上,操作符的優先順序只能決定自減--的運算在+的運算的前面,但是我們並沒有辦法得知,+操作符的左運算元的獲取在右運算元取值之前還是之後,所以結果是不可預測的,是有歧義的。
//程式碼3-非法表示式
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
表示式3在不同編譯器中測試結果:非法表示式程式的結果.
//程式碼4
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);//輸出多少?
return 0;
}
雖然在大多數的編譯器上求得結果都是相同的。
但是上述程式碼 answer = fun() - fun() * fun();
中我們只能通過操作符的優先順序得知:先算乘法,再算減法。
函式的呼叫先後順序無法通過操作符的優先順序確定,也就是說運算元的確定順序和操作符優先順序無關。
//程式碼5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
看看同樣的程式碼產生了不同的結果,這是為什麼?
簡單看一下彙編程式碼, 就可以分析清楚.
這段程式碼中的第一個 + 在執行的時候,第三個++是否執行,這個是不確定的,因為依靠操作符的優先順序和結合性是無法決定第一個 + 和第三個前置 ++ 的先後順序。
總結:我們寫出的表示式如果不能通過操作符的屬性確定唯一的計算路徑,那這個表示式就是存在問題的。