在上一篇《C專家編程》的讀書筆記中,我分享了我對前3章的一些心得體會,沒有看過的朋友能夠去這裏先閱讀那篇文章。這篇文章雖然是從第4章開始,但我只對其中的四、九、10這三章感興趣。所以我只寫了這3章的讀書筆記,若是不足,還請你們多多指出。編程
這章主要講了數組和指針的不一樣之處。但在切入正題以前,做者簡單介紹了下左值和右值。數組
什麼是左值了?左值就是一個在編譯時已肯定了的地址,這個地址能夠是可修改,也能夠是不可修改的,只是當其要做爲賦值語句的左值時,只能是可修改的左值(所以數組名不能被賦值);而右值就是一個值,其值到運行的時候纔可知。做者之因此要先講左值和右值是爲了說明指針和其餘類型的變量有很大的區別:全部的變量(不論是指針仍是其餘變量)在編譯時其地址就已知了。對於非指針的變量,咱們能夠直接根據其地址(也就是做爲左值的變量名)來改變其值。但對於指針而言,咱們須要修改的並非指針自身的值,而是指針所指空間的值。所以,指針相對於通常變量而言,須要多操做一步。函數
若是懂了上面講的左值和右值,你應該就不會輕易的混用指針和數組了。雖然指針和數組在做爲函數參數時候能夠互換,但這並不能說明在其餘狀況下,數組和指針也能隨意的互換。在這章中,做者用了一個簡單的例子來講明爲何指針和數組是不同的:spa
/* 在文件1中 */ int A[100]; /* 在文件2中 */ extern int *A; /* 一些引用A的代碼 */ ...
在上面這個例子中,咱們定義了一個數組A[100],而在另外一個文件中,咱們聲明瞭一個指針。若是數組和指針是同樣的,那這麼作就沒什麼問題。可它倆畢竟不同,這麼作會產生什麼很差的結果嗎?在給出結論前,咱們先看看指針和數組是如何訪問數據的。指針
首先咱們來看看數組是如何訪問數據的:code
對於數組而言,編譯器知道其起始地址,要訪問第 i
個元素,編譯器只須要在數組 a
的地址基礎上偏移 i
個單位地址就好了。
咱們再看看看指針是如何訪問數據的:內存
對於指針而言,編譯器知道指針的地址,但它的值倒是運行時候才知道的。在這個例子中,其中的值是5081,經過這個地址,指針能夠修改這個地址存儲的數據。rem
咱們再來看看指針是如何用下標訪問數據的:字符串
在編譯時,編譯器知道指針的地址;在運行時,指針得到其值(也就是指針指向的地址),根據這個值,再偏移 i
個單位便可訪問存儲在第 i
個位置的數據。編譯器
看完數組和指針的訪問方式,咱們再回頭看看以前那個問題,看看那樣寫是否有問題:
在文件2中,咱們聲明 extern int *A
,所以編譯器將其當作指針來處理。
既然 A
是指針, A[i]
的訪問方式應該和圖C同樣。首先獲取 A
的地址,而後取值,最後偏移再取數組第 i
個元素的值。
但咱們在文件1中定義的 A[100]
是數組,文件2中指針 A
的地址就應該是數組的起始地址。但編譯器卻將其存儲的值做爲了數據訪問的起始地址。所以,這麼作是嚴重錯誤的,極可能會產生嚴重的問題。
在後面的章節,做者還分了2章來詳細講解指針和數組,到時候咱們再來看指針和數組的更多細節。
在第4章中,做者簡單介紹了爲何數組和指針是不同的。在這一章中,做者進一步比較了它們的區別,同時介紹了多維數組。
在《The C Programming Language》這本書中,做者說:
做爲函數定義的形參時,
char s[]
和char *s
是同樣的。
但不少人卻忽略了條件,認爲在任何狀況下數組和指針都是同樣的。在實際使用過程當中,有3種狀況咱們能夠大膽地在數組和指針中進行轉換:
數組名被編譯器看成指向該數組第一個元素的指針。
數組的下標就是指針的偏移量。
在函數定義形參時,數組和指針是等效的。
對於第3種狀況,之因此數組和指針是等效的,是由於無論形參是指針仍是數組,編譯器都會將其轉換爲指向數組第一個元素的指針。爲何要這樣呢?由於在C中,實參傳遞給形參時,會複製一份實參,將複製後的數據傳給形參。若是要對數組進行復制,這樣會消耗大量的空間。爲了節省空間,提升效率,編譯器都會將數組或指針形式的形參按指針形式對待。所以,在這種狀況下,指針和數組都是同樣的。
指針和數組均可以利用下標的形式訪問內存空間,但指針和數組是不同的。在實際使用中,咱們只須要記住兩點:第一,數組名是指向數組第一個元素的指針;第二,數組和指針的定義必定要和聲明一致。
還有一種狀況要注意,由於數組名是不可修改的左值(在第4章中有介紹),即便數組名能夠看做指針,咱們也不能直接給其賦值。但在函數中,經過參數的傳遞,咱們能夠給其賦值:
void fun1(int arr[]) { arr[1] = 3; arr = array2; } vodi fun2() { array1[10], array2[10]; array1[0] = 3; array2[0] = 5; /* 編譯錯誤!*/ array1 = array2; }
除了一維數組,C語言還支持多維數組(更確切地說是數組的數組)。書中給出了一個例子,並用一幅圖來解釋多維數組是如何存儲的:
int array[2][4][5]; int (*a)[3][5] = array; // 1) int (*b)[5] = array[i]; // 2) int (*c) = array[i][j]; // 3) int d = array[i][j][k]; // 4)
咱們來分析下這段代碼,對於一維數組, int a[10]; int *p = a;
這種狀況, a
是數組的首地址(也是第一個元素的地址),而且該數組元素類型是 int
。所以 p
的類型是 int *
。咱們回到多維數組,在 1)
中, array
是三維數組的首地址,若是咱們將其看做數組的數組,那麼 array
實際上是一個元素爲 int [3][6]
,容量爲 2
的一維數組。所以類比一維數組,咱們該用 int (*a)[3][7]
這樣的指針來指向 array
。同理, array[0]
和 array[1]
都是元素爲一維數組的數組的首地址,所以咱們用 int (*b)[5]
來指向 array[i]
。
多維數組是比較難理解的,尤爲是和指針, &
, *
以及 malloc()
聯繫在一塊兒時。不過對於通常的狀況,按照上面的分析方法,將多維數組看成數組的數組來處理,會讓問題變得簡單一些。
這一章承接上一章對數組的討論,對於一個二維數組 A[m][n]
,編譯器將經過如下方式訪問元素 (i,j)
:
*(*(A + i) + j)
咱們簡單分析下, A
是二維數組名,也就是二維數組的首地址。其元素是一個一維數組,經過 *(A + i)
咱們訪問其第 i
個元素,也就是一個一維數組。這個元素的值是該一維數組的首地址(也就是這個一維數組的「名字」),所以咱們經過給它一個 j
的偏移,即可以訪問 (i,j)
這個元素了。
若是一個數組的元素是 char
類型的指針,而且每一個元素指向一個字符串,以達到相似二維數組的效果。那麼這種類型的指針數組就被稱爲Iliffe向量。Iliffe主要有兩個功能:
存儲長度不一的字符串
向函數傳遞長度不一的字符串數組
對編譯器而言,指針數組和二維數組均可以被解釋成 *(*(A + i) + j)
這種形式,但其底層原理卻徹底不一樣,這和第4章的分析相似,這裏我就不詳細講解了,直接把書上的圖貼上來:
咱們先看二維數組:
咱們再看指針數組:
本章最後,做者分析瞭如何優雅地向函數傳遞數組。對於一維數組而言,咱們定義形參爲指向數組第一個元素的指針,而對於傳入數組的長度,咱們一般有兩種方法:
增長一個額外的參數
將最後一個元素設置爲特殊值
而對於二維數組而言,狀況要複雜些,由於咱們要同時保證不能超越二維數組的行和列。書中給出了四種方法,同時指出最好的傳遞 A[i][j]
的方法是:
將
A[i][j]
改寫成A[i+1]
。A[j]
使用上面介紹的方式限制長度,A[i+1]
用於表示二維數組的行結束了(NULL
指針)。
指針和數組是C語言的難點也是重點,只有深刻理解了指針和數組的底層原理,咱們才能更好地使用它們。