1、位運算
可以使用 C 對變量中的個别位進行操作。您可能對人們想這樣做的原因感到奇怪。這種能力有時确實是必須的,或者至少是有用的。C 提供位的邏輯運算符和移位運算符。在以下例子中,我們将使用二進制計數法寫出值,以便您可以了解對位發生的操作。在一個實際程式中,您可以使用一般的形式的整數變量或常量。例如不适用
00011001
的形式,而寫為 25 或者 031 或者 0x19.在我們的例子中,我們将使用8位數字,從左到右,每位的編号是 7 到 0。
程式員書籍資源,值得收藏!,點選檢視
h這一定是你需要的電子書資源,全!值得收藏!
1.1 位邏輯運算符
4 個位運算符用于整型資料,包括 char。将這些位運算符成為位運算的原因是它們對每位進行操作,而不影響左右兩側的位。請不要将這些運算符與正常的邏輯運算符(&& 、||和!)相混淆,正常的位的邏輯運算符對整個值進行操作。
1.1.1 按位取反~
一進制運算符~将每個 1 變為 0,将每個 0 變為 1,如下面的例子:
~(10011010)
01100101
複制
假設 a 是一個
unsigned char
,已指派為 2。在二進制中,2 是
00000010
.于是 -a 的值為
11111101
或者 253。請注意該運算符不會改變 a 的值,a 仍為 2。
unsigned char a = 2; //00000010
unsigned char b = ~a; //11111101
printf("ret = %d\n", a); //ret = 2
printf("ret = %d\n", b); //ret = 253
複制
1.1.2 位與(AND): &
二進制運算符 & 通過對兩個操作數逐位進行比較産生一個新值。對于每個位,隻有兩個操作數的對應位都是 1 時結果才 為 1。
(10010011) & (00111101) = (00010001)
C 也有一個組合的位與-指派運算符:&=。下面兩個将産生相同的結果:
val &= 0377
val = val & 0377
複制
1.1.3 位或(OR): |
二進制運算符 | 通過對兩個操作數逐位進行比較産生一個新值。對于每個位,如果其中任意操作數中對應的位為 1,那麼結果位就為 1。
(10010011)| (00111101) = (10111111)
C 也有組合位或-指派運算符: |=
val |= 0377
val = val | 0377
複制
**1.1.4 位異或: **
二進制運算符^對兩個操作數逐位進行比較。對于每個位,如果操作數中的對應位有一個是 1(但不是都是1),那麼結果是 1.如果都是 0 或者都是 1,則結果位 0。
(10010011)^ (00111101) = (10101110)
C 也有一個組合的位異或 - 指派運算符: ^=
val ^= 0377
val = val ^ 0377
複制
1.1.5 用法
1.1.5.1 打開位
已知:10011010:
1.将位 2 打開
flag | 10011010
(10011010)|(00000100)=(10011110)
2.将所有位打開
flag | ~flag
(10011010)|(01100101)=(11111111)
1.1.5.2 關閉位
flag & ~flag
(10011010)&(01100101)=(00000000)
1.1.5.3 轉置位
轉置(toggling)一個位表示如果該位打開,則關閉該位;如果該位關閉,則打開。您可以使用位異或運算符來轉置。其思想是如果 b 是一個位(1或0),那麼如果 b 為 1 則 b^1 為 0,如果 b 為 0,則 1^b 為 1。無論 b 的值是 0 還是 1,0^b 為 b。
flag ^ 0xff
(10010011)^(11111111)=(01101100)
1.1.5.4 交換兩個數不需要臨時變量
//a ^ b = temp;
//a ^ temp = b;
//b ^ temp = a
(10010011)^(00100110)=(10110101)
(10110101)^(00100110)= 10010011
int a = 10;
int b = 30;
複制
1.2 移位運算符
現在讓我們了解一下 C 的移位運算符。移位運算符将位向左或向右移動。同樣,我們仍将明确地使用二進制形式來說明該機制的工作原理。
1.2.1 左移 <<
左移運算符<<将其左側操作數的值的每位向左移動,移動的位數由其右側操作數指定。空出來的位用 0 填充,并且丢棄移出左側操作數末端的位。在下面例子中,每位向左移動兩個位置。
(10001010) << 2 = (00101000)
該操作将産生一個新位置,但是不改變其操作數。
1 << 1 = 2;
2 << 1 = 4;
4 << 1 = 8;
8 << 2 = 32
複制
左移一位相當于原值 *2。
1.2.2 右移 >>
右移運算符>>将其左側的操作數的值每位向右移動,移動的位數由其右側的操作數指定。丢棄移出左側操作數有段的位。對于
unsigned
類型,使用 0 填充左端空出的位。對于有符号類型,結果依賴于機器。空出的位可能用 0 填充,或者使用符号(最左端)位的副本填充。
//有符号值
(10001010) >> 2
(00100010) //在某些系統上的結果值
(10001010) >> 2
(11100010) //在另一些系統上的結果
//無符号值
(10001010) >> 2
(00100010) //所有系統上的結果值
複制
1.2.3 用法:移位運算符
移位運算符能夠提供快捷、高效(依賴于硬體)對 2 的幂的乘法和除法。
number << n: number乘以2的n次幂
number >> n: 如果number非負,則用number除以2的n次幂
2、數組
2.1 一維數組
- 元素類型角度:數組是相同類型的變量的有序集合
- 記憶體角度:連續的一大片記憶體空間

