C 語言複習與提高— IV. 數組與指標

來源:互聯網
上載者:User
  IV. 數組與指標

C 語言提供訪問數組的兩種方法:指標算術和數組下標。

指標算術的速度可以高於數組下標。考慮到速度因素,程式員一般都使用指標來訪問數組元素。

一、數組(Array):具有相同類型的資料的有序集合,並用唯一的名字來標識。

1、數組必須直接聲明,編譯器在編譯階段為其分配記憶體空間。

2、在 C89 中,數組必須是定長的,數組的大小在編譯時間是固定的;C99 允許使用變長數組(VLA),數組的大小在運行時確定。 但是只有本地數組才可以申請為變長的。增加變長數組是為了支援數值處理。

[例]void f(int longeur, int wide) { int matrix[longeur][wide]; /* 定義一個矩陣 */ /* 數組的長度由兩個參數決定 */ }

3、數組的所有元素佔據連續的記憶體空間,在記憶體中是線性存放的,儲存數組所需的記憶體空間直接與基底類型和數組長度有關。 數組佔用記憶體空間 = sizeof(基底類型) * 數組長度。

4、C 不檢查數組是否越界,程式可以在兩邊越界。程式員應自己加入越界檢查。數組可以越界使用,但是初始化時不允許!

5、向函數傳遞數組: 定義數組形參的方法有三種:指標,定長數組,無尺寸數組。 void func1(int *a) { }

void func2(int a[10]) { }

void func3(int a[]) { }

在函數的形參的聲明中,數組的尺寸無所謂,因為C語言沒有邊界檢查。 實際上,第二種方法在編譯後,編譯器產生的代碼就是讓函數接受指標,並不產生 10 個元素的數組。

(1、)形參中的數組不能再理解為數組,而必須理解為指標:不能用 sizeof() 求大小;但可以再賦值,這與數組名的指標常量性質不一樣。傳值時有內容的複製,但數組內的元素可能很多,為避免內容的大量複製而佔用太多的記憶體,C 規定數組傳參就是傳指標。

(2、)int a[][] 不能做形參,因為 a 是指向 int[] 這樣一種資料類型的數組指標,但下標大小沒有確定。而 int a[][8] 可以,並可以直接用二維數組名(無須顯示轉換)做其實參。

6、在處理一個數組的元素時,使用指標自增(p++)的方式通常比直接使用數組下標更快,使用指標能夠使程式得以最佳化。

7、C 允許定義多維陣列,維數上限由編譯器定義。但多於三維的數組並不常用,因為多維陣列所需的記憶體空間對維數呈指數增長。並且,計算各維下標會佔用 CPU 時間(存取多維陣列元素的速度比存取一維數組元素的速度慢)。

8、對數組初始化時注意,C89 要求必須使用常量初始化字元,而 C99 允許使用非常量初始化字元來初始化本地數組。

二、串(String):數組(尤其是一維數組)最常用的地方。

1、C 沒有專門的字串變數,對於它的操作全部由一維數組實現。字串是字元數組的一種特殊形式,唯一的區別就在於它是作為一個整體操作,而普通數組則不能。最終的差別就在末尾的 NULL(0)上。

2、注意與 C++ 中 string 類型(為字串處理提供了 OO 的方法)的區別,C 並不支援它。

3、初始化操作:要使用字串常量時則把它放到資料區中的 CONST 區(資料區、全域變數區和靜態變數區),用字串常量初始化字元數組時有一個複製內容的操作,而不僅僅是用一個指標指向它。實際上字串常量是在常量區還是堆區、採用何種儲存結構、以及是否連續的問題,取決於不同的編譯器。

4、串的輸入與輸出:下面的函數均由 <stdio.h> 定義。

(1、)printf("%s", str);

(2、)puts(str);

(3、)scanf("%s", str);

(4、)gets(str);

5、串運算:下面的函數均由 <string.h> 定義。

strcpy(s1, s2),strcat(s1, s2),strlen(str),strcmp(s1, s2),strchr(s1, ch),strstr(s1, s2)。

