由於數據的表現形式多種多樣,還有字符型和其它的數值類型,因此僅有基本數據類型是不夠的。是否可以通過基本數據類型的組合抽象構造其它的數據類型呢?下面是小編爲大家帶來的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。