【C\C++語言入門篇】-- 數組與指針
前面一篇我們介紹了指針,相信大家對指針不再是那么陌生,雖然在一些大膽的指針強制類型轉換上有的讀者還不習慣。但是至少大家心里有個數,指針式如此的靈活,以至于你可以操作得比較底層或者根本越過一些語法的限制。這可能也是眾多程序員抨擊CC++不安全的因素之一。安不安全不是本文想要表達的,這里只需要記住一點,如果你有足夠把握,那么你絕對可以毫不猶豫的運用。
本文依然不會離開指針的影子,前面一篇還有沒介紹完的,之前本來想在前面一篇介紹,但是發現在本篇介紹更適合一些。數組和指針可以說是兩家親,很多初學的讀者對這兩者的概念模棱兩可。他們之間有什么聯系和區別也是很多初學的讀者最希望明白的,本文就為解決這個困擾,讓指針和數組進一步加深。還是記住我們的出發點,以發散的思維去理解去聯想。注重思考過程,這個過程最終只需要用程序來表達而已。
首先還是看看什么是數組,數組即是一段連續的空間(內存空間)。從這句話中,我們可以注意到數組其實就是一段空間,而且還是連續的。好了,此時對數組的基本特征就有個大致的了解了。那么內存空間是怎么樣表達出來的呢?很簡單:
char szName[ 16 ];
這兩句即為數組了,在這里a為一個擁有100個int類型元素的數組。在這里我們也可以理解int并不是數組a的類型,而是數組內部元素的類型。它表示數組內部每個元素都是32有符號整數。這樣想來便聯系到了指針,int* p; p代表它指向的內存地址里存放的數據是int型的。第二個szName同理也表示其每個元素的類型就是char型。這樣理解對指針數組和數組指針有幫助,先放這里容后介紹。
這里a和szName并沒有被初始化,那么它們里面每個元素的值我們可以認為是亂碼。也就是說是隨便填充的一些值。當然為什么填充這些值也是有道理的,在不同的平臺可能填充的值不一樣。在windows下通常被填充成類似0xcdcdcdcd或者0xcccccccc之類的。這些值在匯編層面上去理解會更直接,在這里我們就認為它是隨便填充的一些值吧。就認為這些值對于我們正常的程序是沒有什么用處的。
從程序表現上我們已經知道數組的聲明,那么怎么跟指針聯系和區別呢?先貼代碼:
我們從前一篇只可以知道,這時指針p指向了數組a的首地址,這里直接將a賦值給了p。那么可以斷定這個數組名a即是代表數組的首地址。既然代表的是首地址,那么a可以看成是一個指向這100個int類型元素中首元素所在的內存地址。CC++直接規定數組名字將作為指向數組的首地址的指針:
元素1 <---- a
元素2
元素3
元素4
...
因此,將a直接賦值給p是完全合法可行的。當然也可以不仗著這個規定,我們可以使用:
這樣也一樣可以取得數組的首地址,也就是第一個元素的內存地址。
到這里,我們該思考一下。數組就其本質來講它就是一段連續的內存空間,哪怕只有一個元素它也是數組,因為這些元素都是放在內存里面的,它肯定就有自己的內存地址,一牽涉到內存地址,那么它就能用一個指針指向它。因此就聯系上了指針。而為何要將數組的名作為一個指針看待,其實數組名并不能等同于指針,因為數組名還具有比指針更多的信息,例如:
int size_a = sizeof( array);
int* pArray = array;
int size_p = sizeof( pArray);
從這兩段代碼來看,大家應該都知道size_a和size_b的值分別為40和4。這里區別就明顯出來了吧。array如果看成是指針,那么size_a就應該為4長度。前面一篇我們介紹了指針變量即是存放了一個32位無符號的整數。因此指針在32位平臺下長度可以直接認為是4。那么這里為什么剛好是10個元素的byte數呢?原因很簡單,編譯器在處理數組名的時候便知道該數組聲明了多大空間,就會求出這個數組實際占用了多少字節的內存。而當我們將數組首地址賦值給了pArray指針后,pArray將丟失了數組array的大小。這個丟失現象在比如函數傳參數的時候被體現得淋漓盡致,以后咱們講到函數時再談。編譯器將不會去追根究底也不可能去追究被賦值成了什么。因此,要說數組名完全等同于指針也是不準確的。就其本質可以說是等同的,都是存放的數組的首地址。
在前面指針篇我們并沒有談指針++、--操作。在這里結合數組來闡述一下。指針累加或累減同樣跟類型有關,比如:
p++;
p--;
這里累加和累減,說是跟類型有關。那么通過觀察或者猜想,我們可以知道p執行一次累加后所保存的內存地址值是向后4個字節。因為類型是int。這里p指向的是數組a的第0個元素的地址,累加后將指向第一個元素。之間間隔4個字節。累減同樣也是間隔4個字節。同樣類似:
p = p + 2;
p = p + 1;
這些不使用累加的操作,也是要根據類型計算實際的地址應該加多少字節,在這里,+1就等于地址加4,+2就等于加8。如果是其他類型比如結構體類型,累加等操作換算成實際地址的話將一次跳躍結構體大小那么多個字節。以后講結構體時再一一說明。
問題一: int dis = &a[0] -&a[1]; dis的值是什么?為什么?
從累加和累減這個特性,我們可以猜想設計指針之初,應該是考慮到了指針指向數組這一特性,因此累加和累減就變成了數組訪問某個元素的特征。因此這里假如要訪問a的某個元素,我們將可以使用:
p[ 0 ] p[ 1 ] ... p[ n ]這種形式。當然用指針操作一定得把握好數組的大小,否則就會讀寫越界。后果是很嚴重的。到這里又有我們值得比較和聯想的一點了,比如有這樣的代碼:
a++;
(( int*)a )++;
int var = a[ 0 ];
問題二:上面兩個累加這樣的代碼合法嗎?由此是否可以得出數組名和指針的什么差異?
問題三:*p++和(*p)++的區別?
從上面反映的每一個細節,告訴我們應該習慣去思考。去總結讓我們模棱兩個的東西具有的真實聯系和區別。我相信這樣對你很有好處。下面再談談指針數組,所謂指針數組就是一個數組里面存放的全是指針,本質其實就是所有元素都是內存地址。也就是32位無符號整數。完全可以當成是unsigned int數組。
{
int a[ 10 ];
int* ap[ 10 ];
int i;
for ( i = 0; i < 10; ++i )
{
ap[ i ] = &a[ i ];
}
return 0;
}
上面的代碼中ap就是一個指針數組,每個元素類型為int型數據所在的內存地址(可謂:int型指針)。然后一個循環將a數組的所有元素的內存地址復制給指針數組的每個元素,a數組雖然沒有初始化也沒有給定元素的值,但是數組一旦聲明就已經分配了內存空間。這里是局部數組在棧空間上。因此循環里面取a的每個元素的地址是絕對合法的。ap在聲明的時候也沒有初始化,里面的每個指針元素的初始值也跟普通數組一樣是亂的。假如我們在循環之前有此操作:
這樣將會報錯說指針未初始化。假如初始化了,但是初始化成了非法的內存地址,那么這樣間接訪問也是相當危險的。上面的循環將每一個a數組元素的地址都賦值給了ap的對應元素。那么:
p每累加1,p保存的內存地址,能夠準確對應于ap中的每個元素的值(內存地址)。這也說明了數組和指針的一個聯系。這里又可以總結了:
如果指針指向的是一塊合法連續的空間,那么此指針可以當著數組一樣來看到,只需注意上限和下限就可以了。有此也可以認為任何指針隨便賦值(指向)任何內存地址,都可以用數組下標訪問或者累加累減然后間接訪問,只要這塊內存你需要且合法。
再看看前面所說的void類型的指針。
ppp++;
問題四:這句是否合法?為什么?(這點在前面一篇已經說明了原因)
最后再看看數組指針,還是先看代碼:
{
int a[ 2 ][ 10 ];
int (*p )[ 10 ]; //數組指針
p = a;
p = &a[ 0 ];
p = a[ 1 ];
return 0;
}
在上面的程序中,p為數組指針。顧名思義就是這個指針存放的就是一個數組。從此說法可以推出這個指針指向的就是數組的首地址。這里的p就是指向的一個擁有10個元素的整型數組。中括號里面的10就代表它指向的數組是10元素的。這里我們定義了一個二維數組a,p = a;就將a賦值給了p,咱們再想想其內存關系。
a[ 0 ]: { a[ 0 ][ 1 ], a[ 0 ][ 1 ],..., a[ 0 ][ 9 ] } <------p[ 0 ]
a[ 1 ]: { a[ 1 ][ 1 ], a[ 1 ][ 1 ],..., a[ 1 ][ 9 ] } <------p[ 1 ]
因此,p跟a的內存關系就清晰了。p指向的即是一維數組,這里是二維數組a。二維數組可以看成幾行幾列,每一行就是一個一維數組。p就可以稱作是行指針。大家應該明白了吧!那么p++將會跳躍一行那么多字節,這里一行是10個int元素,那么就是40個字節。大家可以自己寫程序觀察。p = &a[ 0 ]; 即表示將二維數組的第1行賦值給p。同樣可以將第一行賦值給p, p = &a[ 1 ]; 由此更形象說明p乃行指針了。
問題五:上面的程序中:p = a[ 1 ];這句合法嗎?為什么?
有的讀者又會提出一個疑問了,有沒有指向二維數組的指針呢?這個大家可以在空間中想象一下,假如指向二維數組,那么指針累加將是增加立方體的厚度(三維空間,三維數組)。就好比我們吃的三明治,三片中的一片就代表我們的二維數組指針。這里我們不做討論。
同樣在這里還得說明一下二級指針和二維數組的聯系和區別。同樣區別指針和數組名在前面已經說過。我們這里只看看二維數組到二級指針后的訪問取值及內存關系。
如果你嘗試:
int** p = ( int**)a;
這樣將帶來災難,語法是沒有問題,本來我們的a數組里面是整數,被強制轉換成指針后。p[ 0 ]就是a的第一行第一個元素的值,然后再p[0][0]就是取p[ 0 ]的值作為內存地址存放在里面的那個值。因此除非你的這個a[0][0]是你掌握的數據,否則你這樣用將帶來毀滅性的后果——崩潰。然而在這里p[0]給了我們一個啟示,我們可以再寫一段代碼:
{
char* ap[ 3 ] = {"hello","happy","good"};
char** app1 = ap;
return 0;
}
這里定義了一個指針數組ap,然后轉換成char**二級指針。我們通過下標運算來看關系。首先ap也可以看成是一個3行6列的二維數組。因此:
ap[ 0 ][ 0 ]的值就為'h'字符。
app[ 0 ][ 0 ]的值也是'h'字符。
相信你已經明白了二維數組和二級指針的區別和關系了。將一個二維數組直接強制類型轉換成二級指針,這樣會出問題,因為這樣一轉。你用二級指針下標訪問第一行數據時,第一行第一個元素的地址將被認為是當前存放的值作為下一級的指針值(地址)。也就是說:
char** app1 = ( char**)ap;
那么,app1[ 0 ](也就是*app1,*(*app1)表示app1[0][0])的值(內存地址)就變成了"hello"字符串的前4字節"hell"逐字符對應的ASCII碼了:0x6c6c6568。
68 65 6c 6c
h e l l
至于0x6c6c6568為什么給倒過來了,是因為我的CPU是小端存儲,高字節被認為是整數的高位,低字節被認為是整數的低位。這下大家知道二維數組和二級指針的關系了吧。這里舉例是一個char數組,大家也可以舉一個int型的數組。這里就不寫了!