C語言:陣列和指標的區別

imred發表於2015-05-02

轉載請註明來源 http://blog.csdn.net/imred/article/details/45441457
實際上關於陣列與指標的區別這個問題在《C專家程式設計》已經有很詳細的闡釋,但我想用自己的語言說一說我的理解。

陣列是指標?

最近在做資料結構課設,其中一個函式發生了令人費解的錯誤,簡化後的程式碼如下:

#include <stdio.h>
int main()
{
    char foo[] = "abcde";
    char **bar = &foo;
    printf("%c\n", *(*bar));
    return 0;
}

程式執行到 printf 語句後便會掛掉,除錯時會提示一個SIGSEGV訊號,根據原來的經驗,這時程式試圖訪問本不應該訪問的記憶體。
原來在 C 語言課堂上老師經常提到陣列就是一個指標,指標也可以像陣列那樣用使用中括號的方式來進行記憶體訪問。以這樣的想法來分析前面的程式:foo 是一個字元指標,即 foo 的值即為“abcde”的首字元“a”的地址,*foo 即為 ‘a’;那麼 foo 這個指標一定存在某個記憶體單元,&foo獲得這個記憶體單元的地址,即 pfoo 是指向 foo 的指標,那麼*pfoo 得到 foo,*(*pfoo)應該得到‘a’了;這樣理解的話,程式是不應該有問題的。
下面我們使用指標代替陣列來實現上面的程式:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char *foo = (char *)malloc(sizeof (char) * 2);
    *foo = 'a';
    *(foo + 1) = 0;
    char **pfoo = &foo;
    printf("%c\n", *(*pfoo));
    return 0;
}

程式這次執行結果和預料的相同,輸出一個字母a。由此可見,陣列就是指標,這種說法是錯誤的。

陣列是靜態常量指標(static/Compile-time constant)?

有人認為陣列是一個靜態常量,即陣列名代表一個靜態的地址值,在編譯時確定,下面程式碼可以證偽這種說法

int main()
{
    char foo[] = {'a'};
    static char *p = foo;
    return 0;
}

使用 gcc 編譯時會有以下錯誤:

error: initializer element is not constant

可見陣列名並不是代表一個靜態量,並非地址常量。如果定義 foo 時加上 static 限定符,編譯就會通過,此時陣列名才代表了一個靜態量。

陣列是動態常量指標(const/Runtime constant)?

請看以下程式碼:

int main()
{
    char foo[] = {'a'};
    char * const bar;    //為什麼是這種寫法,請自行查閱相關資料
    char *baz;           
    foo = baz;           /* 1 */
    bar = baz;           /* 2 */
    return 0;
}

gcc 編譯時錯誤資訊為:

/* 1 */ error: incompatible types when assigning to type 'char[1]' from type 'char *'
/* 2 */ error: assignment of read-only variable 'bar'

1 2 兩處出錯資訊並不相同,若陣列為動態常量指標,出錯資訊應像 2 那樣。

陣列是什麼?

陣列既不是靜態常量,也不是指標,那麼陣列是什麼?

左值和右值

首先補充一些左值和右值的知識,引用《C專家程式設計》中的一段話:

出現在賦值符左邊的符號有時被稱為左值,出現在賦值符右邊的符號有時被稱為右值。編譯器為每個變數分配一個地址(左值)。這個地址在編譯時可知,而且該變數在執行時一直儲存於這個地址。相反,儲存於變數中的值(它的右值)只有在執行時才可知。如果需要用到變數中儲存的值,編譯器就發出指令從指定地址讀入變數值並將它存於暫存器中。

我對左值的理解和書上有些區別,我把這裡的“符號”稱為“物件”,每一個符號都代表一個物件,物件與地址是一一對應的。即如果宣告瞭 int a,那麼 a 作為一個左值時,a 即代表這個儲存在某個特定的地址的物件,對這個物件賦值即為把值放在這個特定的地址;a 作為右值時即代表 a 的內容,就是一個單純的值,而不是物件。一個值是不能作為左值的,比如一個常數 1, 1 = a 這樣的賦值語句是無法編譯通過的。在我看來,“左值”義同“物件”,“右值”義同“值”,所以下面“左值”和“物件”指的是相同的東西。但是“左值”又有一個子集:“可修改的左值”,只有這個子集中的東西才能放在賦值號左邊,因此我認為將引用中的第一句話修改為“出現在賦值符左邊的符號有時被稱為可修改的左值”更能表達其實際的意思。為什麼要引出這個子集,為的就是要把陣列分出來,陣列是左值,但並不是可修改的左值,因此你也不能直接把陣列名放在等號左邊進行賦值。

陣列就是陣列!

我先把結論放在這裡,然後在進行分析:陣列就是陣列,一個陣列名就代表一個陣列物件,這個物件內可以有一個或多個元素,每個元素型別都相同;正如 int 就是 int,一個 int 變數名就代表一個 int 型別物件。看到這裡,你可能要笑了,這不是什麼都沒說嗎,誰不知道陣列是這個意思啊,我想知道陣列和指標什麼關係。其實對陣列的認識就是這樣一個返璞歸真過程,看我來慢慢解釋。
以下程式碼:

