1.一個實例+理論分析
在了解數組和指針的訪問方式前提下,下面再看這個例子:
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個元素,所以a的類型是數組指針;ptr是一個int 型的指針,ptr的類型是整型指針。
&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。
2.Visual C++6.0上的真實調試結果
這些分析我相信大家都能理解,但是在授課時,學生向我(陳正沖老師)提出了如下問題:
在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));”這類的表達式來直接打印出值,如果發現值是正確的就歡天喜地。這個時候往往認為自己的代碼沒有問題,根本就不去查看其變量的值,更別說是內存和寄存器的值了。(嗯,這個壞習慣,我是有的。)
3. 最好不要利用printf函數進行調試
陳正沖老師的經驗與教誨:
更有甚者,printf函數打印出來的值不正確,就措手無策,舉手問“老師,我這里為什么不對啊?”。長此以往就養成了很不好的習慣,只看結果,不重調試。這就是為什么同樣的幾年經驗,有的人水平很高,而有的人水平卻很低。其根本原因就在於此,往往被一些表面現象所迷惑。printf函數打印出來的值是對的就能說明你的代碼一定沒問題嗎?我看未必。曾經一個學生,我讓其實現直接插入排序算法。很快他把函數寫完了,把值用printf函數打印出來給我看。我看其代碼卻發現他使用的算法本質上其實是冒泡排序,只是寫得像直接插入排序罷了。等等這種情況數都數不過來,往往犯了錯誤還以為自己是對的。所以我平時上課之前往往會強調,不到非不得已,不允許使用printf函數,而要自己去查看變量和內存的值。學生的這種不好的習慣也與目前市面上的教材、參考書有關,這些書甚至花大篇幅來介紹scanf和printf這類的函數,卻幾乎不講解調試技術。甚至有的書還在講TruboC 2.0之類的調試器!如此教材教出來的學生質量可想而知。
參考:陳正沖老師的《c語言深度剖析》。