[注意]字元數組、字元指標之間的 == 比較是地址的比較,結果不可能是 true。但字串常量的 == 比較則不一定:VC 對 == 進行了重載,將字串常量的地址比較變為內容比較(如同輸出字元指標實際是輸出字串一樣,都是重載在作怪),因而 ("abc" == "abc") 在 VC 中成立,但在 BC 中則不成立。為避免二義性,應該儘可能用 strcmp() 來比較。

三、指標(Pointer):

指標是 C 語言的精華,正確理解並靈活運用指標是衡量能否成功地編寫 C 程式的標準。

1、使用指標的好處:

--> 能夠為調用函數靈活地修改實參變數的值。

--> 支援動態記憶體分配,能夠方便地實現動態資料結構(如二*樹和鏈表)。

--> 可以提高某些程式的效率。

--> 實現緩衝方式的檔案存取。

2、指標是地址。技術上,任何類型的指標都可以指向記憶體的任何位置。但是指標的操作都是基於類型的。

指標操作是相對於指標的基底類型而執行的,儘管在技術上指標可以指向對象的其它類型,但指標始終認為它指向的是其基底類型的對象。指標操作受指標類型而不是它所指向的物件類型的支配。

3、指標運算式:原則上講,涉及指標的運算式符合其它 C 運算式的規則。

(1、)printf("%p", …); /* 以宿主要電腦使用的格式顯示地址 */

(2、)指標轉換:

--> void * 型:為 Generic Pointer,常用來說明基底類型未知的指標。 它允許函數指定參數,此參數可以接受任何類型的指標變數而不必報告類型失配。 當記憶體語義不清時,也常用於指原始記憶體。

[例]一個函數可以返回“多個”類型(類似於 malloc()): void * f1(void *p) { return p; } void main(void) { int a=100; int *pp=&a; printf("%d/n", *((int*)f1(pp))); }

--> 其它類型的指標轉換必須使用明確的強制類型轉換。但要注意,一種類型的指標向另一種類型轉換時可能會產生不明確的行為。

--> 允許將 int 型轉換為指標或將指標轉換為 int 型,但必須使用強制類型轉換,且轉換結果是已定義的實現,可能導致非定義的行為。(轉換 0 時不需要強制轉換,因為它是 NULL 指標)

--> 為了與 C++ 更好地相容,很多 C 程式員捨棄了指標轉換,因為在 C++ 中,必須使用強制類型轉換,包括 void * 型。

(3、)指標算術:可以作用於指標的算術操作只有加法和減法。 --> 指標與整數的加、減法。 --> 從一個指標中減去另一個指標(主要目的是為了求指標位移量)。

(4、)指標比較:主要用於兩個或多個指標指向共同對象的情況。

4、初始化指標:

(1、)非靜態局部指標已聲明但未賦值前,其值不確定。

(2、)全域和靜態局部指標自動初始化為 NULL。

(3、)賦值前使用指標,不僅可能導致程式癱瘓,還有可能使 OS 崩潰,錯誤可謂嚴重之至!

(4、)[習慣用法]對於當前沒有指向合法的記憶體空間的指標,為其賦值 NULL。 因為 C 保證空地址不存在對象,所以任何null 指標都意味著它不指向任何對象,不應該使用它。 用null 指標來說明不用的指標基本上就是程式員遵守的協定(但並不是 C 的強制規則)。

[例]int *p=NULL; *p=1; /* ERROR!*/ /* 能通過編譯,但對 0 賦值通常會使程式崩潰 */

5、函數指標:void process(char *a, char *b, int (* appel)(const char *, const char *)); /* 函數是通過指標調用的,appel 是函數指標 */

在工作中常需要向過程傳入任意函數,有時需要使用函數指標構成的數組。如在解釋程式運行時,常需要根據語句調用各種函數。此時,用函數指標構成的數組取代大型 switch 語句是非常方便的,由數組下標實施調用。

6、動態分配(Dynamic Allocation)記憶體空間:指程式在運行中取得記憶體空間。

