當前位置:才華齋>計算機>C語言>

C語言陣列與指標詳解

C語言 閱讀(9.44K)

由於資料的表現形式多種多樣,還有字元型和其它的數值型別,因此僅有基本資料型別是不夠的。是否可以通過基本資料型別的組合抽象構造其它的資料型別呢?下面是小編為大家帶來的C語言陣列與指標詳解的知識,歡迎閱讀。

C語言陣列與指標詳解

  1.陣列

  (1)陣列的宣告

我們知道,一個基本資料型別的變數只能儲存一個數據,比如:

int data = 0x64;

如果需要儲存一組int型資料呢?比如,1、2、3,則至少需要3個變數data0、data1、data2。比如:

int data0 = 1, data1 = 2, data2 =3;

由於資料的表現形式多種多樣,還有字元型和其它的數值型別,因此僅有基本資料型別是不夠的。是否可以通過基本資料型別的組合抽象構造其它的資料型別呢?答案是可以的,構造資料型別陣列就是這樣產生的。

從概念的視角來看,int型整數1、2和3都是相同的資料型別,data0、data1和data2三個變數的共性是data,其差異性是下標不一樣。因此可以將data0、data1和data2抽象為一個名字,然後用下標區分這些變數的集合——data[0]、data[1]和data[2]。如果有以下宣告:

intdata[3]; //解讀為data是int陣列(元素個數3)

那麼data[3]就成了存放3個int型資料1、2、3的data[0]、data[1]和data[2]所組成的陣列,即可分別對data[0]、data[1]和data[2]賦值:

data[0] = 1, data[1] =2, data[2] = 3;

當然,也可以按照以下方式宣告一個數組並進行初始化:

intdata[3] = {1, 2, 3};

通常將data稱為陣列(變數)名,data[0]、data[1]和data[2]被稱為變數。因而可以說,陣列是將相同型別資料的若干變數按有序的形式組織起來,用一個名字命名,然後用下標區分這些變數的集合。

由於陣列是建立在其它型別的基礎上,因此C將陣列看作構造型別,在宣告陣列時必須說明其元素的型別。比如,int型別的陣列、float型別的陣列或其它型別的陣列。而其它型別也可以是陣列型別,在這種情況下,建立的是陣列型別的陣列,簡稱陣列的陣列。

  (2)下標與變數的值

在這裡,定義了一個名為data的陣列型別變數,它是由存放3個int型資料1、2、3的變數data[0]、data[1]和data[2]組成的。通常又將陣列的各個變數稱為陣列的元素,而陣列的元素是按照順序編號的,這些元素的編號又稱為陣列元素的下標。

由於有了下標,因此陣列元素在記憶體中的位置就被唯一確定下來了。下標總是從0開始的,最後一個元素的下標為元素的個數減1(即2),data[0]叫第1個元素,data[1]叫第2個元素,data[2]叫第3個元素,也就意味著所有的元素在記憶體中都是連續儲存的。

直觀上,陣列是由下標(或稱為索引)和值所組成的序對集合,對於每個有定義的下標都存在一個與其關聯的值,在數學上稱為對映。除了建立新陣列外,大多數語言對陣列只提供兩種標準操作:一個操作是檢索一個值,另一個操作是儲存一個值。

函式Create(data, size)建立一個新的具有適當大小的空陣列,初始時陣列的每一項都沒有定義。Retrieve操作接受一個數組data和一個下標index,如果下標合法,則該操作返回與下標關聯的值,否則產生一個錯誤。Store操作接受一個數組data、一個下標index和一個項item的集合,即項是value值的集合,有時也將值(value)稱為項(item),返回在原來陣列中增加新的序對後的陣列。

顯然,int系的任何常量表達式都可以作為陣列元素的下標。比如:

int array[3+5]; // 合法

int array['a']; //表示int array[97];

上述定義之所以合法,因為表示元素個數的常量表達式在編譯時就具有確定的意義,與變數的定義一樣明確地分配了固定大小的空間。

