1. 程式人生 > >C 語言返璞歸真: 指標篇(2)

C 語言返璞歸真: 指標篇(2)

前言

上篇博文講到 C 語言指標的基本使用(宣告和初始化以及解引用), 對指標有了基本的瞭解之後,就來說說指標的一個曖昧物件——陣列。

陣列是什麼?

陣列是幾乎所有程式語言都有的一種型別, 實際上它也是最簡單的一種資料結構。 在 C 語言當中, 陣列由一組數目固定的、資料型別相同的元素構成。 當用戶定義一個數組, 系統就會在記憶體中自動分配一塊連續的大小固定的棧記憶體供其使用。 陣列的定義和使用想必讀者都很清楚, 這裡就不再贅述了。 下面我們先來說說指標和陣列為什麼曖昧。

指標和陣列名

說指標和陣列曖昧, 曖昧在哪裡? 其實就曖昧在指標和陣列名。 經常可以看到類似於如下的一段程式:

int a[10] = {0,};
int *pa = NULL;
pa = a;

為了更加方便地說明陣列和指標的關係, 可以看一下一維陣列在記憶體中的排列方式:


圖中開闢了兩塊記憶體,分別用於儲存一個長度為 10 的陣列 a (其資料型別為 int)和一個指標 pa(其資料型別為 int)。 並且指標指向了陣列 a:「pa = a」。可能對於 C 初學者而言,「 pa = a 」實際上並不是個容易理解的表示式, 更加一目瞭然的語句應該是「 pa = &a[0] 」。在上篇博文中說過指標的初始化是通過對目標物件進行取址運算完成, 而這裡卻使用了陣列名 a 來代替 &a[0], 這說明陣列名 a 出現在賦值運算子「=」右邊時和 &a[0] 是等效的, 都代表了

陣列首元素的地址。 但是不能將陣列名等價於陣列首元素的地址, 實際上陣列名的型別就是是陣列型別——TYPE [], 陣列名只是一個符號, 在記憶體中並沒有實際的空間用於儲存陣列名 a 。但是在大多數情況下陣列名會隱式轉換為指標型別。

從上面的圖看到訪問陣列成員可以使用兩種方式。 一種是直接訪問, 譬如訪問陣列的第一個成員: 第一個成員的地址是 &a[0], 對其解引用, 即 *&a[0](在 C 語言中, 取址運算子和解引用運算子是相反的運算子, 類比於數學中的平方和開平方), 與 a[0] 等價。 這種直接訪問方式稱為下標訪問, 改變下標的數字即可直接訪問陣列的其他元素; 另外一種則是間接訪問,  譬如訪問陣列的第一個成員: 如果指標指向陣列的首元素, 這時候可以直接對指標進行解引用, 即 *pa。 假如想要訪問其他元素, 可以在指標原有的地址(基地址)上增加一個偏移量, 譬如 pa + 2, 對其解引用得到 *(pa + 2)。 在習慣上我們喜歡把指標指向陣列的首元素, 實際上可以指向任何位置, 甚至陣列外, 譬如 「pa = a - 1

」。 不過需要注意的是在訪問的時候千萬不要越出陣列邊界, C 語言編譯器是不會幫我們作陣列邊界檢查的。 除了上述的兩種方式, C 語言還提供了一種有趣的訪問陣列的方式:指標 + 下標。 譬如訪問陣列的第三個成員: pa[2]。 這實際上是 C 語言給程式設計師提供的「語法糖」(什麼意思請自行百度), 它在本質上與 *(pa+2) 並無區別, 不過由於與陣列的下標訪問方式相同, 使得程式的可讀性增加了。

關於陣列的概念, 需要明確以下兩個結論:

1. 陣列名的型別就是陣列型別——Type[], 它不是常量, 不是指標, 也不是常量指標。

2. 陣列名在大多數情況下會轉換為一般指標型別, 除了以下三種情況(C99 標準):

  • 使用 sizeof 運算子計算陣列所佔記憶體空間大小的時候 
  • 使用字串字面量初始化陣列的時候(即初始化一個字串陣列)
  • 對陣列名進行取址運算 「&」的時候 ,得到型別是陣列指標型別——Type(*) [], 關於這一點可以看到一個有趣的現象, 我們來看以下這段程式碼。
/* 
    GCC 5.4 環境編譯 
     
*/ 

#include <stdio.h>

int main(void)
{
	int a[10] = {0};
	
	printf("a = %p\n",a);
	printf("&a[0] = %p\n",&a[0]);
	printf("&a = %p\n",&a);	
	
	return 0;	
}

輸出結果如下:

a = 0x7ffce513bb70
&a[0] = 0x7ffce513bb70
&a = 0x7ffce513bb70

可以看到三個地址值完全一樣, 前面我們說過了 a 和 &a[0] 在某些情況下是等效的, 都代表了陣列首元素的首地址, 而 &a  則代表的是整個陣列的首地址。 雖然兩者的值相同, 但含義卻完全不同。 

左值和右值

這裡提及一個 C 初學者不是很熟悉的一個概念:「左值」(lvalue)和「右值」(rvalue)。這個概念來自於賦值語句: 「X = Y;」。 很多人認為「lvalue」的「l」是 left 的意思,「rvalue」的「r」是 right 的意思, 所以賦值運算子「=」左邊的就稱為左值, 右邊的就稱為右值。 但是這其實算是比較古老的一種說法,不過現在也習慣這麼稱呼。 實際上「l」應該解讀為「location」, 表示可定址(即可使用「&」運算子取址); 「r」可以解讀為「read」, 表示可讀。 一般來說, 可以這樣定義左值」(lvalue)和「右值」(rvalue): 可以使用取址運算子「&」獲取地址的是左值, 否則為右值。 大多數的左值都可以放在賦值運算的左側或者右側, 除了使用 const 限定符的變數和陣列以外。 這兩者(比較常見)被稱為不可修改的左值。 這和我們平時所說的「const 修飾的變數無法修改」,「陣列無法整體賦值」相符合。 所以以下的賦值語句都是錯誤的:

const int a = 10;
a  = 12; //錯誤

int b[10] = {0};
int c[10] = {0, 1, 2 ,3 ,4, 5, 6, 7, 8, 9,}
b = c ; //錯誤

關於左值和右值我們可以得出以下幾個結論:
  • 可修改的左值是可設定地址的、可賦值的。
  • 不可修改的左值是可設定地址、不可賦值的。
  • 右值不可設定地址,也不可賦值。
關於陣列和指標的關係大致講到這裡, 其實這篇的目的就是為了讓讀者重新認識一下陣列。 關於陣列和指標的內容在之後還會提到。 下一篇博文我們來聊聊一些複雜的指標, 這些指標在一些公司的筆試題中經常出現。  2017 年 7 月 7 日 Kilento