全域變數在編譯時間分配記憶體空間,非靜態局部變數使用棧區,兩者在運行時使用固定長度的記憶體空間。

(1、)為了實現動態資料結構,C 動態分配函數在堆區分配記憶體空間。堆是系統的自由記憶體區,空間一般都很大。

(2、)核心函數:malloc() 和 free()。

(3、)堆區是有限的,分配記憶體空間後必須檢查 malloc() 的傳回值,確保指標使用前它是非空的。

(4、)絕對不要用無效的指標調用 free(),否則將破壞自由表。

7、雜項:主要是一些常見的與指標有關的問題。

(1、)在某些情況下,用 const 來限制指標對提高程式的安全性有重要意義。大家可以仔細看一下微軟編寫的系統函數便會有所體會。

(2、)指標的錯誤難於定位,因為指標本身並沒有問題。問題在於,通過錯誤的指標操作時,可能引入最難排除的錯誤:程式對未知記憶體區進行讀或寫操作。

--> 讀:最壞的情況是取得無用資料。 --> 寫:可能衝掉其它代碼或資料。

這種錯誤可能要到程式執行了相當一段時間後才出現,因此把排錯工作引入歧途。

雖然使用指標可能導致奇怪的錯誤,但是我們不能因此而放棄使用指標。(記得我剛開始做項目的時候,由於懼怕指標,在我做的第一個項目裡,我使用了 200 多個數組……)使用指標時應當小心謹慎,請記住,一定要首先確定指標指向記憶體的什麼位置。

[例1]未初始化的指標(uninitialized pointer)。 int *p; scanf("%d", p); /* ERROR */ /* 把值寫到未知的記憶體空間 */

運行小程式時,p 中隨機地址指向安全區域(不指向程式的代碼和資料,或 OS)的可能性較大。但隨著程式的增大,p 指向重要地區的機率增加,最終使程式癱瘓。

[例2]對記憶體中資料放置的錯誤假定。 char a1[80], a2[80]; char *p1=a1, *p2=a2; if (p1<p2) { /* 處理 */ } /* 概念錯誤!*/

通常程式員不能確保資料處於記憶體中的同樣位置,不能確保各種平台都用同樣格式儲存資料,也不能確保各種編譯器處理資料的方法完全相同。比較指向不同對象的指標時,容易產生意外結果。

[例3]假設相鄰數組順序排列,從而簡單地對指標增值,希望跨越數組邊界。 int a1[10], a2[10]; int *p=a1; for (int i=0; i<20; i++) *p++=i; /* ERROR */

儘管在某些條件下可適用於某些編譯器,但這是假設兩個數組在記憶體中先存放 a1,隨後存放 a2 的條件下進行的。這種情況並不常有。

[例4]時刻關注指標的當前位置。 char a[80], *p; p=a; /* ERROR */ do { /* p=a; 應該把這句放在迴圈體內 */ gets(a); /* 讀入 */ while(*p) printf("%d", *p++); } while(strcmp(a, "DONE"));

第一次迴圈,p 從 a[0] 開始。第二次迴圈時,p 值從第一次迴圈的結束點開始。此時 p 可能指向另一個串,另一個變數,甚至是程式的某一段。

[例5]運算式的地址。 int *p=&(a+b); /* ERROR */

在 C 中,& 只能擷取一個變數的地址。在程式中,除變數外,其它運算式的儲存空間是不可訪問的。

四、指標與數組:

數組和指標關係非常緊密,幾乎就像 UNIX 和 C 的關係一樣:二者是不分家的。實際上,一維數組名可被不嚴格地認為是指標常量:可執行指標的操作;可按地址單元賦值和引用;也可用來初始化同型的指標變數;但不能對一維數組名賦值。

char p[]="Hello, World!"; char *p="Hello, Wrold!"; /* 兩條語句完全等價 */

1、指標數組:常用於放置指向串的指標。

在程式中,對串的操作是很常見的,但就介紹過的技術來看還是有一定的局限性的。此外,在一些特殊的項目中,可能需要把若干個串作為參數傳遞給函數,但串的長度不能確定,這是採用定長的數組顯然不合適。

