數組和指針的一個問題?

假設 int array[10]={}, 數組在內存中的初始存儲地址為100,int型長度=4位元組,指針長度=4位元組,則 array+4=?

《C和指針》的答案給的是116(參見8.7第一題),但是我覺得不對,數組名在單目操作符之後,代表整個數組, array是int (*)[10]類型,即這個指針指向 int [10]類型的數據,也就是指向長度為10的int數組,sizeof(int [10])=40,

則 array+4 = 100 + 4*sizeof(int [10])=100+160=260. 我覺得是這樣的。

出於對《C和指針》作者的敬畏,我還是不敢肯定,總覺得可能自己漏掉了什麼。請各位C語言大神幫忙看一看這個問題,是我錯了嗎?


1 說寫程序驗證的其實沒啥意義,因為對於一個不知道過程的問題,你不能保證這個程序的結果不是:①undefined behavior implementation-defined 兩種情況之一。

恰好現在得票數最高的例子中的指針到int的類型轉換,拿這個例子來說,如果地址(比如32位)可以存儲到int整形就屬於 implementation-defined ,如果地址是64位的,那麼結果未定義。

參考C99標準的6.3.2.3 Pointers 的第6條:

Any pointer type may be converted to an integer type. Except as previously specified, the

result is implementation-defined. If the result cannot be represented in the integer type,

the behavior is undefined.

2 我開始也覺得你的想法是對的。。。然後一邊看世界盃一邊無聊翻了一下C99的標準手冊。。。

裡面的6.5.6 Additive operators的第8節裡面這麼描述的:

When an expression that has integer type is added to or subtracted from a pointer, the

result has the type of the pointer operand. .....(省略)....If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

所以我又去翻了一下《C語言程序設計:現代方法》第二版,裡面特別警告:

在一個不指向任何數組元素的指針上執行算數運算會導致未定義的行為。

3 很多書雖然經典,但是已經老了。比如《C缺陷與陷阱》裡面論述的很多問題在C語言標準化後已經不是問題了。

-------------------------------------------2014-6-19更新一下-------------------------------------------------------------

關於上面的回答,大致解釋一下我的理解,考慮到我的渣英語,如果錯誤歡迎指正~

首先解釋一下第一條提到的兩個坑,implementation-defined這個的意思是C標準不做要求,實現自行定義。為什麼要有這一條呢?因為C標準不知道編譯器所處的環境,上世紀七十年代的電腦主機千差萬別,底層硬體是什麼結構的都有,所以就由編譯器的具體實現自行去定義了。常見的implementation-defined 有int的size,size_t的size。還有我之前提到的指針類型和整形之間的相互轉換(前提是這個整形可以存儲到指針大小)。implementation-defined不是錯誤,是正確的寫法,唯一的問題是同樣的寫法可能換個編譯器實現結果就不同了。但是同一個編譯器實現的結果可以肯定是確定的行為。

undefined behavior 未定義行為則是一個C語言常見的錯誤,這個不是語法上的錯誤,而是語義上的錯誤。未定義行為一般都是一個非法的操作,最常見的未定義行為則是數組越界,指針指向不允許訪問的地方、指針轉換到存儲不了的整形等等。既然是未定義行為,則編譯器出現什麼結果都是可能的,可能報錯,也可能不報錯,或者電腦藍屏重啟爆炸,誰知道呢,因為 結果未定義。而且對於算術操作而言,並不像其他的未定義行為有那麼明顯的錯誤。比如初學者常見的一個未定義行為:a = i++ + i++ +i++;這個在所有的編譯器上都會有一個結果,而且很可能結果還會比較一致。但這依然是一個未定義行為。這就是為什麼我說對於不了解的東西,寫代碼看結果並不那麼保險的原因。

未定義行為有時候並不那麼明顯,寫的時候並不容易發現。比如在cocos2d-x 2.x裡面的CCTexture2D.cpp裡面到處充滿了形如

for(unsigned int i = 0; i &< length; ++i) { *outPixel16++ = (((*inPixel8++ 0xFF) &>&> 3) &<&< 11) | // R (((*inPixel8++ 0xFF) &>&> 2) &<&< 5) | // G (((*inPixel8++ 0xFF) &>&> 3) &<&< 0); // B }

這樣的寫法,用的時候真是提心弔膽。

好吧,扯完了這兩個坑完了,再來說說問題本身。

其實我開始也是跟樓主的想法相同,不過這種語言本身的不明確問題還是去翻了一下標準文檔:

6.5.6 加法操作符

裡面的4,5,6是算數操作,略過。。。

7) 在這個操作中,一個指向不是一個數組中元素的對象的指針,行為等同 指向一個長度為1,相同類型的數組首元素的指針。

