C語言 數組名不是首地址指針


今天上計算機系統課的時候老師講到了C中的聚合類型的數據結構。在解釋數組名的時候說“數組名是一個指針,指向該數組的第一個元素”,附上ppt(第二行):




我覺得這是不正確的,是一個常見的由“簡化”產生的錯誤,數組名 != 指針。數組名是一個標識符,它標識出我們之前申請的一連串內存空間,而且這個空間內的元素類型是相同的——即數組名代表的是一個內存塊及這個內存塊中的元素類型只是在大多數情況下數組名會“退化”(C標准使用的decay和converted這兩個詞)為指向第一個元素的指針。 而指針不是一種聚合類的數據結構,它保存着某一種類型的對象的地址(void*除外),也說它指向這個對象。我們可以通過這個地址訪問這個對象。用一個圖來解釋,其中a代表了整個我們聲明的內存塊,p僅僅指向了一個char類型的對象:


C99 6.3.2.1 Lvalues, arrays, and function designators 中第三段是這樣說的:

Except when it is the operand of the sizeof operator or the unary & operator, or is a
string literal used to initialize an array, an expression that has type ‘‘array of type’’ is
converted to an expression with type ‘‘pointer to type’’ that points to the initial element of
the array object and is not an lvalue. If the array object has register storage class, the
behavior is undefined.

譯:除了在使用sizeof&運算符或者使用字符串字面量初始化數組之外,一個含有數組名的表達式會轉化為含有指向首元素的表達式,並且轉化后不是一個左值(這也是為什么我們不能修改這個標志符,例如val++,所以有的人也會說數組名是一個const指針,從本質上說這也是錯的)。如果數組的存儲類型是寄存器的話,行為是未定義的。(估計也沒人這么做吧。。)


下面我舉5個例子,123展示了數組名不是指針的情況,45表現的是數組名“退化”為指針:

本機環境


1.sizeof運算符(另外提一點,sizeof不是函數而是運算符)

可以看到,sizeof(a)打印出了整個數組的大小而非一個指針的大小,說明它不是一個指針。

2.&運算符

如果按照”數組名就是指針”的思想來,&a應該產生一個int**類型的指針,但是編譯器報了p1的警告:指針類型不兼容,而p2卻沒有報錯,那么p1和p2的區別在哪呢?

p1是一個指向一個指向整數指針的指針,如果我們進行p1++運算,得到的將是p1+8(我是64位環境)。而p2表示的是一個指向一個元素類型為整數,元素個數為5的內存塊的指針 ,如果我們進行p1++運算,得到的將是p1 + (4*5)。這也是為什么編譯器會報p1的警告。

3.使用字符串字面量初始化數組

就用上面的圖舉例子,如果我們聲明:

char a[] = "hello";
char *p = "hello";

對於第一行,其等價char a[6] = {'h', 'e', 'l', 'l', 'o', '\0'} ,編譯器會自動分配合理的空間,最終在內存中是這么個情況:

那有什么區別呢?

訪存方式和地區不一樣,例如,a[0]和p[0]都是'h',但是a[0]的操作是:來到a這個內存塊(大小為6字節) -> 取出第一個元素(偏移量為0),而且這個元素是在棧中的。而p[0]的操作是:來到p這個內存塊(大小為8字節,因為是64位環境),取出p的值,通過p獲取對於對象(一個字節)的值,而且這個對象是在.data段中的! (並且是只讀的)

4.算術運算與數組取下標操作符

在作為右值參與運算的時候,數組名會自動”退化“為指向首元素的指針,例如:

char a[] = "hello";
char *p = a + 1;

a會由char [5]類型退化為char *類型,所以這是可行的。

而我們常見的數組取下標操作符,c標准中對它的定義是等價於*(p + offset)運算。也是就說,你寫a[3]其實等價於*(a+3),可以看到括號內是一個算術運算,於是a“退化”為一個指針,隨后參與進行計算和解引用。有趣的是,由於加法的交換律,我們也可以寫成*(3+a),也是就3[a]。

不過平常最好別這么寫,不然別人會認為你在炫技或者腦袋有問題。。。

5.函數調用傳遞數組

我們學在給函數傳遞數組的時候,經常會聽到“按值傳遞機制和按引用傳遞機制 ”這樣的說法(網上也有很多),即傳遞數組是“按引用傳遞的”,這也是為什么傳遞數組在函數內讀寫數組,退出函數后數組會發生變化的原因。

其實,c語言傳參只有一種,就是傳遞值。

那么,數組為何被改變呢?

假設數組為int a[5], 對於函數原型,我們可以有以下幾種寫法:

void test(int a[5])

void test(int [5])

void test(int*)

許多人認為,第一種寫法是最好的,清晰(這個是對的,對於代碼閱讀者而言)而且可以告訴編輯器這個數組的大小。但是,這三種聲明在編譯器看來只有一種void test(int*), 所以那個5不過是一個心里安慰

所以說,test函數得到的是一個值為a“退化”后指向數組首元素(內存塊首地址)的指針 ,在test內部是不知到a是一個數組的,它僅僅認為它是一個整數指針。但是我們依然可以使用數組取下標操作符進行運算,因為即使a是一個數組名,它被用作數組取下標操作符的操作數時也會“退化”為指針(參見4)。

例如:

可以看到,在main函數中,編譯器認為a代表是一個數組(sizeof大小為4*5字節),而在test函數內部,a變成了一個指向整數的指針。(gcc發現了這個隱晦的可能導致錯誤的地方,給出了一個警告)




總之,指針就是保存地址的一個內存塊,數組名就是一連串相同類型元素組成的內存塊的標識符,兩個不是等價的。在大多數實際使用的情況下數組名會“轉化”為指向首元素的指針,也可以這么“簡單”的理解,但是我們還是要記住理解他們的本質差別。


另外推薦一個工具cdecl ,它可以將很多復雜的聲明用語句來解釋,例如int ((foo)(const void *))[3]這個很難明白的聲明:

參考

ISO/IEC 9899:TC3

Arrays and Pointers

stackoverflow1

stackoverflow2


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM