C數組篇(一維數組-上)

數組,相信大家都使用過。本文將由淺入深的討論數組,探索一些更高級的數組話題,如多維數組、數組與指針及數組的初始化等。

一、一維數組

在討論多維數組之前,先來學習下一維數組的知識。首先我們學習一個概念,它被許多人認爲是C語言設計的一個缺陷。但實際上,這個概念是以一種相當優雅的方式把一些完全的不同的概念聯繫在了一起。

1.數組名

考慮下面聲明:

int a;

int b[10];

我們把變量a稱爲標量,因爲它是一個單一的值,這個變量的類型是一個整數。我們把變量b稱爲數組,因爲它是一些值的集合。下標和數組名一起使用,用於標識該集合中某個特定的值。例如,b[0]標識數組b的第一個值。每個特定值都是一個標量,可以用於任何可以使用標量數據的上下文環境中。

b[4]的類型是整型,但b的類型又是什麼?它所表示的又是什麼?一個合乎邏輯的答案是它標識整個數組,但事實並非如此。在C中,在幾乎所有使用數組名的表達式中,數組名的值是一個指針常量,也就是數組第一個元素的地址。它的類型取決於數組元素的類型;如果數組元素是int,那麼數組名的類型就是“指向int的常量指針”;如果是其他類型,那麼數組名的類型就是“指向其他類型的常量指針”。

請不要根據這個事實得出數組和指針是相同的結論。數組具有一些和指針完全不同的特徵。例如,數組具有確定數量的元素,而指針只是一個標量值。編譯器用數組名來記住這些屬性。只有當數組名在表達式中使用時,編譯器纔會爲它產生一個指針常量。

注意這個值是指針常量,而不是指針變量。你不能修改常量的值。你只要稍微回想一下,就會認爲這個限制是合理的;指針常量所指向的是內存中數組的起始位置,如果修改這個指針常量,唯一可行的操作就是把整個數組移動到內存的其他位置。但是,在程序完成鏈接之後,內存中數組的位置是固定的,所以當程序運行時,再想移動數組就爲時已晚了。因此數組名的值是一個指針常量。

只有在兩種場合下,數組名不用指針常量來表示---就是當數組名作爲sizeof操作符或單目操作符&的操作數時。sizeof返回整個數組的長度,而不是指向數組的指針的長度。取一個數組名的地址產生的是一個指向數組的指針,而不是指向某個指針常量的指針。

舉個例子:

int a[10];

int b[10];

int *c;

...

c = &a[0];

表達式&a[0]是一個指向數組第一個元素的指針。但那正是數組名本身的值,所以下面這條賦值語句和上面那條賦值語句所執行的任務是完全一樣的。

c = a;

這條賦值語句說明了爲什麼理解表達式中的數組名的真正含義是非常重要的。如果數組名錶示整個數組,這條語句就表示整個數組被複制到一個新的數組。但事實上完全不是這樣的,實際被賦值的是一個指針的拷貝,c所指向的是數組的第一個元素。因此像下面這樣的表達式:

b = a;

是非法的,你不能使用賦值符把一個數組的所有元素複製到另一個數組。你必須使用一個循環,每次複製一個元素。

考慮下面這條語句:

a = c;

c被聲明爲了一個指針變量,這條語句看上去像是執行某種形式的指針賦值,把c的值複製到a,但這個賦值是非法的,和上面的例子一樣:記住!在這個表達式中,a的值是個常量,不能被修改。

2.下標引用

在前面聲明的上下文環境中,下面的表達式是什麼意思?

*(b + 3)

首先,b的值是一個指向整型的指針,所以3這個值根據整型值的長度進行調整。b+3的結果是另一個指向整型的指針,它所指向的是數組第一個元素向後移3個整型長度的位置。然後,*間接訪問操作符訪問這個新的位置,當整個表達式是右值的情況下,它會取得那裏的值,當整個表達式是左值的情況下,它會將某個新值存儲於該處。

這個過程聽上去是不是很熟悉?這是因爲它和下標引用的執行過程完全相同。事實上,除了優先級外,下標引用和間接訪問完全相同。例如,下面兩個表達式是完全等同的:

array[subscript]

*(array + (subscript))

3.指針與下標

如果你可以互換指針表達式和下標表達式,那麼你應該使用哪一個呢?這裏並沒有一個明確的答案,對於大多數人而言,下標更容易理解,尤其是在多維數組中。所以在可讀性方面,下標有一定的優勢。但在另一方面,這個選擇可能會影響運行時效率。

