很多初學者弄不清指針和數組到底有什么樣的關系。我現在就告訴你:他們之間沒有任何關系!只是他們經常穿著相似的衣服來逗你玩罷了。
指針就是指針,指針變量在32 位系統下,永遠占4 個byte,其值為某一個內存的地址。指針可以指向任何地方,但是不是任何地方你都能通過這個指針變量訪問到。
數組就是數組,其大小與元素的類型和個數有關。定義數組時必須指定其元素的類型和個數。數組可以存任何類型的數據,但不能存函數。
既然它們之間沒有任何關系,那為何很多人把數組和指針混淆呢?甚至很多人認為指針和數組是一樣的。這就與市面上的C 語言的書有關,幾乎沒有一本書把這個問題講透徹,講明白了。
A)
char *p = “abcdef”;
B)
char a[] = “123456”;
1、以指針的形式訪問和以下標的形式訪問指針
例子A)定義了一個指針變量p,p 本身在棧上占4 個byte,p 里存儲的是一塊內存的首地址。這塊內存在靜態區,其空間大小為7 個byte,這塊內存也沒有名字。對這塊內存的訪問完全是匿名的訪問。比如現在需要讀取字符‘e’,我們有兩種方式:
1)
以指針的形式:*(p+4)。先取出p 里存儲的地址值,假設為0x0000FF00,然后加上4 個字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的值。
2)
以下標的形式:p[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。p[4]這個操作會被解析成:先取出p 里存儲的地址值,然后加上中括號中4 個元素的偏移量,計算出新的地址,然后從新的地址中取出值。也就是說以下標的形式訪問在本質上與以指針的形式訪問沒有區別,只是寫法上不同罷了。
2、以指針的形式訪問和以下標的形式訪問數組
例子B)定義了一個數組a,a 擁有7 個char 類型的元素,其空間大小為7。數組a 本身在棧上面。對a 的元素的訪問必須先根據數組的名字a 找到數組首元素的首地址,然后根據偏移量找到相應的值。這是一種典型的“具名+匿名”訪問。比如現在需要讀取字符‘5’,我們有兩種方式:
1)
以指針的形式:*(a+4)。a 這時候代表的是數組首元素的首地址,假設為0x0000FF00,然后加上4 個字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的值。
2)
以下標的形式:a[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。a[4]這個操作會被解析成:a 作為數組首元素的首地址,然后加上中括號中4 個元素的偏移量,計算出新的地址,然后從新的地址中取出值。
由上面的分析,我們可以看到,指針和數組根本就是兩個完全不一樣的東西。只是它們都可以“以指針形式”或“以下標形式”進行訪問。一個是完全的匿名訪問,一個是典型的具名+匿名訪問。一定要注意的是這個“以XXX 的形式的訪問”這種表達方式。
另外一個需要強調的是:上面所說的偏移量4 代表的是4 個元素,而不是4 個byte。只不過這里剛好是char 類型數據1 個字符的大小就為1 個byte。記住這個偏移量的單位是元素的個數而不是byte 數,在計算新地址時千萬別弄錯了。
main()
{
? ?int a[5]={1,2,3,4,5};
? ?int *ptr=(int *)(&a+1);
? ?printf("%d,%d",*(a+1),*(ptr-1));
}
打印出來的值為多少呢? 這里主要是考查關于指針加減操作的理解。
對指針進行加1 操作,得到的是下一個元素的地址,而不是原有地址值直接加1。所以,一個類型為T 的指針的移動,以sizeof(T) 為移動單位。因此,對上題來說,a 是一個一維數組,數組中有5 個元素; ptr 是一個int 型的指針。
&a + 1: 取數組a 的首地址,該地址的值加上sizeof(a) 的值,即&a + 5*sizeof(int),也就是下一個數組的首地址,顯然當前指針已經越過了數組的界限。
(int *)(&a+1): 則是把上一步計算出來的地址,強制轉換為int * 類型,賦值給ptr。
*(a+1): a,&a 的值是一樣的,但意思不一樣,a 是數組首元素的首地址,也就是a[0]的首地址,&a 是數組的首地址,a+1 是數組下一元素的首地址,即a[1]的首地址,&a+1 是下一個數組的首地址。所以輸出2*(ptr-1): 因為ptr 是指向a[5],并且ptr 是int * 類型,所以*(ptr-1) 是指向a[4] ,輸出5。
這些分析我相信大家都能理解,但是在授課時,學生向我提出了如下問題:在Visual C++6.0 的Watch 窗口中&a+1 的值怎么會是(x0012ff6d(0x0012ff6c+1)呢?
&a 代表的是數組的首地址,其值為0x0012ff6c。
a+1 的值是0x0012ff6c+1*sizeof(int),等于0x0012ff70。
問題就是&a+1 的值怎么會是(x0012ff6d(0x0012ff6c+1)呢?
按照我們上面的分析應該為0x0012ff6c+5*sizeof(int)。其實很好理解。當你把&a+1放到Watch 窗口中觀察其值時,表達式&a+1 已經脫離其上下文環境,編譯器就很簡單的把它解析為&a 的值然后加上1byte。而a+1 的解析就正確,我認為這是Visual C++6.0 的一個bug。既然如此,我們怎么證明證明&a+1 的值確實為0x0012ff6c+5*sizeof(int)呢?很好辦,用printf 函數打印出來。這就是我在本書前言里所說的,有的時候我們確實需要printf 函數才能解決問題。你可以試試用printf("%x",&a+1);打印其值,看是否為0x0012ff6c+5*sizeof(int)。注意如果你用的是printf("%d",&a+1);打印,那你必須在十進制和十六進制之間換算一下,不要冤枉了編譯器。
另外我要強調一點:不到非不得已,盡量別使用printf 函數,它會使你養成只看結果不問為什么的習慣。比如這個列子,*(a+1)和*(ptr-1)的值完全可以通過Watch 窗口來查看。
平時初學者很喜歡用“printf("%d,%d",*(a+1),*(ptr-1));”這類的表達式來直接打印出值,如果發現值是正確的就歡天喜地。這個時候往往認為自己的代碼沒有問題,根本就不去查看其變量的值,更別說是內存和寄存器的值了。更有甚者,printf 函數打印出來的值不正確,就措手無策,舉手問“老師,我這里為什么不對啊?”。長此以往就養成了很不好的習慣,只看結果,不重調試。這就是為什么同樣的幾年經驗,有的人水平很高,而有的人水平卻很低。其根本原因就在于此,往往被一些表面現象所迷惑。printf 函數打印出來的值是對的就能說明你的代碼一定沒問題嗎?我看未必。曾經一個學生,我讓其實現直接插入排序算法。很快他把函數寫完了,把值用printf 函數打印出來給我看。我看其代碼卻發現他使用的算法本質上其實是冒泡排序,只是寫得像直接插入排序罷了。等等這種情況數都數不過來,往往犯了錯誤還以為自己是對的。所以我平時上課之前往往會強調,不到非不得已,不允許使用printf 函數,而要自己去查看變量和內存的值。學生的這種不好的習慣也與目前市面上的教材、參考書有關,這些書甚至花大篇幅來介紹scanf 和printf 這類的函數,卻幾乎不講解調試技術。甚至有的書還在講TruboC 2.0 之類的調試器!如此教材教出來的學生質量
可想而知。
文件1 中定義如下:
? ?char a[100];
文件2 中聲明如下(關于extern 的用法,以及定義和聲明的區別,請復習第一章):
? ?extern char *a;
這里,文件1 中定義了數組a,文件2 中聲明它為指針。這有什么問題嗎?平時不是總說數組與指針相似,甚至可以通用嗎?但是,很不幸,這是錯誤的。通過上面的分析我們也能明白一些,但是“革命尚未成功,同志仍需努力”。你或許還記得我上面說過的話:數組就是數組,指針就是指針,它們是完全不同的兩碼事!他們之間沒有任何關系,只是經常穿著相似的衣服來迷惑你罷了。下面就來分析分析這個問題:
在第一章的開始,我就強調了定義和聲明之間的區別,定義分配的內存,而聲明沒有。
定義只能出現一次,而聲明可以出現多次。這里extern 告訴編譯器a 這個名字已經在別的文件中被定義了,下面的代碼使用的名字a 是別的文件定義的。再回顧到前面對于左值和右值的討論,我們知道如果編譯器需要某個地址(可能還需要加上偏移量)來執行某種操作的話,它就可以直接通過開鎖動作(使用“*”這把鑰匙)來讀或者寫這個地址上的內存,并不需要先去找到儲存這個地址的地方。相反,對于指針而言,必須先去找到儲存這個地址的地方,取出這個地址值然后對這個地址進行開鎖(使用“*”這把鑰匙)。如下圖:
這就是為什么extern char a[]與extern char a[100]等價的原因。因為這只是聲明,不分配空間,所以編譯器無需知道這個數組有多少個元素。這兩個聲明都告訴編譯器a 是在別的文件中被定義的一個數組,a 同時代表著數組a 的首元素的首地址,也就是這塊內存的起始地址。數組內地任何元素的的地址都只需要知道這個地址就可以計算出來。
但是,當你聲明為extern char *a 時,編譯器理所當然的認為a 是一個指針變量,在32 位系統下,占4 個byte。這4 個byte 里保存了一個地址,這個地址上存的是字符類型數據。雖然在文件1 中,編譯器知道a 是一個數組,但是在文件2 中,編譯器并不知道這點。大多數編譯器是按文件分別編譯的,編譯器只按照本文件中聲明的類型來處理。所以,雖然a 實際大小為100 個byte,但是在文件2 中,編譯器認為a 只占4 個byte。
我們說過,編譯器會把存在指針變量中的任何數據當作地址來處理。所以,如果需要訪問這些字符類型數據,我們必須先從指針變量a 中取出其保存的地址。如下圖:
2、定義為指針,聲明為數組
顯然,按照上面的分析,我們把文件1 中定義的數組在文件2 中聲明為指針會發生錯誤。
同樣的,如果在文件1 中定義為指針,而在文件中聲明為數組也會發生錯誤:
文件1
? ?char *p = “abcdefg”;
文件2
? ?extern char p[];
在文件1 中,編譯器分配4 個byte 空間,并命名為p。同時p 里保存了字符串常量“abcdefg”的首字符的首地址。這個字符串常量本身保存在內存的靜態區,其內容不可更改。在文件2中,編譯器認為p 是一個數組,其大小為4 個byte,數組內保存的是char 類型的數據。在文件2 中使用p 的過程如下圖:
通過上面的分析,相信你已經知道數組與指針的的確確是兩碼事了。他們之間是不可以混淆的,但是我們可以“以XXXX 的形式”訪問數組的元素或指針指向的內容。以后一定要確認你的代碼在一個地方定義為指針,在別的地方也只能聲明為指針;在一個的地方定義為數組,在別的地方也只能聲明為數組。切記不可混淆。下面再用一個表來總結一下指針和數組的特性:
指針就是指針,指針變量在32 位系統下,永遠占4 個byte,其值為某一個內存的地址。指針可以指向任何地方,但是不是任何地方你都能通過這個指針變量訪問到。
數組就是數組,其大小與元素的類型和個數有關。定義數組時必須指定其元素的類型和個數。數組可以存任何類型的數據,但不能存函數。
既然它們之間沒有任何關系,那為何很多人把數組和指針混淆呢?甚至很多人認為指針和數組是一樣的。這就與市面上的C 語言的書有關,幾乎沒有一本書把這個問題講透徹,講明白了。
一、以指針的形式訪問和以下標的形式訪問
下面我們就詳細討論討論它們之間似是而非的一些特點。例如,函數內部有如下定義:A)
char *p = “abcdef”;
B)
char a[] = “123456”;
1、以指針的形式訪問和以下標的形式訪問指針
例子A)定義了一個指針變量p,p 本身在棧上占4 個byte,p 里存儲的是一塊內存的首地址。這塊內存在靜態區,其空間大小為7 個byte,這塊內存也沒有名字。對這塊內存的訪問完全是匿名的訪問。比如現在需要讀取字符‘e’,我們有兩種方式:
1)
以指針的形式:*(p+4)。先取出p 里存儲的地址值,假設為0x0000FF00,然后加上4 個字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的值。
2)
以下標的形式:p[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。p[4]這個操作會被解析成:先取出p 里存儲的地址值,然后加上中括號中4 個元素的偏移量,計算出新的地址,然后從新的地址中取出值。也就是說以下標的形式訪問在本質上與以指針的形式訪問沒有區別,只是寫法上不同罷了。
2、以指針的形式訪問和以下標的形式訪問數組
例子B)定義了一個數組a,a 擁有7 個char 類型的元素,其空間大小為7。數組a 本身在棧上面。對a 的元素的訪問必須先根據數組的名字a 找到數組首元素的首地址,然后根據偏移量找到相應的值。這是一種典型的“具名+匿名”訪問。比如現在需要讀取字符‘5’,我們有兩種方式:
1)
以指針的形式:*(a+4)。a 這時候代表的是數組首元素的首地址,假設為0x0000FF00,然后加上4 個字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的值。
2)
以下標的形式:a[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。a[4]這個操作會被解析成:a 作為數組首元素的首地址,然后加上中括號中4 個元素的偏移量,計算出新的地址,然后從新的地址中取出值。
由上面的分析,我們可以看到,指針和數組根本就是兩個完全不一樣的東西。只是它們都可以“以指針形式”或“以下標形式”進行訪問。一個是完全的匿名訪問,一個是典型的具名+匿名訪問。一定要注意的是這個“以XXX 的形式的訪問”這種表達方式。
另外一個需要強調的是:上面所說的偏移量4 代表的是4 個元素,而不是4 個byte。只不過這里剛好是char 類型數據1 個字符的大小就為1 個byte。記住這個偏移量的單位是元素的個數而不是byte 數,在計算新地址時千萬別弄錯了。
二、a 和&a 的區別
通過上面的分析,相信你已經明白數組和指針的訪問方式了,下面再看這個例子:main()
{
? ?int a[5]={1,2,3,4,5};
? ?int *ptr=(int *)(&a+1);
? ?printf("%d,%d",*(a+1),*(ptr-1));
}
打印出來的值為多少呢? 這里主要是考查關于指針加減操作的理解。
對指針進行加1 操作,得到的是下一個元素的地址,而不是原有地址值直接加1。所以,一個類型為T 的指針的移動,以sizeof(T) 為移動單位。因此,對上題來說,a 是一個一維數組,數組中有5 個元素; ptr 是一個int 型的指針。
&a + 1: 取數組a 的首地址,該地址的值加上sizeof(a) 的值,即&a + 5*sizeof(int),也就是下一個數組的首地址,顯然當前指針已經越過了數組的界限。
(int *)(&a+1): 則是把上一步計算出來的地址,強制轉換為int * 類型,賦值給ptr。
*(a+1): a,&a 的值是一樣的,但意思不一樣,a 是數組首元素的首地址,也就是a[0]的首地址,&a 是數組的首地址,a+1 是數組下一元素的首地址,即a[1]的首地址,&a+1 是下一個數組的首地址。所以輸出2*(ptr-1): 因為ptr 是指向a[5],并且ptr 是int * 類型,所以*(ptr-1) 是指向a[4] ,輸出5。
這些分析我相信大家都能理解,但是在授課時,學生向我提出了如下問題:在Visual C++6.0 的Watch 窗口中&a+1 的值怎么會是(x0012ff6d(0x0012ff6c+1)呢?

上圖是在Visual C++6.0 調試本函數時的截圖。
a 在這里代表是的數組首元素的地址即a[0]的首地址,其值為0x0012ff6c。&a 代表的是數組的首地址,其值為0x0012ff6c。
a+1 的值是0x0012ff6c+1*sizeof(int),等于0x0012ff70。
問題就是&a+1 的值怎么會是(x0012ff6d(0x0012ff6c+1)呢?
按照我們上面的分析應該為0x0012ff6c+5*sizeof(int)。其實很好理解。當你把&a+1放到Watch 窗口中觀察其值時,表達式&a+1 已經脫離其上下文環境,編譯器就很簡單的把它解析為&a 的值然后加上1byte。而a+1 的解析就正確,我認為這是Visual C++6.0 的一個bug。既然如此,我們怎么證明證明&a+1 的值確實為0x0012ff6c+5*sizeof(int)呢?很好辦,用printf 函數打印出來。這就是我在本書前言里所說的,有的時候我們確實需要printf 函數才能解決問題。你可以試試用printf("%x",&a+1);打印其值,看是否為0x0012ff6c+5*sizeof(int)。注意如果你用的是printf("%d",&a+1);打印,那你必須在十進制和十六進制之間換算一下,不要冤枉了編譯器。
另外我要強調一點:不到非不得已,盡量別使用printf 函數,它會使你養成只看結果不問為什么的習慣。比如這個列子,*(a+1)和*(ptr-1)的值完全可以通過Watch 窗口來查看。
平時初學者很喜歡用“printf("%d,%d",*(a+1),*(ptr-1));”這類的表達式來直接打印出值,如果發現值是正確的就歡天喜地。這個時候往往認為自己的代碼沒有問題,根本就不去查看其變量的值,更別說是內存和寄存器的值了。更有甚者,printf 函數打印出來的值不正確,就措手無策,舉手問“老師,我這里為什么不對啊?”。長此以往就養成了很不好的習慣,只看結果,不重調試。這就是為什么同樣的幾年經驗,有的人水平很高,而有的人水平卻很低。其根本原因就在于此,往往被一些表面現象所迷惑。printf 函數打印出來的值是對的就能說明你的代碼一定沒問題嗎?我看未必。曾經一個學生,我讓其實現直接插入排序算法。很快他把函數寫完了,把值用printf 函數打印出來給我看。我看其代碼卻發現他使用的算法本質上其實是冒泡排序,只是寫得像直接插入排序罷了。等等這種情況數都數不過來,往往犯了錯誤還以為自己是對的。所以我平時上課之前往往會強調,不到非不得已,不允許使用printf 函數,而要自己去查看變量和內存的值。學生的這種不好的習慣也與目前市面上的教材、參考書有關,這些書甚至花大篇幅來介紹scanf 和printf 這類的函數,卻幾乎不講解調試技術。甚至有的書還在講TruboC 2.0 之類的調試器!如此教材教出來的學生質量
可想而知。
三、指針和數組的定義與聲明
1、定義為數組,聲明為指針文件1 中定義如下:
? ?char a[100];
文件2 中聲明如下(關于extern 的用法,以及定義和聲明的區別,請復習第一章):
? ?extern char *a;
這里,文件1 中定義了數組a,文件2 中聲明它為指針。這有什么問題嗎?平時不是總說數組與指針相似,甚至可以通用嗎?但是,很不幸,這是錯誤的。通過上面的分析我們也能明白一些,但是“革命尚未成功,同志仍需努力”。你或許還記得我上面說過的話:數組就是數組,指針就是指針,它們是完全不同的兩碼事!他們之間沒有任何關系,只是經常穿著相似的衣服來迷惑你罷了。下面就來分析分析這個問題:
在第一章的開始,我就強調了定義和聲明之間的區別,定義分配的內存,而聲明沒有。
定義只能出現一次,而聲明可以出現多次。這里extern 告訴編譯器a 這個名字已經在別的文件中被定義了,下面的代碼使用的名字a 是別的文件定義的。再回顧到前面對于左值和右值的討論,我們知道如果編譯器需要某個地址(可能還需要加上偏移量)來執行某種操作的話,它就可以直接通過開鎖動作(使用“*”這把鑰匙)來讀或者寫這個地址上的內存,并不需要先去找到儲存這個地址的地方。相反,對于指針而言,必須先去找到儲存這個地址的地方,取出這個地址值然后對這個地址進行開鎖(使用“*”這把鑰匙)。如下圖:

但是,當你聲明為extern char *a 時,編譯器理所當然的認為a 是一個指針變量,在32 位系統下,占4 個byte。這4 個byte 里保存了一個地址,這個地址上存的是字符類型數據。雖然在文件1 中,編譯器知道a 是一個數組,但是在文件2 中,編譯器并不知道這點。大多數編譯器是按文件分別編譯的,編譯器只按照本文件中聲明的類型來處理。所以,雖然a 實際大小為100 個byte,但是在文件2 中,編譯器認為a 只占4 個byte。
我們說過,編譯器會把存在指針變量中的任何數據當作地址來處理。所以,如果需要訪問這些字符類型數據,我們必須先從指針變量a 中取出其保存的地址。如下圖:

顯然,按照上面的分析,我們把文件1 中定義的數組在文件2 中聲明為指針會發生錯誤。
同樣的,如果在文件1 中定義為指針,而在文件中聲明為數組也會發生錯誤:
文件1
? ?char *p = “abcdefg”;
文件2
? ?extern char p[];
在文件1 中,編譯器分配4 個byte 空間,并命名為p。同時p 里保存了字符串常量“abcdefg”的首字符的首地址。這個字符串常量本身保存在內存的靜態區,其內容不可更改。在文件2中,編譯器認為p 是一個數組,其大小為4 個byte,數組內保存的是char 類型的數據。在文件2 中使用p 的過程如下圖:


