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