浅谈数组和指针
???? 這應該不是議數據和指針,應該是閱讀筆記吧!但是其這個題目更好點。
????? C專家編程第四章內容:令人震驚的事實:數組和指針并不相同。自己也收獲不小,了解很多基礎的東西,對那些初入C開發人員有很好的幫助。那廢話少說吧。
???? 我們總以為數據和指針是完全等同的,兩者是可以互換的,這種說法是片面的。我們在編程中經常使用全局變量,在其他文件中聲明中也可以使用這個全局變量。下面就舉個例子說明:
文件1:
Int mango[100];
文件2:
Extern int *mango;? /*①*/
...
/*一些使用mango的代碼*/
????? 這里,文件1定義int 變量mango,但文件2聲明它為指針int *型,這里顯然是類型不匹配,也說明了數組和指針并不是完全等同的。這樣使用肯定是錯誤的,代碼不可能正常運行。那么應該怎么聲明呢, 如下:
Extern int mango【】;/*②*/
說明: ①語句聲明mango是個int*型;②聲明mango為int 型數組,長度尚未確定,其存儲在別處定義。
?????
?? 1 ?那么什么是聲明?什么是定義?
??????? 在C語言中對象必須有且只有一個定義,但可以有多個聲明。定義是一個特殊的聲明,它創建一個對象;聲明只是說明了在其他地方創建了這個對象,它允許在這里使用:
*******************************************************************************
定義???? 只能出現在一個地方? 確定對象的類型并分配內存,用于創建新的對象
聲明???? 可以多次出現??????? 描述對象的類型,用于指代其他地方定義的對象
*******************************************************************************
??? 只要記住下面的內容可以分清定義和聲明:
聲明相當于普通的聲明:它說明的并非自身,而是描述其他的地方創建的對象。
定義相當于特殊的聲明:它為對象分配內存。
?????? Extern 對象聲明告訴編譯器對象的類型和名字,對象的內存分配在別處進行。由于并未在聲明中為數組分配內存,所以并不需要提供關于數組長度的信息。對于多維數組需要提供除最左邊一維其他維的長度—這是給編譯器足夠的信息產生相應的代碼。
?? 2 、數組和指針式如何訪問的
???? 這里講述對數組的引用和對指針的引用有何不同之處,首先需要注意的是“地址y“和“地址y的內容”之間的區別。這里一個相當微妙之處,是在大多數編程語音中我們用同一個符號來表示這兩個東西,有編譯器根據上下文環境來判斷他的具體含義。以一個簡潔的例子來說明:
???
????? 出現在賦值符左邊的符號有事被稱為左值(由于它位于“左手邊”或“表示地點”),出現在賦值符右邊的符號有時則被稱為右值(由于它位于右手邊)。編譯器為每個變量分配一個地址(左值),這個地址在編譯時可知,而且該變量在運行時一直保存于這個地址。相反,存儲于變量中的值(右值)只有在運行時才知。如果需要用到變量中存儲的數值,編譯器就發出質量從指定地址讀取變量并將它存儲在寄存器中。
????? 這也是為什么在if(-1 == x)判斷語句中,如果將“==”誤寫為“=”編譯器會報警的原因(相信很多人只知道這么用,不知道為什么吧?)
??? 這里的關鍵在于每個符號在編譯時可知。所以如果編譯器需要一個地址(可能還需要加上偏移量)來執行某種操作。它可以直接進行操作,并不需要增加指令首先取得具體的地址,相反對于指針在運行時首先取得它的當前值,然后才能對它進行接觸引用操作。具體看下圖:
?????? 這也是為什么extern char a[] 與 extern char a[100]等價的原因。這兩個都說明a是個數組,也就是個內存地址,數組內的字符可以從這個地址中找到。編譯器并不需要知道這個地址有多長,因為它只產生偏離起始地址的偏移地址。從數據中提取一個數值,只需要簡單的知道現實a的地址加上下標,需要的字符就位于這個地址中。
???? 相反,如果聲明extern char *p,它告訴編譯器P是個指針,它指向的是一個字符。為了取得這個字符,必須知道P的內容,把他作為字符的地址取其內容就是這個字符,指針的訪問比較靈活,但需要增加一次額外的提取。
??? 定義成指針,但以數組方式引用
現在讓我看下定義成指針,當以數組方式引用會發生什么,看下面的例子:
?
對照圖C的訪問方式
Char *p = “abcdefgh“;?? ...?? p[3]
和圖A的訪問方式:
Char a[] = “abcdefgh”;? ....? a[3]
在這兩種情況下,都可以取得字符‘d’,但是兩者的途徑是不一樣的。
? 當書寫了 extern char *p,然后用p【3】來引用其中的元素時,其實質是圖A和圖B的訪問方式的組合。首先,進行圖B所示的間接引用。然后,如圖A所示用下標作為偏移量進行直接訪問。更為正式的說明是,編譯器將會:
1.?????? 取得符號P的地址,提取存儲于此處的指針。
2.?????? 把下標所表示的偏移量與指針的值相加,產生一個地址。
3.?????? 訪問上面的地址,取得數值。
?到這也許已經知道為什么文章開頭舉得例子中文件2中聲明Extern int *mango;? /*①*/
為什么會出現編譯失敗的問題?既然把P聲明成指針,那么不管P原先定義為指針還是數組,都會按照上面所示的三個步驟進行操作,但是只有當P原來定義成指針時,上面這個方法才是正確的。考慮上面那個問題。當用mango【i】這種形式提取這個聲明的內容時,實際上得到是一個字符,按照上面的方法,編譯器卻把它當成一個指針,把ACSII字符當作成地址顯然是錯誤的。
?? 數組和指針的區別:
NO | 指針 | 數組 |
1 | 保存數據的地址 | 保存數據 |
2 | 簡潔訪問數據。首先取得指針的內容,把它當成地址,然后從這個地址取提取數據。如果指針有一個下標【i】,那么就把指針的內用加上i的地址,從中提取數據 | 直接訪問數據。A【i】只是簡單地以a+i為地址取得數組。 |
3 | 通常用于動態內存分配 | 通常用于固定數目其數據類型相同的元素 |
4 | 相關函數malloc()和free() | 隱身分配和刪除 |
5 | 通常指匿名數據 | 自身即為數據名 |
?
再議數組:
??
1、 什么時候數組和指針相同
在實際的運用過程中,數組和指針互換的情景比兩者不可互換的情形更為常見,讓我們分別來考慮“聲明”和“使用”兩種情況。
?
聲明本來還可以分為3中情況:
????????? 外部數組的聲明;
????????? 數組的定義(記住,定義是聲明的一種特殊情況,它分配內存空間,并提供一個初始值);
????????? 函數參數的聲明。
所有作為函數參數的數組名總是可以通過編譯器轉換為指針。在其他情況下,數組的聲明就是數組,指針的聲明就是指針,兩者不能混淆。但是在使用數組時,數組總是可以寫成指針的形式,兩者可以互換。下面是總結:
?
為什么會發生混淆
?
當人們學習編程時,一開始總是把所有的代碼都放在一個函數里。隨著水平的進步,他們把代碼分別放在幾個函數中,在水平繼續提高后,他們最終學會了如何用幾個文件來構造個程序。在這個過程中,他們可以看到大量的作為函數參數的數組和指針,在這種情況下,他們是完全可以互換的,如下所示:
char my_array[10]; char *my_ptr; ... i = strlen(my_array); j = strlen(my_ptr);printf("%s,%s",my_array,my_ptr);上面清晰地展示了數組和指針的課互換性,人們很容易忽視這只是放生在一種特定的上下文環境中,也就是說作為一個函數調用的參數使用。更糟的是它可以如下編寫:
printf("array at location %x holds string %s",a,a);
??在同一個語句中,即把數組名作為一個地址(指針),有把他作為一個字符數組。
?
? 在C標準中對“什么時候數組和指針時相同的”作了如下的聲明:
規則1:表達式中的數組名(與聲明不同)被編譯器當做一個指向該數組第一個元素的指針。
規則2:下標總是與指針的偏移量相同。
規則3:在函數參數聲明中,數組名悲編譯器當做指向該數組第一個元素的指針
下面詳細介紹這3個規則的含義:
規則1:“表達式數組名”就是指針
?? 上面的規則1,2一起理解,就是對數組下標的引用總是可以寫成“一個指向數組的起始地址的指針加上偏移量”。例如,假如我們聲明:
int a[10],*p,i = 2;
/*我們可以通過下面集中方法訪問a[i]*/
p = a;
p[i];
/*****************/
p = a;
*(p + i);
/******************/
p = a + i;
*p;
? 應該還有很多方法。對數組的引用如a[i]在編譯時總是被編譯器改寫成*(a+i)的形式,C語言要求編譯器必須具備這個概念性的行為。取指針或者數組名+方括號【】中的下標 就是訪問元素的地址,再取其值。所以,你也記住,在表達式中,指針和數組時可以互換的,因為他們在編譯器的最終都是以指針的形式。因此我們也可以理解下面的取值的表示方法為什么相同了并都是正確的。
a[6] = ......;
6[a] = .....;
規則2:C語言把數組下標作為指針的偏移量
?? 我們通常認為:在編寫數組算法時,使用指針比數組 更效率 。這種說法在通常情況下是錯誤的。在一維數組和指針引用所產生的代碼并不具有顯著的差別。這里可以參考前面的內容,自己在研究下。
規則3:作為函數參數的數組名等同于指針
?
| 術語 | 定義 | 例子 |
| 形參(parameter) | 他是一個變量,在函數定義或聲明的原型 中定義。又稱形式參數 | int power(int base;int n); base,n都是形參 |
| 實參(argument) | 在函數實際調用過程中傳遞給函數的值。 | I = power(10,2); 10和2是實參, |
在標準中規定,類型的數組 的形參的聲明應該調整為 類型的指針。下面三種寫法都是一樣的 my_function (int *a); my_function (int a[]); my_function (int a[200]);
??????數組形參是如何被引用的?
??? 下圖展示了對一個下標形式的數組形參進行訪問所需要的幾個步驟:
? 注意:有一種操作只能在指針里進行而無法在數組中進行,那就是修改它的值。數組名是不可修改的左值,它的值是不能改變的。
?***********************************************************************************************************************************************************************
錯誤1 :定義數組,聲明指針
? ? 文件 1 中定義如下:
chara[100];
文件 2 中聲明如下:
externchar*a;
? ? ?這里,文件 1 中定義了數組 a,文件 2 中聲明它為指針。這有什么問題嗎?平時不是總說數組與指針相似,甚至可以通用嗎?但是,很不幸,這是錯誤的。通過上面的分析我們也能明白一些,但是“革命尚未成功,同志仍需努力” 。你或許還記得我上面說過的話:數組就是數組,指針就是指針,它們是完全不同的兩碼事!他們之間沒有任何關系,只是經常穿著相似的衣服來迷惑你罷了。
下面就來分析分析這個問題:?
?
?
?
?extern char * a;
錯誤2:定義指針,聲明數組:
? ??顯然, 按照上面的分析, 我們把文件 1 中定義的數組在文件 2 中聲明為指針會發生錯誤。同樣的,如果在文件 1 中定義為指針,而在文件中聲明為數組也會發生錯誤:
文件 1
? ? ? char*p = “abcdefg”;
文件 2
? ? ? extern charp[];
? ? ? 在文件 1 中, 編譯器分配 4 個 byte 空間, 并命名為 p。 同時 p 里保存了字符串常量 “abcdefg”的首字符的首地址。這個字符串常量本身保存在內存的靜態區,其內容不可更改。在文件 2中,編譯器認為 p 是一個數組,其大小為 4 個 byte,數組內保存的是 char 類型的數據。在文件 2 中使用 p 的過程如下圖:
總結