在讨論多元數組之前,我們還需要學習很多關于一維數組的知識。首先讓我們學習一個概念。
2.1.1 數組名
考慮下面這些聲明:
int a;
int b[10];
複制
我們把 a 稱作标量,因為它是個單一的值,這個變量是的類型是一個整數。我們把 b 稱作數組,因為它是一些值的集合。下标和數名一起使用,用于辨別該集合中某個特定的值。例如,b[0] 表示數組 b 的第 1 個值,b[4] 表示第 5 個值。每個值都是一個特定的标量。
那麼問題是 b 的類型是什麼?它所表示的又是什麼?一個合乎邏輯的答案是它表示整個數組,但事實并非如此。在 C中,在幾乎所有數組名的表達式中,數組名的值是一個指針常量,也就是數組第一個元素的位址。它的類型取決于數組元素的類型:如果他們是int類型,那麼數組名的類型就是“指向 int 的常量指針”;如果它們是其他類型,那麼數組名的類型也就是“指向其他類型的常量指針”。
請問:指針和數組是等價的嗎?
答案是否定的。數組名在表達式中使用的時候,編譯器才會産生一個指針常量。那麼數組在什麼情況下不能作為指針常量呢?在以下兩種場景下:
- 當數組名作為sizeof操作符的操作數的時候,此時sizeof傳回的是整個數組的長度,而不是指針數組指針的長度。
- 當數組名作為&操作符的操作數的時候,此時傳回的是一個指向數組的指針,而不是指向某個數組元素的指針常量。
int arr[10];
//arr = NULL; //arr作為指針常量,不可修改
int *p = arr; //此時arr作為指針常量來使用
printf("sizeof(arr):%d\n", sizeof(arr)); //此時sizeof結果為整個數組的長度
printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*
複制
2.1.2 下标引用
int arr[] = { 1, 2, 3, 4, 5, 6 };
複制
*(arr + 3)
,這個表達式是什麼意思呢?
首先,我們說數組在表達式中是一個指向整型的指針,是以此表達式表示 arr 指針向後移動了 3 個元素的長度。然後通過間接通路操作符從這個新位址開始擷取這個位置的值。這個和下标的引用的執行過程完全相同。是以如下表達式是等同的:
*(arr + 3)
arr[3]
複制
問題1:數組下标可否為負值?
問題2:請閱讀如下代碼,說出結果:
int arr[] = { 5, 3, 6, 8, 2, 9 };
int *p = arr + 2;
printf("*p = %d\n", *p);
printf("*p = %d\n", p[-1]);
複制
那麼是用下标還是指針來操作數組呢?對于大部分人而言,下标的可讀性會強一些。
2.1.3 數組和指針
指針和數組并不是相等的。為了說明這個概念,請考慮下面兩個聲明:
int a[10];
int *b;
複制
聲明一個數組時,編譯器根據聲明所指定的元素數量為數組配置設定記憶體空間,然後再建立數組名,指向這段空間的起始位置。聲明一個指針變量的時候,編譯器隻為指針本身配置設定記憶體空間,并不為任何整型值配置設定記憶體空間,指針并未初始化指向任何現有的記憶體空間。
是以,表達式 *a 是完全合法的,但是表達式 *b 卻是非法的。*b 将通路記憶體中一個不确定的位置,将會導緻程式終止。另一方面b++可以通過編譯,a++ 卻不行,因為a是一個常量值。
2.1.4 作為函數參數的數組名
當一個數組名作為一個參數傳遞給一個函數的時候發生什麼情況呢?
我們現在知道數組名其實就是一個指向數組第 1 個元素的指針,是以很明白此時傳遞給函數的是一份指針的拷貝。是以函數的形參實際上是一個指針。但是為了使程式員新手容易上手一些,編譯器也接受數組形式的函數形參。是以下面兩種函數原型是相等的:
int print_array(int *arr);
int print_array(int arr[]);
複制
我們可以使用任何一種聲明,但哪一個更準确一些呢?答案是指針。因為實參實際上是個指針,而不是數組。同樣 sizeof arr 值是指針的長度,而不是數組的長度。
現在我們清楚了,為什麼一維數組中無須寫明它的元素數目了,因為形參隻是一個指針,并不需要為數組參數配置設定記憶體。另一方面,這種方式使得函數無法知道數組的長度。如果函數需要知道數組的長度,它必須顯式傳遞一個長度參數給函數。
2.2 多元數組
如果某個數組的維數不止1個,它就被稱為多元數組。接下來的案例講解以二維數組舉例。
void test01(){
//二維數組初始化
int arr1[3][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//列印二維數組
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j ++){
printf("%d ",arr1[i][j]);
}
printf("\n");
}
}
複制
2.2.1 數組名
一維數組名的值是一個指針常量,它的類型是“指向元素類型的指針”,它指向數組的第 1 個元素。多元數組也是同理,多元數組的數組名也是指向第一個元素,隻不過第一個元素是一個數組。例如:
int arr[3][10]
複制
可以了解為這是一個一維數組,包含了 3 個元素,隻是每個元素恰好是包含了 10 個元素的數組。arr 就表示指向它的第1個元素的指針,是以 arr 是一個指向了包含了 10 個整型元素的數組的指針。
2.2.2 指向數組的指針(數組指針)
數組指針,它是指針,指向數組的指針。
數組的類型由元素類型和數組大小共同決定:
int array[5]
的類型為
int[5]
;
C 語言可通過 typedef 定義一個數組類型:
定義數組指針有一下三種方式:
//方式一
void test01(){
//先定義數組類型,再用數組類型定義數組指針
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//有typedef是定義類型,沒有則是定義變量,下面代碼定義了一個數組類型ArrayType
typedef int(ArrayType)[10];
//int ArrayType[10]; //定義一個數組,數組名為ArrayType
ArrayType myarr; //等價于 int myarr[10];
ArrayType* pArr = &arr; //定義了一個數組指針pArr,并且指針指向數組arr
for (int i = 0; i < 10;i++){
printf("%d ",(*pArr)[i]);
}
printf("\n");
}
//方式二
void test02(){
int arr[10];
//定義數組指針類型
typedef int(*ArrayType)[10];
ArrayType pArr = &arr; //定義了一個數組指針pArr,并且指針指向數組arr
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}
//方式三
void test03(){
int arr[10];
int(*pArr)[10] = &arr;
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}
複制
2.2.3 指針數組(元素為指針)
2.2.3.1 棧區指針數組
//數組做函數函數,退化為指針
void array_sort(char** arr,int len){
for (int i = 0; i < len; i++){
for (int j = len - 1; j > i; j --){
//比較兩個字元串
if (strcmp(arr[j-1],arr[j]) > 0){
char* temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
}
}
}
}
//列印數組
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
void test(){
//主調函數配置設定記憶體
//指針數組
char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
//char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //錯誤
int len = sizeof(p) / sizeof(char*);
//列印數組
array_print(p, len);
//對字元串進行排序
array_sort(p, len);
//列印數組
array_print(p, len);
}
複制
2.2.3.2 堆區指針數組
//配置設定記憶體
char** allocate_memory(int n){
if (n < 0 ){
return NULL;
}
char** temp = (char**)malloc(sizeof(char*) * n);
if (temp == NULL){
return NULL;
}
//分别給每一個指針malloc配置設定記憶體
for (int i = 0; i < n; i ++){
temp[i] = malloc(sizeof(char)* 30);
sprintf(temp[i], "%2d_hello world!", i + 1);
}
return temp;
}
//列印數組
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
//釋放記憶體
void free_memory(char** buf,int len){
if (buf == NULL){
return;
}
for (int i = 0; i < len; i ++){
free(buf[i]);
buf[i] = NULL;
}
free(buf);
}
void test(){
int n = 10;
char** p = allocate_memory(n);
//列印數組
array_print(p, n);
//釋放記憶體
free_memory(p, n);
}
複制
2.2.4二維數組三種參數形式
2.2.4.1 二維數組的線性存儲特性
void PrintArray(int* arr, int len){
for (int i = 0; i < len; i++){
printf("%d ", arr[i]);
}
printf("\n");
}
//二維數組的線性存儲
void test(){
int arr[][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int arr2[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int len = sizeof(arr2) / sizeof(int);
//如何證明二維數組是線性的?
//通過将數組首位址指針轉成Int*類型,那麼步長就變成了4,就可以周遊整個數組
int* p = (int*)arr;
for (int i = 0; i < len; i++){
printf("%d ", p[i]);
}
printf("\n");
PrintArray((int*)arr, len);
PrintArray((int*)arr2, len);
}
複制
2.2.4.2 二維數組的3種形式參數
//二維數組的第一種形式
void PrintArray01(int arr[3][3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
//二維數組的第二種形式
void PrintArray02(int arr[][3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
//二維數組的第二種形式
void PrintArray03(int(*arr)[3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
void test(){
int arr[][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
PrintArray01(arr);
PrintArray02(arr);
PrintArray03(arr);
}
複制
2.3總結
2.3.1 程式設計提示
- 源代碼的可讀性幾乎總是比程式的運作時效率更為重要
- 隻要有可能,函數的指針形參都應該聲明為 const。
- 在多元數組的初始值清單中使用完整的多層花括号提高可讀性
2.3.2 内容總結
在絕大多數表達式中,數組名的值是指向數組第 1 個元素的指針。這個規則隻有兩個例外,sizeof 和對數組名&。
指針和數組并不相等。當我們聲明一個數組的時候,同時也配置設定了記憶體。但是聲明指針的時候,隻配置設定容納指針本身的空間。
當數組名作為函數參數時,實際傳遞給函數的是一個指向數組第 1 個元素的指針。
我們不單可以建立指向普通變量的指針,也可建立指向數組的指針。