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

緊接着上篇,繼續來講講C中的數組。

 

6.作爲函數參數的數組名

當一個數組名作爲函數參數傳遞給一個函數時會發生什麼情況呢?

數組名的值就是一個指向數組第一個元素的指針,所以很容易明白此時傳遞給函數的是一份該指針的拷貝。函數如果執行了下標引用,實際上是對這個指針執行間接訪問操作,並且通過這種間接訪問,函數可以訪問和修改調用程序的數組元素。

現在來解釋下C關於參數傳遞的表面上的矛盾之處。之前說過所有傳遞給函數的參數都是通過傳值方式進行的,但數組名參數的行爲彷彿它是通過傳址調用傳遞的。傳址調用是通過傳遞一個指向所需元素的指針,然後在函數中對該指針執行間接訪問操作實現對數據的訪問。作爲參數的數組名是個指針,下標引用實際執行的就是間接訪問。

那麼數組的傳值調用行爲又是表現在什麼地方呢?傳遞給函數的是參數的一份拷貝(指向數組起始位置的指針的拷貝),所以函數可以自由地操作它的指針形參,而不必擔心會修改對應的作爲實參的指針。

所以,此處不存在矛盾。所有的參數都是通過傳值方式傳遞的。當然,如果你傳遞了一個指向變量的指針,而函數對該指針執行了間接訪問操作,那麼函數就可以修改那個變量。儘管看上去並不明顯,但數組名作爲參數時發生的正式這種情況。這個參數(指針)實際上是通過傳值方式傳遞的,函數得到的是該指針的一份拷貝,它可以被修改,但調用程序所傳遞的實參並不受影響。

下面是一個簡單的函數,用於說明這些觀點。

void strcpy(char *buffer, char const *string){  /* 複製字符,直到遇到NULL字節 */  while((*buffer++ = *string++) != '\0');}

它把第2個參數中的字符串複製到第1個參數所指向的緩衝區。調用程序的緩衝區將被修改,因爲函數對參數進行了間接訪問操作。但是,無論函數對參數(指針)如何進行修改,都不會修改調用程序的指針實參本身(但可能修改它所指向的內容)。

注意while語句中的*string++表達式。它取得string所指向的那個字符,併產生一個副作用,就是修改string,使它指向了下一個字符。用這種方式修改形參並不會影響調用程序的實參,因爲只有傳遞給函數的那份拷貝進行了修改。

7.聲明數組參數

這裏有一個有趣的問題。如果你想把一個數組名參數傳遞給函數,正確的函數形參應該是怎樣的?它應該被聲明爲一個指針還是一個數組?

如你所見,調用函數時實際傳遞的是一個指針,所以函數的形參實際上是個指針,但爲了新手更容易上手一些,編譯器也接受數組形式的函數形參。因此,下面兩個函數類型是相等的:

int strlen(char *string);

int strlen(char string[]);

這個相等性暗示指針和數組名實際上是相等的,但千萬不要被它糊弄了!這兩個聲明確實相等,但只是在當前這個上下文環境中。如果出現在別處,可能完全不同,就像前面討論的那樣。但對於數組形參,你可以使用任何一種形式的聲明。

你可以使用任何一種形式的聲明,但哪個“更加準確”呢?答案是指針。因爲實參實際上是個指針,而不是數組。同樣,sizeof(string)的值是指向字符的指針的長度,而不是數組的長度。

現在你應該清楚爲什麼函數原型中的一位數組形參無需寫明它的元素數目,因爲函數並不爲數組參數分配內存空間。形參只是一個指針,它指向的是已經在其他地方分配好內存的空間。這個事實解釋了爲什麼數組形參可以與任何長度的數組匹配----它實際傳遞的只是指向數組第一個元素的指針。另一方面,這種實現方法使函數無法知道數組的長度。如果需要知道數組的長度,它必須作爲一個顯式的參數傳遞給函數。

8.初始化