雖然使用符號常量增強了陣列的靈活性,但如果定義採用了以下的形式:

int n = 5;

int array[n]; //非法

因為標準C認為陣列元素的個數n不是常量,雖然編譯器似乎已經“看到”了n的值,但intarray[n]要在執行時才能讀取變數n的值,所以在編譯期無法確定其空間大小。使用符號常量定義陣列長度的正確形式如下:

#define N 10

int array[N];

即可根據實際的需要修改常量N的值。

由於陣列元素下標的有效範圍為0~N-1,因此data[N]是不存在的,但C語言並不檢查下標是否越界。如果訪問了陣列末端之後的元素,訪問的就是與陣列不相關的記憶體。它不是陣列的一部分,使用它肯定會出問題。C為何允許這種情況發生呢?這要歸功於C信任程式設計師,因為不檢查越界可以使執行速度更快,所以編譯器沒有必要檢查所有的下標錯誤。因為在程式執行之前,陣列的下標可能尚未確定,所以為了安全起見,編譯器必須在執行時新增額外程式碼檢查陣列的每個下標值,但這樣會降低程式的執行速度。C相信程式設計師能編寫正確的程式碼,這樣的程式執行速度更快。但並不是所有的程式設計師都能做到這一點,越界恰恰是初學者最容易犯的錯誤,因此要特別注意下標的範圍不能超出合理的界限。

  (3)變數的地址與型別

當將變數data[0]、data[1]和data[2]作為&的運算元時,&data[0]是指向變數data[0]的指標,&data[1]是指向變數data[1]的指標,&data[2]是指向變數data[2]的指標。data[0]、data[1]和data[2]變數的型別為int,&data[0]、&data[1]和&data[2]指標的型別為int *const,即指向常量的指標,簡稱常量指標,其指向的值不可修改。比如:

int a;

int * const ptr = &a;

ptr = NULL; //試圖修改,則編譯報警

&a = NULL; //試圖修改,則編譯報警

同理,&data是指向變數data的指標,那麼data是什麼型別?

按照宣告變數的規約,將識別符號data取出後,剩下的“int [3]”就是data的型別,通常將其解釋為由3個int組成的陣列型別,簡稱陣列型別。其目的是告訴編譯器需要分配多少記憶體?3個元素的整數陣列,data型別測試程式詳見程式清單 1.20。

程式清單 1.20 data型別測試程式

1 #include

2

3 void f(int x);

4 int main(int argc, char *argv[])

5 {

6 int data[3];

7 f(data);

8 return 0;

10 }

通過編譯器提示的警告,“funtion: 'int' differ in levels ofindirection from 'int [3]'”,說明陣列變數data的型別為不是int而是int [3]陣列型別。由於在設計C語言時,過多地考慮了開發編譯器的便利。雖然設計編譯器更方便了,卻因為概念的模糊給初學者造成了理解上的困難。實際上陣列應該這樣定義:

int[3] data;

即int是與[3]結合的。&data到底是什麼型別?

當data作為&的運算元時,則&data是指向data的指標。由於data的型別為int [3],因此&data是指向“int [3]陣列型別”變數data的指標,簡稱陣列指標。其型別為int (*)[3],即指向int [2]的指標型別。為何要用“()”將“*”括起來?

如果不用括號將星號括起來,那麼“int (*)[3]”就變成了“int *[3]”,而int*[3]型別名為指向int的指標的陣列(元素個數3)型別,這是設計編譯器時約定的語法規則。

&data的型別到底是不是“int (*)[3]”?其驗證程式範例詳見程式清單 1.21。

程式清單 1.21 &data型別測試程式

1 #include

2 int main(int argc, char *argv[])

3 {

4 int data[3];

5 int b = &data;

6 return 0;

7 }

通過編譯器提示的警告,“'int' differ in levels ofindirection from 'int (*)[3]'”,說明&data的型別為int (*)[3]。

(4)sizeof(data)

