【C/C++語言入門篇】-- 數組與指針

 前面一篇咱們介紹了指針,相信你們對指針再也不是那麼陌生,雖然在一些大膽的指針強制類型轉換上有的讀者還不習慣。可是至少你們內心有個數,指針式如此的靈活,以致於你能夠操做得比較底層或者根本越過一些語法的限制。這可能也是衆多程序員抨擊CC++不安全的因素之一。安不安全不是本文想要表達的,這裏只須要記住一點,若是你有足夠把握,那麼你絕對能夠絕不猶豫的運用。程序員

 

本文依然不會離開指針的影子,前面一篇還有沒介紹完的,以前原本想在前面一篇介紹,可是發如今本篇介紹更適合一些。數組和指針能夠說是兩家親,不少初學的讀者對這二者的概念模棱兩可。他們之間有什麼聯繫和區別也是不少初學的讀者最但願明白的,本文就爲解決這個困擾,讓指針和數組進一步加深。仍是記住咱們的出發點,以發散的思惟去理解去聯想。注重思考過程,這個過程最終只須要用程序來表達而已。windows

 

首先仍是看看什麼是數組,數組便是一段連續的空間(內存空間)。從這句話中,咱們能夠注意到數組其實就是一段空間,並且仍是連續的。好了,此時對數組的基本特徵就有個大體的瞭解了。那麼內存空間是怎麼樣表達出來的呢?很簡單:數組

int    a[ 100 ];安全

char szName[ 16 ];app

 

這兩句即爲數組了,在這裏a爲一個擁有100個int類型元素的數組。在這裏咱們也能夠理解int並非數組a的類型,而是數組內部元素的類型。它表示數組內部每一個元素都是32有符號整數。這樣想來便聯繫到了指針,int* p; p表明它指向的內存地址裏存放的數據是int型的。第二個szName同理也表示其每一個元素的類型就是char型。這樣理解對指針數組和數組指針有幫助,先放這裏容後介紹。ide

 

這裏a和szName並無被初始化,那麼它們裏面每一個元素的值咱們能夠認爲是亂碼。也就是說是隨便填充的一些值。固然爲何填充這些值也是有道理的,在不一樣的平臺可能填充的值不同。在windows下一般被填充成相似0xcdcdcdcd或者0xcccccccc之類的。這些值在彙編層面上去理解會更直接,在這裏咱們就認爲它是隨便填充的一些值吧。就認爲這些值對於咱們正常的程序是沒有什麼用處的。函數

 

從程序表現上咱們已經知道數組的聲明,那麼怎麼跟指針聯繫和區別呢?先貼代碼:spa

int* p = a;   // 這裏a使用上面的a[ 100 ]數組。設計

咱們從前一篇只能夠知道,這時指針p指向了數組a的首地址,這裏直接將a賦值給了p。那麼能夠判定這個數組名a便是表明數組的首地址。既然表明的是首地址,那麼a能夠當作是一個指向這100個int類型元素中首元素所在的內存地址。CC++直接規定數組名字將做爲指向數組的首地址的指針:指針

元素1  <---- a

元素2    

元素3     

元素4

  ...

所以,將a直接賦值給p是徹底合法可行的。固然也能夠不仗着這個規定,咱們可使用:

int* p = &a[ 0 ];

這樣也同樣能夠取得數組的首地址,也就是第一個元素的內存地址。

 

到這裏,咱們該思考一下。數組就其本質來說它就是一段連續的內存空間,哪怕只有一個元素它也是數組,由於這些元素都是放在內存裏面的,它確定就有本身的內存地址,一牽涉到內存地址,那麼它就能用一個指針指向它。所以就聯繫上了指針。而爲什麼要將數組的名做爲一個指針看待,其實數組名並不能等同於指針,由於數組名還具備比指針更多的信息,例如:

int array[ 10 ];

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的大小。這個丟失現象在好比函數傳參數的時候被體現得淋漓盡致,之後我們講到函數時再談。編譯器將不會去追根究底也不可能去追究被賦值成了什麼。所以,要說數組名徹底等同於指針也是不許確的。就其本質能夠說是等同的,都是存放的數組的首地址。

 

在前面指針篇咱們並無談指針++、--操做。在這裏結合數組來闡述一下。指針累加或累減一樣跟類型有關,好比:

int* p = a;  // a is a[ 100 ]

p++;

p--;

 

這裏累加和累減,說是跟類型有關。那麼經過觀察或者猜測,咱們能夠知道p執行一次累加後所保存的內存地址值是向後4個字節。由於類型是int。這裏p指向的是數組a的第0個元素的地址,累加後將指向第一個元素。之間間隔4個字節。累減一樣也是間隔4個字節。一樣相似:

int* p = a;  // a is a[ 100 ]