假定這兩種方法都正確,下標絕不會比指針更有效率,但指針有時會比下標更有效率。

爲了理解這個效率問題,讓我們來研究兩個循環。它們執行相同的任務。首先,我們使用下標方案將數組中的所有元素都設置爲0。

int array[10], i;

for (i = 0; i < 10; i++)

    array[i] = 0;

爲了對下標表達式求值,編譯器在程序中插入指令,取得i的值,並把它與整型的長度(也就是4)相乘。整個乘法需要花費一定的時間和空間。

現在我們來看看下面這個循環,它所執行的任務和前面的循環完全一樣。

int array[10], *p;

for (p = array; p < array + 10; p++)

    *p = 0;

儘管這裏並不存在下標,但還是存在乘法運算。

現在這個乘法運算出現在for語句的調整部分。++的1必須與整型的長度相乘,然後再與指針相加。但這裏存在一個重大區別:循環每次執行時,執行乘法運算的都是兩個相同的數(自加運算的1和整型長度的4)。結果,這個乘法只在編譯時執行一次----程序現在包含了一條指令,把4與指針相加。程序在運行時並不執行乘法運算。

這個例子說明了指針比下標更有效率的場合----當你在數組中1次1步(或某個固定的數字)地移動時,與固定數字相乘的運算在編譯時完成,所以在運行時所需的指令就少一些。在絕大多數機器上,程序會更小一些、更快一些。

現在考慮下面的代碼段:

a = get_value();                                  a = get_value();

array[a] = 0;                                       *(array + a)  = 0;

兩邊的語句所產生的代碼並無區別。a可能是任何值,在運行時方知。所以兩種方案都需要乘法指令,用於對a的值進行調整。這個例子說明了指針和下標的效率完全相同的場合。

4.指針的效率

前面說過,指針有時比下標更有效率,前提是它們被正確的使用,它的結果可能不同,這取決於編譯器和機器。然而,程序的效率主要取決於你所編寫的代碼,和使用下標一樣,使用指針也很容易寫出質量低劣的代碼,而且通常這個可能性更大。

由於涉及到了彙編、篇幅、實用性等原因。這邊不具體展開講指針優化的寫法,只對其作評價。

通常,綜合評價來講,通過指針優化的寫法對效率的提升極爲有限,而且會使得非常容易理解的代碼變成“莫名其妙”的代碼。對於極少數情況,這種寫法值得使用,但在絕大多數情況下,爲了一點點運行效率,使得程序難以維護,這是非常不值得的。

你很容易爭辯說,經驗豐富的C程序員在使用指針時不會遇到太大麻煩。但這個論斷存在兩個荒謬之處,首先“不會遇到太大麻煩”意味着“還是會遇到麻煩”。從本質上說,複雜的用法比簡單的用法所涉及的風險大太多。其次維護代碼的程序員可能不如閣下經驗豐富,程序維護是軟件產品的主要成本所在。所以那些使得程序維護工作更爲困難的編程技巧應慎重使用。

同時,有些機器在設計時用了特殊的指令,用於執行數組下標操作,目的是爲了使這種極常用的操作更加快速,在這種機器上的編譯器將使用一些特殊指令來實現下標表達式,但編譯器並不一定會用這些指令實現指針表達式。這樣,在這種機器上,下標可能比指針效率高。

5.數組和指針

指針和數組並不是相等的。爲了說明這個概念,請考慮下面這兩個聲明:

int a[5];

int *b;

a和b都具有指針值,都可以進行間接訪問和下標引用操作。但是,它們還是存在相當大的區別。

聲明一個數組時,編譯器將根據所指定的元素數量爲數組保留內存空間,然後再創建數組名,它的值是一個常量,指向這段空間的起始位置。聲明一個指針變量時,編譯器只爲指針本身保留內存空間,它並不爲任何整型值分配內存空間。而且指針變量並未被初始化爲指向任何現有內存空間,如果它是一個自動變量,它甚至根本不會被初始化。把這兩個聲明用途的方法來表示,你可以發現他們之間存在顯著的不同。

因此,上述聲明之後,表達式*a是完全合法的,但表達式*b卻是非法的。*b將訪問內存中某個不確定的位置,或者導致程序終止。另一方面,表達式b++可以通過編譯,但a++卻不行,因爲a的值是個常量。

你必須清楚地理解它們之間的區別,這是非常重要的,因爲我們所討論的下一個話題可能把水攪渾。​

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章