將如何尋找相應的陣列元素呢?常用的方法是通過“陣列的基地址+偏移量”算出陣列元素的地址。在這裡,第一個元素&data[0]的地址稱為基地址,其偏移量就是下標值和每個元素的大小sizeof(int)相乘。假設陣列元素&data[0] 的地址為A,且在記憶體中的'實際地址為0x22FF74,那麼&data[1]的值為:當data作為sizeof的運算元時,其返回的是整個陣列的長度。在這裡,sizeof(data)的大小為12,即3個元素佔用的位元組數為4×3=12,系統會認為&data+1中的“1”,偏移了一個數組的大小,因此&data +1是下一個未知的儲存空間的地址(即越界)。在小端模式下,陣列在記憶體中的儲存方式詳見圖 1.10。

A + 1×sizeof(int) = (unsignedint)data + 4 = 0x22FF74 + 4 = 0x22FF78

&data[2]的值為:

A + 2×sizeof(int) = (unsigned int)data+ 8 = 0x22FF74 + 8 = 0x22FF7C

實際上,當在C語言中書寫data[i]時,C將它翻譯為一個指向int的指標。Data是指向data[0]的指標,data+i是指向data[i]的,因此不管data陣列是什麼型別,總有data+i等於data[i],於是*(data+i)等於data[i],其相應的測試範例程式詳見程式清單 1.22。

程式清單 1.22變數的地址測試程式

1 #include

2 int main(int argc, char *argv[])

3 {

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

5 printf("%x, %x, %x, %x, %x",&data[0], &data[1], &data[2], &data, &data+1);

6 return0;

7 }

實踐證明,雖然&data[0]與&data的型別不一樣,但它們的值相等。同時也可以看出,陣列的元素是連續儲存的。如果將陣列變數佔用記憶體的大小除以陣列變數中一個元素所佔用空間的大小,便可得到陣列元素的個數。即:

int numData = sizeof(data) / sizeof(data[0]);

當然,也可以使用巨集定義計算陣列元素的個數:

#define NELEMS(data)(sizeof(data) / sizeof(data[0]))

當陣列作為函式的引數時,C語言函式的所有引數必須在函式內部宣告。但是,由於在函式內部並沒有給陣列分配新的儲存空間,因此一維陣列的容量只在主程式中定義。顯然,如果函式需要得到一維陣列的大小,則必須將它以函式引數的形式傳入函式中,或將它作為全域性變數訪問。

  2. 規約

標準C規定:除了“在宣告中”或“當一個數組名是sizeof或&的運算元”之外,只要陣列名出現在表示式中,這編譯器總是將陣列名解釋為指向該陣列的第一個元素的指標。

雖然data在表示式中解讀為指向該陣列首元素data[0]的指標,但實際上data被解讀為&data[0]或等價於“*data==data[0]”,因此data與&data[0]的值相等,且它們的型別都是int *const,即一個數組名是一個不可修改的常量指標(左值)。

根據指標運算規則,當將一個整型變數i和一個數組名相加時,其結果是指向陣列第i個元素的指標,即data+index==&data[index],*(data+index)==data[index],因此習慣性地將:

int * ptr = &data[0];

寫成下面這樣的形式:

int *ptr = data;

由於data的型別是不可修改的常量指標int*const,因此任何試圖使陣列名指向其它地方的行為都是錯誤的。比如:

data ++; //錯誤

雖然data有地址,但其型別為int *const,因此不能對data++賦值,同樣也不能反過來給data賦值。比如:

data = ptr;

型別地,象下面這樣的表示式也是非法的:

intdata[3];

data= {1, 2, 3};

但可以將data複製給指標變數ptr,通過改變指標變數達到目的。比如:

#define N 10

int data[N];

int *ptr;

for(ptr = data; ptr < data + N; ptr ++)

sum+= *ptr;

for語句中的條件p

int sum = 0;

while(ptr < data +N)

sum += *ptr++;

其中,*ptr++等價於*(ptr++),它的含義為自增前表示式的值是*ptr,即ptr當前指向的物件,以後再自增ptr。