p = p + 2;

p = p + 1;

這些不使用累加的操做,也是要根據類型計算實際的地址應該加多少字節,在這裏,+1就等於地址加4,+2就等於加8。若是是其餘類型好比結構體類型,累加等操做換算成實際地址的話將一次跳躍結構體大小那麼多個字節。之後講結構體時再一一說明。

問題一:  int dis = &a[0] - &a[1];  dis的值是什麼?爲何? 

 

從累加和累減這個特性,咱們能夠猜測設計指針之初,應該是考慮到了指針指向數組這一特性,所以累加和累減就變成了數組訪問某個元素的特徵。所以這裏假如要訪問a的某個元素,咱們將可使用:

p[ 0 ] p[ 1 ] ... p[ n ]這種形式。固然用指針操做必定得把握好數組的大小,不然就會讀寫越界。後果是很嚴重的。

 

到這裏又有咱們值得比較和聯想的一點了,好比有這樣的代碼:

int a[ 100 ];

a++;

( ( int* )a )++;

int var = a[ 0 ];

問題二:上面兩個累加這樣的代碼合法嗎?由此是否能夠得出數組名和指針的什麼差別?

 

int* p = a; // a[100]

問題三:*p++和(*p)++的區別?

 

從上面反映的每個細節,告訴咱們應該習慣去思考。去總結讓咱們模棱兩個的東西具備的真實聯繫和區別。我相信這樣對你頗有好處。

 

下面再談談指針數組,所謂指針數組就是一個數組裏面存放的全是指針,本質其實就是全部元素都是內存地址。也就是32位無符號整數。徹底能夠當成是unsigned int數組。

int main( void )
{
    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在聲明的時候也沒有初始化,裏面的每一個指針元素的初始值也跟普通數組同樣是亂的。假如咱們在循環以前有此操做:

int a_var = *( ap[ 0 ] ); // 此句的目的是想去ap第0個元素(指針)所指向的數據。這裏不打括號一個效果。

這樣將會報錯說指針未初始化。假如初始化了,可是初始化成了非法的內存地址,那麼這樣間接訪問也是至關危險的。

上面的循環將每個a數組元素的地址都賦值給了ap的對應元素。那麼:

int* p = a; // a[ 100 ]

p每累加1,p保存的內存地址,可以準確對應於ap中的每一個元素的值(內存地址)。這也說明了數組和指針的一個聯繫。這裏又能夠總結了:

若是指針指向的是一塊合法連續的空間,那麼此指針能夠當着數組同樣來看到,只需注意上限和下限就能夠了。有此也能夠認爲任何指針隨便賦值(指向)任何內存地址,均可以用數組下標訪問或者累加累減而後間接訪問,只要這塊內存你須要且合法。

 

再看看前面所說的void類型的指針。

void* ppp = ( void* )a;  // a[ 100 ]
ppp++;

問題四:這句是否合法?爲何?(這點在前面一篇已經說明了緣由)

 

最後再看看數組指針,仍是先看代碼:

int main( void )
{
    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  a[ 2 ][ 10 ];
int** p = ( int** )a; 

這樣將帶來災難,語法是沒有問題,原本咱們的a數組裏面是整數,被強制轉換成指針後。p[ 0 ]就是a的第一行第一個元素的值,而後再p[0][0]就是取p[ 0 ]的值做爲內存地址存放在裏面的那個值。所以除非你的這個a[0][0]是你掌握的數據,不然你這樣用將帶來毀滅性的後果——崩潰。然而在這裏p[0]給了咱們一個啓示,咱們能夠再寫一段代碼:

 int main( void )
{
    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 ap[ 3 ][ 6 ] = { "hello", "happy", "good" };
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型的數組。這裏就不寫了!

好了,本篇就到此吧!又寫了4個多小時了。已是次日0點多了。

上面提的幾個問題仍是比較值得初學的讀者思考的,加油!

 

 

【C++語言入門篇】系列:

【C++語言入門篇】-- HelloWorld思考

【C/C++語言入門篇】-- 基本數據類型

【C/C++語言入門篇】-- 調試基礎

【C/C++語言入門篇】-- 深刻指針

【C/C++語言入門篇】-- 數組與指針

【C/C++語言入門篇】-- 結構體

【C/C++語言入門篇】-- 深刻函數【上篇】

【C/C++語言入門篇】-- 深刻函數【下篇】

【C/C++語言入門篇】-- 位運算【上篇】

【C/C++語言入門篇】-- 位運算【下篇】

【C/C++語言入門篇】-- 剖析浮點數

【C/C++語言入門篇】-- 文件操做【上篇】

【C/C++語言入門篇】-- 文件操做【中篇】

【C/C++語言入門篇】-- 文件操做【下篇】

相關文章
相關標籤/搜索