數組
C語言包含4種基本資料類型:
- 整型
- 浮點型
- 指針
- 聚合類型
- 數組
- 結構
- ……
之前的文章中,我們已經學習了前面三種基本資料類型。
今天我們将學習聚合類型中的數組。
數組分為兩種:
- 一維數組
- 多元數組
但是多元數組其實是一維數組的一種擴充,是以我們僅僅學習一維數組。
一維數組的聲明
為了聲明一個一維數組,我們需要在數組名後面跟一對方括号[],方括号裡面是一個整數,指定數組中的元素的個數。
int values[];
顯而易見,我們聲明了一個整型數組,數組包含20個整型元素。
數組下标總是從0開始,最後一個元素的下标是元素的數目減1。
一維數組初始化
值得我們注意的是,與整型,浮點型,指針類型的變量初始化不同的是,數組的初始化需要一系列的值。
這些用于初始化數組的值位于一對花括号{}中,每個值之間用逗号,分隔。
int vector[] = {,,,,};
初始化清單給出的值逐個指派給數組的各個元素。
是以:
{0}. vector[0] = 10
{0}. vector[1] = 20
{0}. vector[2] = 30
{0}. vector[3] = 40
{0}. vector[4] = 50
靜态初始化和自動初始化
變量的存儲類型決定變量何時建立,何時銷毀以及它的值儲存多久。
同樣,數組變量的聲明位置也決定着數組變量的預設存儲類型。
- 對于靜态變量而言
- 存儲于靜态記憶體中的數組隻初始化一次,也就是在程式開始執行之前。
- 連結器用包含可執行程式的檔案中合适的值對數組元素進行初始化。
- 如果數組未被初始化,數組元素的初始值将會自動設定為0。
- 對于自動變量而言
- 由于自動變量位于運作時堆棧中,執行流每次進入它們所在的代碼塊時,這類變量每次所處的記憶體位置可能并不相同,是以在程式開始執行前,編譯器沒有辦法對這些位置進行初始化。是以,自動變量在預設情況下是未初始化的。
- 如果自動變量的聲明中給出了初始值,每當執行流進入自動變量聲明所在的作用域時,變量就被一條隐式的指派語句初始化。這條指派語句需要時間和空間執行。
- 數組的問題在于初始化清單中可能有很多值,這就可能産生很多條指派語句。對于那些非常龐大的數組,它的初始化時間可能非常可觀。
是以,當數組的初始化局部于一個函數(或代碼塊)時,我們應該考慮——在程式的執行流每次進入該函數(或代碼塊)時,每次都對數組進行重新初始化是不是值得?
如果不值得,那麼我們應該把數組聲明為static,這樣數組的初始化隻需在程式開始前執行一次。
不完整的數組初始化
按理來說,數組的初始化應該如上面所示。
但是,在編譯器中,存在幾種不完整的數組初始化,且運作無誤。
int list[] = {,,,};
int deque[] = {,,,,};
對于第一條初始化語句,它将前4個初始值分别賦予list[0],list[1],list[2],list[3],而對于list[4]則預設初始化為0。
對于第二條初始化語句,我們在聲明中并沒有給出數組的長度,這時編譯器自動把數組的長度設定為剛好能夠容納所有初始值的長度。
但是,我們也不能盲目的使用不完整的初始化。
下面這兩種情況,編譯器在運作時則會報錯或出現與我們希望情況不符合的現象:
int list1[] = {,,,,,};
int list2[] = {,};
對于第一條聲明語句,編譯器直接報錯。這是因為初始化值的數目和數組元素的數目并不比對,沒辦法把6個資料放到5個變量中。
對于第二條聲明語句,我們本意是希望list[0] = 1,list[4] = 5。但是結果是list[0]=1,list[1] = 5,後面三個元素都為0。
即,我們在使用不完整初始化數組時,僅僅可以省略最後幾個初始值。
字元數組初始化
對于數組中的每個元素,除了可以存儲int或float型的數值之外,我們還可以存儲char型的字元。
根據之前的知識,我們會對字元數組的初始化采用如下的方式:
int message[] = {'h','e','l','l','o',};
顯而易見,這種數組初始化方式比較麻瓜。
是以,C語言标準提供了一種快速初始化字元數組的方式:
int message[] = "hello";
盡管初始值看上去像是一個字元串常量,但實際上并不是。
數組與指針
接下來,就是我們的核心——數組與指針的關系。
我們再次研究下述聲明:
int a;
char c;
double d;
int *p;
int b[];
我們把前四個變量a、c、d、p稱為标量,因為它們的類型單一、值單一。
變量名 | 值 | 類型 |
---|---|---|
a | 10 | int |
c | ‘H’ | char |
d | 3.14 | double |
p | &a | int* |
我們把最後一個變量b稱為數組,因為它是一些值的集合。
如下圖所示:
我們将數組名和下标一起使用,用于辨別該集合中的某個特定的值。
注意,每個特定的值都是一個标量,可以用于任何可以使用标量的上下文中環境中。
變量名 | 值 | 類型 |
---|---|---|
b[0] | 1 | int |
b[1] | 1 | int |
b[2] | 1 | int |
b[3] | 1 | int |
b[4] | 1 | int |
b[5] | 1 | int |
b[6] | 1 | int |
b[7] | 1 | int |
b[8] | 1 | int |
b[9] | 1 | int |
但是,數組名b是什麼類型呢?
在C中,幾乎所有使用數組名的表達式中,數組名的值是一個指針常量,也就是數組第一個元素的位址!
數組名的值是一個指針,也就是數組第一個元素的位址!?
大多數人看到這句話,第一反應會産生如下圖所示的記憶體結構:
如圖所示,數組名b是一個指針,值是數組第一個元素的位址,是以指針b應該指向數組的第一個元素。
乍一看,這種分析非常正确。
但是,如果數組是這種記憶體結構的華,那麼其所占據的總記憶體空間是不是應該加上數組名所占據的這一塊呢?
是以,這種記憶體結構的示意圖是錯誤的!
究其原因,是由于我們忽略了句中的“的值”二字。C語言隻是說數組名的值是一個指針,而沒有說數組名就是一個指針。
是以,正确的記憶體結構示意圖應該如下圖所示:
數組名b占據的記憶體空間就是數組中各個元素所占空間連接配接而成的總的記憶體空間。
數組名的類型取決于數組元素的類型。
- 如果數組元素的類型是int型,那麼數組名就是指向int的常量指針。
- 如果數組元素的類型是其他類型,那麼數組名就是指向其他類型的常量指針。
數組名的值是一個指針常量!?
注意這個是指針常量,而不是指針變量。
指針常量所指向的是記憶體中數組的起始位置,如果修改這個指針常量,唯一可行的操作就是把整個數組移動到記憶體的其他位置。但是,在程式完成連結之後,記憶體中數組的位置是固定的。是以當程式運作之後,再想移動數組就為時已晚了。
是以,數組名的值是一個指針常量。
是以,如下的操作是非法的:
int a[];
int *c;
a = c;
數組名的值是數組第一個元素的位址!?
如圖所示,我們可以将數組名完全看做數組第一個元素的位址。
考慮下列代碼:
int b[];
int *c;
c = &b[];
假設b[0]的位址為100。
在執行了指派語句之後,指針變量c的值變成100,指針c指向數組b的第一個元素。
如下圖所示:
由于我們可以将數組名完全看做數組第一個元素的位址,上面的指派語句也可以改寫為如下代碼:
int b[];
int *c;
c = b;
相當于把“類指針變量”b拷貝給了指針變量c。
數組指派操作
對于一般的标量,我們可以使用指派語句對其指派:
int x = y;
很多時候,我們也希望将一個數組的所有元素指派給另外一個數組,這時候應該怎麼辦呢?
如果我們采取了和普通标量一樣的方式,采用指派操作符=,那麼編譯器将會報錯。
這也是很多C初學者容易犯的錯誤。
int m[];
int n[];
m = n;
/*錯誤*/
這是因為數組名不能代表整個數組元素,數組名類似于指針,數組名的值的其第一個元素的位址。
對于數組而言,如果我們希望将一個數組的所有元素指派給另外一個數組,我們必須使用一個循環,一次複制一個元素。
下标引用操作符[]和解引用操作符*
首先需要聲明的是,除了優先級之外,下标引用操作符[]和間接通路操作符*完全相同。
考慮下列代碼:
int b[];
*(b + )是什麼???
不急,我們一步步分析:
- 數組名b的類型是一個指向整型的指針,是以3這個值需要根據整型值的長度進行調整。
- 指針加法運算的結果是另一個指向整型的指針,它指向的是數組第一個元素向後移3個整型長度的位置。
- 解引用操作符*通路這個位置,取得其值。
再考慮下列代碼:
int b[];
b[]是什麼???
對于b[3],我們之前分析過,這是一個标量,表示數組b中的第4個元素。
是以,我們能夠得到如下的等式:
array[subscript]
等價于
*(array + subscript)
那麼,
- 在使用下标引用的地方,我們完全可以使用對等的指針表達式來代替。
- 在使用上面這種形式的指針表達式的地方,我們也可以使用下标表達式來代替。
最後,我們通過一個案例,深度了解上面的内容。
代碼如下:
int b[] = {,,,,,,,,,};
int *ap = b + ;
表達式 | 值 |
---|---|
ap | b +2 或 &b[2] |
*ap | *(b + 2) 或 b[2] |
ap + 6 | b + 8 或 &b[8] |
*ap + 6 | b[2] + 6 |
*(ap + 6) | *(b + 8)或 b[8] |
ap[0] | 等價于*(ap+0),即b[2] |
ap[-1] | 等價于*(ap-1),即b[1] |
ap[9] | 等價于*(ap+9),但是下标越界,非法!!! |
數組下标越界檢查
最早的C編譯器不檢查下标,同時最新的編譯器依然不檢查下标。
這是由于數組和指針的密不可分的關系,導緻編譯器檢查下标将是一項非常龐大的任務。
是以,我們的經驗是:
- 如果下标值是從那些已知是正确的值計算得來,那麼就無需檢查它的值。
- 如果一個用作下标的值是根據某種方法從使用者輸入的資料産生而來的,那麼在使用它之前必須進行檢查,確定它們位于有效的範圍之内!