/* 1.c */
int main()
{
    int foo[] = {1};
    int bar = 1;
    return 0;
}

使用 gcc 將其彙編並以 intel 格式輸出組合語言檔案:

gcc -S -masm=intel 1.c

關鍵部分為:

mov     DWORD PTR [esp+8], 1
mov     DWORD PTR [esp+12], 1

esp+8 位置就是那個 int foo[],esp+12 位置就是那個 int bar。可見,給 int 陣列的賦值時就像給一個 int 變數賦值一樣,並沒用指標來進行間接訪問,這個 int 陣列物件 foo 的記憶體地址在編譯時就確定了,是 esp+8;正如那個 int 物件 bar 一樣,它的記憶體地址在編譯時也確定了,是esp+12。
以示區別,我將下面程式碼同樣以組合語言輸出:

/* 2.c */
#include <stdlib.h>
int main()
{
    int *foo = (int *)malloc(sizeof (int));
    *foo = 1;
    return 0;
}

彙編的關鍵部分為:

mov     DWORD PTR [esp], 4
call    _malloc
mov     DWORD PTR [esp+28], eax
mov     eax, DWORD PTR [esp+28]
mov     DWORD PTR [eax], 1

前兩句為 foo 分配記憶體空間,第三句將分配的記憶體空間地址值賦給 foo,foo 的地址為 esp+28,編譯時已知。下面是賦值部分,首先從 foo 那裡得到地址值,然後向這個地址賦值,這裡可以看出和給陣列賦值的差別,給陣列賦值時是將值直接賦到了陣列中,而不用從哪裡得到陣列的地址。
由上面可以看出,陣列更像一個普通的變數,編譯時就知道了其地址,可以直接賦值。

陣列作為左值

陣列不能放在賦值號左邊,但陣列仍可以作為一個左值或者說物件出現在語句中,一個重要的例子就是取地址操作:&。取地址操作 &的運算元必須是一個左值,而不能是一個右值。比如一個變數int a = 1,&a 就可以得到 a 的地址,但 &1 是非法的,一個單純的數值是沒有地址的。那麼對於一個int foo[],&foo 會返回一個什麼樣的值呢?自然是一個指向陣列的指標咯,下面的程式可以看出來:

int main()
{
   int foo[1];
   int bar[1];
   bar = &foo;    //故意觸發一個 error
   return 0;
}

那個賦值語句一定會觸發一個的錯誤,我們可以根據編譯輸出來確定它們的型別,錯誤為:

error: incompatible types when assigning to type 'int[1]' from type 'int (*)[1]'

沒錯,&foo 返回資料型別為 int (*)[1],就是一個指向陣列的指標。指向陣列?指向陣列的哪裡呢?指向陣列物件首地址,正如一個指向 int 物件的指標指向那個 int 物件佔有的兩個或四個記憶體單元的首地址一樣。
把 &foo 賦給一個普通的指標是可以的,不過會觸發一個 warning,因為int * 與 int (*)[1] 並不相容。賦值後普通指標的值與 &foo 的值是相同的,都是陣列物件的首地址,只是普通指標把這塊記憶體當做 int 物件處理而已。
由於 C 語言是弱型別語言,你把 &foo 賦給int **********bar 或者 int *baz都是可以的,都不會導致 error,只會導致 warning,此時你列印出 *bar 或者 *baz 的值都是 foo 中第一個整數的值(前提是指標和陣列佔用空間大小相等)。正如文章開頭的程式碼那樣,以這個整數的值作為一個地址值進行間接訪問(*(*bar))就會導致非法訪問的錯誤。

陣列作為右值

陣列作為右值時會發生什麼?返回陣列物件內的所有值自然不可能,因此 C 語言中採取的方法是陣列作為右值時返回物件中元素型別的指標,指標指向第一個元素,類似上一個例子:

int main()
{
   int foo[1];
   int bar[1];
   bar = foo;    //故意觸發一個 error
   return 0;
}

出錯資訊為:

error: incompatible types when assigning to type 'int[1]' from type 'int *'

foo 作為右值時返回了一個 int *,就是這個特性給人造成了陣列就是指標的假象。

總結

陣列作為左值和陣列作為右值時的區別造成了無數人的困惑與誤解:foo 作為右值時確實等價於一個指標,因為陣列無法像普通物件那樣返回它的值,它的元素可能有成百上千個,但作為一個左值時——比如作為取地址操作符的運算元時,陣列就是作為一個陣列物件而出現的,而不是指標,取地址返回一個指向陣列的指標,而不是指向指標的指標。
一句話總結就是:陣列就是陣列,有著自己的特性。
(題外話:從生成的組合語言看,用指標來訪問記憶體實際上並不比使用陣列來訪問記憶體快,反而是慢了)
轉載請註明來源 http://blog.csdn.net/imred/article/details/45441457

相關文章