一、位運算
可使用 C 對變量中的個別位進行操做。您可能對人們想這樣作的緣由感到奇怪。這種能力有時確實是必須的,或者至少是有用的。C 提供位的邏輯運算符和移位運算符。在如下例子中,咱們將使用二進制計數法寫出值,以便您能夠了解對位發生的操做。在一個實際程序中,您可使用通常的形式的整數變量或常量。例如不適用 00011001
的形式,而寫爲 25 或者 031 或者 0x19.在咱們的例子中,咱們將使用8位數字,從左到右,每位的編號是 7 到 0。程序員
1.1 位邏輯運算符函數
4 個位運算符用於整型數據,包括 char。將這些位運算符成爲位運算的緣由是它們對每位進行操做,而不影響左右兩側的位。請不要將這些運算符與常規的邏輯運算符(&& 、||和!)相混淆,常規的位的邏輯運算符對整個值進行操做。學習
1.1.1 按位取反~url
一元運算符~將每一個 1 變爲 0,將每一個 0 變爲 1,以下面的例子:spa
~(10011010) 01100101
假設 a 是一個unsigned char
,已賦值爲 2。在二進制中,2 是00000010
.因而 -a 的值爲11111101
或者 253。請注意該運算符不會改變 a 的值,a 仍爲 2。.net
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。code
(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.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 個元素的指針。
咱們不單能夠建立指向普通變量的指針,也可建立指向數組的指針。