就像標量變量可以在它們的聲明中進行初始化一樣,數組也可以這樣做。唯一的區別是數組的初始化需要一系列的值。這些值位於一對花括號中,每個值之間用逗號分隔。如下面的例子所示:

int vector[5] = {10, 20, 30, 40, 50};

初始化列表給出的值逐個賦值給數組的各個元素,所以vector[0]獲得的值是10,vector[1]獲得的值是20,以此類推。

靜態和自動初始化

數組初始化的方式類似於標量變量的初始化方式----也就是取決於它們的存儲類型。存儲於靜態內存的數組只初始化以此,也就是在程序開始執行之前。程序並不需要執行指令把這些值放到合適的位置,它們一開始就在那裏了。這個過程是由鏈接器完成的,它用包含可執行程序的文件中合適的值對數組元素進行初始化。如果數組未被初始化,數組元素的初始值將會自動設置爲0。當這個文件載入到內存中執行時,初始化後的數組值和程序指令一樣也被載入到內存中。因此,當程序執行時,靜態數組已經初始化完畢。

但是,對於自動變量而言,初始化過程就沒有那麼簡單了,因爲自動變量位於運行時堆棧中,執行流每次進入它們的代碼塊時,這類變量每次所處的內存位置可能並不相同。在程序開始之前,編譯器沒有辦法對這些位置進行初始化。所以自動變量在缺省情況下是未初始化的。如果自動變量的聲明中給出了初始值,每次都執行流進入自動變量聲明所在的作用域時,變量就會被一條隱式的賦值語句初始化。這條隱式的賦值語句和普通的賦值語句一樣需要時間和空間來執行。數組的問題在於初始化列表可能有很多值,這就可能產生許多賦值語句。對於那些非常龐大的數組,它的初始化時間可能非常可觀。

因此,這裏就需要權衡利弊。當數組的初始化局部於一個函數(代碼塊)時,你應該仔細考慮一下,在程序的執行流每次進入該函數(或代碼塊)時,每次都對數組進行重新初始化是不是值得。如果答案是否定的,你就把數組聲明爲static,這樣數組的初始化只需在程序開始前執行一次。

9.不完整的初始化

在下面兩個聲明中會發生聲明情況呢?

int vector[5] = {1, 2, 3, 4, 5, 6};

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

在這兩種情況下,初始化值的數目和數組元素的數目並不匹配。第1個聲明是錯誤的,我們沒有辦法把6個整型值裝到5個整型變量中。但是,第2個聲明卻是合法的,它爲數組的前4個元素提供了初始值,最後一個元素則初始化爲0。

那麼,我們可不可以省略列表中間的那些值呢?

int vector[5] = {1, 5};

編譯器只知道初始值不夠,但它無法知道缺少的是那些值。所以只允許省略最後幾個初始值。

10.自動計算數組長度

這裏是另一個有用技巧的例子

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

如果聲明中並未給出數組的長度,編譯器就把數組的長度設置爲剛好能夠容納所有的初始值的長度。如果初始值列表經常修改,這個技巧尤其有用。

11.字符數組的初始化

根據目前所學到的知識,你可能認爲字符數組將以下面這種形式進行初始化:

char message[] = {'H', 'e', 'l', 'l', 'o', 0};

這個方法當然可行,但除了非常短的字符串,這種方法確實很笨拙。因此,語言標準提供了一種快速方法用於初始化字符數組:

char message[] = "Hello";

儘管它看上去像是一個字符串常量,實際上並不是。它只是前例的初始化列表的另一種寫法。

如果它們看上去完全相同,你如何分辨字符串常量和這種初始化列表快速記法呢?它們是根據它們所處的上下文環境進行區分的。當用於初始化一個字符數組時,它就是一個初始化列表。在其他任何地方,它都表示一個字符串常量。

這裏有一個例子:

char message1[] = "Hello";

char *message2 = "Hello";

這兩個初始化看上去很像,但它們具有不同的含義。前者初始化一個字符數組的元素,而後者則是一個真正的字符串常量。這個指針變量被初始化爲指向這個字符串常量的存儲位置,如下圖所示:


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