天天看點

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

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

C語言 數組名不是首位址指針
我覺得這是不正确的,是一個常見的由“簡化”産生的錯誤,數組名 != 指針。數組名是一個辨別符,它辨別出我們之前申請的一連串記憶體空間,而且這個空間内的元素類型是相同的——即數組名代表的是一個記憶體塊及這個記憶體塊中的元素類型 。隻是在大多數情況下數組名會“退化”(C标準使用的decay和converted這兩個詞)為指向第一個元素的指針。 而指針不是一種聚合類的資料結構,它儲存着某一種類型的對象的位址(void*除外),也說它指向這個對象。我們可以通過這個位址通路這個對象。用一個圖來解釋,其中a代表了整個我們聲明的記憶體塊,p僅僅指向了一個char類型的對象:
C語言 數組名不是首位址指針
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表現的是數組名“退化”為指針:

本機環境

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

1.

sizeof

運算符(另外提一點,

sizeof

不是函數而是運算符)

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

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

2.

&

運算符

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

如果按照”數組名就是指針”的思想來,&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'}

,編譯器會自動配置設定合理的空間,最終在記憶體中是這麼個情況:

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

那有什麼差別呢?

訪存方式和地區不一樣,例如,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]。

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

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

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)。

例如:

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

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

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

另外推薦一個工具

cdecl

,它可以将很多複雜的聲明用語句來解釋,例如int ((foo)(const void *))[3]這個很難明白的聲明:

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

參考

ISO/IEC 9899:TC3

Arrays and Pointers

stackoverflow1

stackoverflow2