8) 當一個表達式是一個從指針加或減一個整數時,結果是指針相同的類型。如果指針指向一個數組的元素,並且這個數組足夠大,則結果指向一個新元素的下標與原始元素的下標只差等於這個整數。換句話說,如果表達式P指向一個數組的第i個元素,表達式(P)+N和(P)-N分別表示指向數組的第i+n和第i-n個元素,前提是如果他們在數組中存在的話。此外,如果表達式P指向數組的最後一個元素,則表達式(P)+1指向數組最後一個元素後面的一個元素,並且如果表達式Q指向一個數組最後一個元素後面一個元素,則表達式(Q)-1指向數組的最後一個元素。如果全部指針操作數和結果指向相同數組中的元素,或者指向數組最後一個元素後面一個元素,則這個求值不會產生一個溢出;否則,這個行為是未定義的。如果結果指向數組最後一個元素後面的一個元素,則他不應當使用單目*運算符求值。

好吧,標準文檔中這兩條應該可以解釋題主的問題了。

按照我的理解,說明中的 the behavior只可能指的是前面說的加法求值這個操作,如果發現這個求值操作的結果指向了數組範圍內或者數組最後一個元素後面一個元素兩種情況之外的任何結果,都是未定義的行為。討論未定義行為的求值結結果是對是錯是沒有任何意義的。

評論中有人說這個加法操作是有定義的,而對這個指針指向的元素操作才是未定義。前半句我之前解釋了我的看法。後半句嘛,其實對於一個指針的任何解引用都有相應的標準說明,包括[]和*,都在文檔不同的地方定義了,相應的未定義行為標準也說明了。不可能出現一個*操作的未定義行為在加法的說明中定義。


在評論中已經爭論許久,現在整理成答案。

先說結論:

array + 4
// 確實是未定義的行為,但
array + 4 == 260
// 是可以確定的。

這看起來有點矛盾,但我認為這是最接近事實的描述。

首先看一下C標準的原文。

For the purposes of these operators, a pointer to an object that is not an element of an
array behaves the same as a pointer to the first element of an array of length one with the
type of the object as its element type.

When an expression that has integer type is added to or subtracted from a pointer, the
result has the type of the pointer operand. If the pointer operand points to an element of
an array object, and the array is large enough, the result points to an element offset from
the original element such that the difference of the subscripts of the resulting and original
array elements equals the integer expression. In other words, if the expression P points to
the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and
(P)-N (where N has the value n) point to, respectively, the i+n-th and i-n-th elements of
the array object, provided they exist. Moreover, if the expression P points to the last
element of an array object, the expression (P)+1 points one past the last element of the
array object, and if the expression Q points one past the last element of an array object,
the expression (Q)-1 points to the last element of the array object. If both the pointer
operand and the result point to elements of the same array object, or one past the last
element of the array object, the evaluation shall not produce an overflow; otherwise, the
behavior is undefined. If the result points one past the last element of the array object, it
shall not be used as the operand of a unary * operator that is evaluated.

從原文中我們可以總結出有定義的情況:

object + 1 // object 可以是任意類型
ptr + 1 // ptr 可以是除 void* 與函數指針以外的任意類型指針
array[i] + j // 假設 array 大小為 n 那麼 0 &<= i + j &<= n 其中 i != n ptr + j // 假設 ptr == array[i] 其餘同上

那麼現在讓我們考慮一下題目中的情況:

object + 4

對於object我們可以認為是某個長度為1的數組的首元素地址(依據C標準原文)。

所以我們可以改成這樣:

TYPE array[1];
array[0] + 4;

這是一個未定義的情況。

TYPE array[4];
array[0] + 4;

好了,這是一個有定義的情況。兩者的區別僅在於數組的大小。根據C標準,對於後者編譯器必須給出正確的地址,而對於前者則不用。(或者說對於第一種情況不存在正確地址)

如果編譯器不能夠發現這兩種情況下的array[0]是有區別的,就不得不在任何情況下都給出對於後者來說是正確的地址;而如果編譯器能夠發現這種區別,也就意味著編譯器實際上具備了檢查越界的功能。無論是哪一種情況,使用array[0] + 4都不可能出現隨機結果由此得出結論:

儘管嚴格來說array[0] + 4是未定義的,但其結果是可知的。

另一個證據:

void *calloc(size_t nmemb, size_t size);

C標準對它的描述是

The calloc function allocates space for an array of nmemb objects, each of whose size
is size. The space is initialized to all bits zero.

C標準明確的說了這個函數申請的是一個數組,可是這個函數返回的只是一個void*指針。這意味著編譯器必須做到僅通過一個指向數組首地址的void*指針就能訪問到一個數組中的所有元素。void*指針本身是不能夠通過下標訪問元素的,所以我們必須將其轉換成一個帶有類型的指針,而這個帶有類型的指針與calloc創建的數組之間沒有任何關係(因為它是獨立聲明並通過一個void*指針賦值得來的,關於數組的信息已經被完全抹掉了)。於是乎編譯器必須提供一種僅通過指針自身,而不用考慮指針實際指向的對象,就能夠進行地址計算的方案。

由此問題延伸出的思考:

問題:C標準為何要採取這種描述方式?

回答:C標準本身應該是與實現無關的,它不能決定編譯器究竟採取何種方式給出一個數組元素的地址,甚至不能要求同一數組中元素的地址必須是連續的。在這種情況下,C標準只能將定義的範圍嚴格限制在數組自身之內,至多要求一個有效的數組結束地址。

問題:既然array[0] + 4的結果是可知的,為什麼依然是未定義的?

回答:array[0] + 4返回的是一個地址,我們可以確實的知道這個地址會是多少,無論是否屬於未定義的情況——只要假設其符合有定義的情況即可,因為結果必須是一樣的。但是,我們不能保證得到的這個地址是有效的。而如果這個地址不一定是有效的,我們就沒法說明它的意義,也無法對其進行定義。


我也覺得是這樣的。


Talk is cheap,show me the code~~

out


我表示疑問,什麼樣的程序對array[10]會有array的用法,至少我從來沒看過。


你是對的。

《C和指針》給的答案是錯的。

===================

PS:《C和指針》里的錯誤不只這一處(《C和指針》的筆記),沒必要跪著讀書。

-----------------------------------------

@李車干

非常抱歉,我昨天的回答有個漏洞,看到了 @李洋 的回答我才想起來。

array+4實際上是未定義行為,C語言沒有規定這個運算的結果

因為C語言規定,指針加法運算得到的結果必須指向的還是數組元素,或者指向數組最後一個元素的下一個位置。

對於 int array[10];而言

array可以+0,+1,……+10,但不能+11。

而array最多可以+1,加2或更多則是未定義行為。因此《C和指針》這個題目本身就是錯誤的,就如同問 (i++) + (i++) + (i++)的值為多少一樣。正確的回答應該是這是未定義行為,不可能知道結果。這種代碼本身就是錯誤的。

在此,為昨天解答的漏洞向題主表示歉意,向 @李洋 表示感謝。


是這樣的。

詳情請看: http://www.cnblogs.com/xiahualin/p/3702491.html

在進入主題前,我們先看一個例子:

#include

int main()

{

int a[5] = { 1, 2, 3, 4, 5 };

int *ptr = (int *) (a + 1);

printf("%d,%d
", *(a + 1), *(ptr - 1));

return 0;

}

列印出來的值是多少呢?結果:2,5具體分析:

對指針進行加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。

我們來看一下 VS 中 Watch 窗口中的值:

a 在這裡代表的是數組首元素的地址即a[0]的首地址,其值為0x0082fb6c

a 代表的是數組的首地址,其值為0x0082fb6c

a+1 的值是0x0082fb6c + 1*sizeof(int),等於0x0082fb70

但是 a+1 的結果卻是 0x0082fb80

a+1 我們很容易理解,是數組第二個元素的首地址,但 a+1 呢?

其實,我們從一開始就沒有把概念理清,a 代表的是數組首元素的地址 ,即等同於 ( a[0] ) ,

而,a 代表的是數組的首地址,注意,是 數組,不妨我們用 a+1 的值減去 a 的值,0x0082fb80 - 0x0082fb6c = 20 = 5 * sizeof(int)

是不是發現了? 原來 a+1 中的加1 ,相當於加的是 a 這個數組的長度,這個要區別於 a+1 。

現在我們知道:

a+1 的值為 0x0082fb6c + 1*sizeof(int),等於0x0082fb70

a+1 的值為 0x0082fb6c + sizeof(a)* sizeof(int),等於0x0082fb80 (這裡有一點需要注意,當 a 作為實參傳遞進函數時,32位機子下 sizeof ( a) 不是數組元素個數,而是 4 )


array是100 array還是100嗎?


推薦閱讀:

C/C++ 的語法是 LL(1) 語法嗎?
請問:".h"頭文件是用什麼語言編寫的?
char*(*(*a)(void))[20];這個是個什麼意思?
在C語言中,如何安全地使用void*?

TAG:C編程語言 | CC | 指針 |