常常能夠看到「數組就是指針」、「數組名就是常量指針」這些說法,但真的是這樣嗎?讓咱們先看一下指針和數組的定義。html
1. 指針node
根據C99標準,指針的定義以下:linux
A pointer type may be derived from a function type or an object type, called the referenced type. A pointer type describes an object whose value provides a reference to an entity of the referenced type. A pointer type derived from the referenced type T is sometimes called 「pointer to T」. The construction of a pointer type from a referenced type is called "pointer type derivation".
指針是一種派生類型,它描述了這樣一個對象,其值爲對某種類型實體的引用。它包含了兩方面的涵義:實體和類型。ios
須要注意的是,指針變量只是指針的一種形態,但指針並不只僅只有指針變量。shell
2. 數組編程
數組的定義以下:小程序
An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T , the array type is sometimes called 「array of T」. The construction of an array type from an element type is called 「array type derivation」.
數組類型一樣是派生類型,它描述了某種對象的連續的非空集合,由其中元素類型和元素個數來刻畫。數組
由指針和數組定義能夠看出,指針和數組是徹底不一樣的類型。但數組名是指針嗎?ide
根據《征服C指針》一書,數組名並非指針,只不過在表達式中,數組名能夠解讀成「指向它的初始元素的指針」。函數
在一篇博文第二章 數組名是一個指針常量嗎?中,做者就分析的更加透徹:
「
數組名是一個指針常量這種觀點來源於數組名在表達式計算中與指針的結果等效性。例以下面的代碼:
1 int a[10], *p = a, *q; 2 q = a + 1; 3 q = p + 1;
在效果上看,a + 1與 p + 1是相同的,這很容易給人一種a就是p的假象,但,這僅僅是假象。
在《C與指針》一書中,做者用一個著名的例子闡述了數組名與指針的不一樣。在一個文件中定義:int a[10]; 而後在另外一個文件中聲明:extern int *a; 筆者不在這裏重複其中的原理,書中的做者試圖從底層操做上闡述數組名與指針的不一樣點,但筆者認爲這個例子存在一些不足,a在表達式中會轉換爲一個非對象的符號地址,而指針a倒是一個對象,用一個非對象去跟一個對象比較,有「偷跑」的嫌疑,這個例子只是說明了數組名的非對象性質,只能證實對象與非對象實體在底層操做上的不一樣,事實上,如上一章所述,指針也有非對象形態。筆者認爲,無須從底層的角度上花費那麼多脣舌,僅僅從字面上的語義就能夠推翻數組名是一個指針的觀點。
首先,在C/C++中,數組類型跟指針類型是兩種不一樣的派生類型,數組名跟指針是兩種不一樣類型的實體,把數組類型的實體說成「是」另外一個類型的實體,自己就是荒謬的;
其次,a + 1在效果上之因此等同於p + 1,是由於a進行了數組到指針的隱式轉換,這是一個轉換的過程,是converted to而不是is a的過程。若是是兩個相同的事物,又怎會有轉換的過程呢?當把a放在a + 1表達式中時,a已經從一個數組名轉換爲一個指針,a是做爲指針而不是數組名參與運算的;
第三,a + 1與p + 1是等效關係,不是等價關係。等價是相同事物的不一樣表現形式,而等效是不一樣事物的相同效果。把數組名說成是指針實際上把等效關係誤解爲等價關係。
所以,數組名不是指針,永遠也不是,但在必定條件下,數組名能夠轉換爲指針。
」
而根據《C和指針》一書的第8章8.1節,做者提到:「只有當數組名在表達式中使用時,編譯器纔會爲它產生一個指針常量」。注意這個值是指針常量,而不是指針變量。咱們不能修改常量的值。只有在兩種場合下,數組名在表達式中不用指針常量來表示——就是當數組名做爲sizeof操做符或單目操做符&的操做數時。sizeof返回整個數組的長度,而不是指向數組的指針的長度。取一個數組名的地址所產生的是一個指向數組的指針,而不是一個指向某個指針常量值的指針。
綜上所述,數組不是指針,數組名也只有在表達式中才會被當成一個指針常量。
對於下標運算符,相信你們都再熟悉不過了,咱們可用其方便快速訪問數組中的元素。但它和數組有關係嗎?先看一個例子:
1 #include <iostream> 2 using namespace std; 3 4 int main() 5 { 6 int p[10]; 7 for (int i = 0; i < 10; i++) 8 p[i] = i + 1; 9 10 cout << "*(p + i)\t" << "p[i]" << endl; 11 for (int i = 0; i < 10; i++) 12 cout << *(p + i) << "\t\t" << p[i] << endl; 13 14 return 0; 15 }
程序的輸出以下:
因此,*(p + i)跟p[i]的效果是同樣的。根據《征服C指針》,p[i]這種寫法只不過是*(p + i)的簡便寫法。實際上,至少對於編譯器來講,[]這樣的運算符徹底能夠不存在。[]運算符是爲了方便人們讀寫而引入的,是一種語法糖。
注意,認爲[]和數組沒有關係中的[]指的是表達式中出現的下標運算符,而不是聲明中的[]。
請看一小程序:
1 #include <iostream> 2 using namespace std; 3 4 void myFunc(int a[]) 5 { 6 cout << sizeof(a) << endl; 7 } 8 9 int main() 10 { 11 int a[]{1, 2, 3, 4}; 12 int *p = a; 13 cout << sizeof(a) << endl; 14 cout << sizeof(p) << endl; 15 myFunc(a); 16 17 return 0; 18 }
在本人機器上的運行結果以下;
很明顯,對於數組而言,sizeof的結果是數組所佔的全部字節數;而對於指針而言,sizeof的結果是指針類型的大小。爲何呢?由於:
1)對於數組而言,它的大小是固定的和已知的,因此sizeof求到的結果是數組所佔的全部字節數(雖然在表達式中數組名被看成指針處理);
2)對於指針而言,咱們只能知道它指向的內存的字節大小,而沒法知道它知道它指向的連續內存的字節大小(由於不清楚在哪裏結束),因此sizeof是沒法返回指針所指連續內存的字節大小。在這種狀況下,可能返回指針類型的大小可能較好。
另外,當把當數組做爲函數的參數進行傳遞時,該數組自動退化爲同類型的指針。因此函數myFunc輸出結果是4。
二級指針是指向指針的指針的簡稱,以下常見例子:
int **p;
而指針數組則是元素類型爲指針的數組,如:
int *p[10];
咱們知道指針和數組是不同的,固然二級指針和指針數組也是不同的。那他們有什麼聯繫呢?
下邊請看一個例子:
對於main函數,常見的其中兩種寫法以下:
int main(int argc, char *argv[]);
或
int main(int argc, char **argv);
根據Linux C編程一站式學習說法,函數原型中的[]
表示指針而不表示數組,等價於char **argv
。那爲何要寫成char *argv[]
而不寫成char **argv
呢?這樣寫給讀代碼的人提供了有用信息,argv
不是指向單個指針,而是指向一個指針數組的首元素。數組中每一個元素都是char *
指針,指向一個命令行參數字符串。
其實,就算在表達式中,它們也是等效的。
在表達式中,二級指針和指針數組是等效的。因此咱們下文能夠只以二級指針來講明。
對於一個二級指針而言,第一級指針指向一個指針數組的首元素,所以利用下標運算符[]便可得到指針數組的每個元素;而指針數組每一個元素存儲的是指針,能夠再額外指向另外一個數組。這樣一來,咱們就能夠利用二級指針來實現一個二維數組了,以下例:
先看一個簡單例子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 int main(int argc, char *argv[]) 6 { 7 int i; 8 for(i = 0; i < argc; i++) 9 { 10 printf("argv[%d]=%s\t", i, argv[i]); 11 int len = strlen(argv[i]), j; 12 for(j = 0; j < len; j++) 13 printf("%s[%d]=%c ", argv[i], j, argv[i][j]); 14 printf("\n"); 15 } 16 return 0; 17 }
在Linux下編譯執行:
主要參考:
Linus:利用二級指針刪除單向鏈表(有詳細解釋,很是值得參考!)
在刪除單向鏈表(保留頭指針head)時,咱們可能會採用比較典型的作法:
1 typedef struct node 2 { 3 struct node * next; 4 .... 5 } node; 6 7 typedef bool (* remove_fn)(node const * v); 8 9 // Remove all nodes from the supplied list for which the 10 // supplied remove function returns true. 11 // Returns the new head of the list. 12 node * remove_if(node * head, remove_fn rm) 13 { 14 for (node * prev = NULL, * curr = head; curr != NULL; ) 15 { 16 node * const next = curr->next; 17 if (rm(curr)) 18 { 19 if (prev) 20 prev->next = next; 21 else 22 head = next; 23 free(curr); 24 } 25 else 26 prev = curr; 27 curr = next; 28 } 29 return head; 30 }
但Linus大嬸就要說這樣子寫的人了:「This person doesn’t understand pointers」。那對於Linus大嬸來講,怎樣作纔是最好的?那就是利用二級指針,具體以下:
1 void remove_if(node ** head, remove_fn rm) 2 { 3 for (node** curr = head; *curr; ) 4 { 5 node * entry = *curr; 6 if (rm(entry)) 7 { 8 *curr = entry->next; 9 free(entry); 10 } 11 else 12 curr = &entry->next; 13 } 14 }
果真,改寫以後的程序簡潔了許多,並且也不須要維護一個prev表項指針和考慮頭指針的問題。
在博文Linus:利用二級指針刪除單向鏈表中,做者對利用二級指針的程序還附上了一個比較詳細的解說:
「
對於——
1)(第12行)若是不刪除當前結點 —— curr保存的是當前結點next指針的地址。
2)(第5行) entry 保存了 *curr —— 這意味着在下一次循環:entry就是prev->next指針所指向的內存。
3)(第8行)刪除結點:*curr = entry->next; —— 因而:prev->next 指向了 entry -> next;
」
《征服C指針》
《C和指針》