[例1]給定錯誤編號後,輸出錯誤資訊。 void print_error(int n) { static char *erreur[]={"Syntax Error/n", "Variable Error/n", "Disk Error/n"}; printf("%s", error[n]); }

[例2]典型應用:列印命令列參數(類似於 echo 命令)。 void main(int argc, char **argv, char **env) { while(*++argv) printf("%s ", *argv); }

[例3]訪問命令列參數中的字元(對 argv 施加第二下標)。 void main(int argc, char *argv[], char **env) { int i, j; for (i=0; i<argc; ++i) { j=0; while(argv[i][j]) { putchar(argv[i][j]); j++; } printf("/n"); } }

[注意] --> 不要像數組那樣按下標單獨賦值,也不要使用 *(p+n) 這樣的間接引用來修改某個單元的值,這樣做都可能引起運行錯,因為字串常量是在常量區,是不允許被修改的。嚴格來說,用一個普通指標指向一個常量是不對的,會產生一個“cannot convert from 'const type *' to 'type *'”的編譯錯誤。字元指標的初始化是一個特例,可用一個字元指標變數指向一個字串常量,但仍然不能修改其內容。將const char * 強制轉換成 char * 能獲得正確的地址,可引用,但仍然不能修改其內容。(將const int * 轉換成 int * 則無法獲得正確的地址,這是字串常量的特殊性)。

--> 按下標或*(p+n)這樣的間接引用來讀取某單元的內容有時是可行的,這取決於不同的編譯器對字串常量的存放方式:在常量區還是堆區;採用何種儲存結構;是否連續(塊鏈就不連續)。而字元數組這樣引用和賦值總是可行的,因為它開闢了一塊連續的空間並複製了內容。

2、對於數組 a[],輸出 a、*a、&a 的值都一樣,但我們並不能認為含義一樣。a 有雙重含義,只不過通過 printf() 表現出來的是首元素地址;*a 中 a 取了其數組指標含義,值為第一個元素的地址;&a 中 a 取了其自訂資料類型含義,值為結構中第一個元素的地址。若把 a 賦給數組指標變數 p,則 &p 是 p 的地址,因為 p 並不具備數組這一層自訂資料類型含義。用數組名初始化指標或數組指標時做了自動類型轉化,取了其指標含義。

也就是說:不要把賦值就理解為一樣了。這個等到將來學過 C++ 後就會有一個比較深入的認識了。C++ 的運算子多載和自動類型轉化隱含了很多東西。

3、純指標數組:即 JAVA 中定義數組的方法。這樣的數組是真正的多級指標,不能用 sizeof() 求數組大小,但可以實現多維動態,而且存取效率較高(無須做乘法來定址)。 [例]int n1,n2,n3;

int *p1=new int[n1]; //p1[n1],但與普通數組名不同,p1 不是指標常量,而是有記憶體的指標變數。

int **p2=new int*[n1]; for(int I=0;I<n2;I++) p2[I]=new int[n2];

int ***p3=new int**[n1]; for(int j=0;j<n1;j++) { p3[j]=new int*[n2]; for(int k=0;k<n2;k++) p2[j][k]=new int[n3]; //三維動態數組p3[n1] [n2] [n3] } p2-

4、指向指標的指標(Pointers to Pointers):指標數組名就是二級指標。 在實際工作中很少需要使用指向指標的指標,它容易引起概念錯誤。

5、動態分配的數組(Dynamically Allocated Array):

[例1]char *p=(char *) malloc(80); /* 必須對 p 進行下面的測試,以避免使用null 指標 */ if (!p) { printf("ERROR/n"); exit(1); }

get(p); for (int i=strlen(p)-1; i>=0; i--) putchar(p[i]); free(p);

[例2]int (*p)[10] = (int (*)[10]) malloc(40*sizeof(int)); /* 為了與 C++ 相容,必須捨棄所有的指標轉換 */ if (!p) { printf("ERROR/n"); exit(1); }

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.