1. 程式人生 > >給指標malloc分配空間後就等於陣列嗎?

給指標malloc分配空間後就等於陣列嗎?

首先回答這個的問題:嚴格的說不等於陣列,但是可以認為它是個陣列一樣的使用而不產生任何問題。
不過既然這樣,那它應該算是個陣列吧。
所以,一般我們都用“動態陣列”這種名字來稱呼這種東西。

要講清楚這個東西,涉及到malloc函式,指標型別和“[ ]”下標運算。


======分割線[0]======
malloc是C的標準庫函式之一,用來分配動態記憶體。

一般來說,由C/C++編譯的程式會在執行的時候在記憶體中佔用一些空間,它們分為以下幾個部分:
1.二進位制程式碼區 不必過多解釋了,就是放二進位制程式碼的地方。
2.常量區 存放文字字串和常量
3.靜態儲存區 存放靜態和全域性變數
4.堆空間 動態記憶體區,程式設計師可控制分配和釋放的區域。
5.棧空間 由編譯器分配記憶體用於儲存函式引數和普通變數。

malloc能操作的是程式中的堆空間,而普通的陣列則是存放在棧空間裡面的。
由於作業系統對這兩部分的記憶體管理模式差別很大,所以我們一般認為是不同的。

堆空間是系統記憶體中的可用區域,和普遍意義上的“堆(Heap)”不同,基本上可以看作是由空閒記憶體組成的大連結串列。
嘛,作業系統怎麼處理這東西不管了,反正你就可以認為堆空間是可用記憶體裡的一片連續區域。
malloc函式的作用就是從這一片記憶體中劃出一塊空間來。你可以認為是malloc從記憶體中找到了一片可以安全存放資料的可用空間,這樣你的資料就可以放在這片空間裡面。這片空間的大小是你自己指定的。通過malloc(位元組數)這樣簡單的方法。

為了找到這片空間,malloc函式會告訴你這片空間開頭的地址,你可以把它賦值給一個變數存放起來。
這樣我們就知道申請到的這片記憶體的首地址(malloc返回)和大小(程式設計師指定)了。


======分割線[1]======
這部分先放著,我們來看指標型別。
C語言的指標也有型別,但是指標總是記憶體地址,是一個(32位/64位)二進位制整數,長度也好大小也好都是確定的,理應一種型別就夠了。那麼,指標型別的作用是什麼呢?其實指標型別就是用於判斷指標所指向的資料的型別。

不得不說這是一個非常天才的設計。
指標裡存放著的是一個地址,它能找到一個記憶體單元(複雜的東西不說了,作業系統都給你做了,你就認為是某一個位元組就好。這個括號內部的東西寫給某些較真的人看,實際上並不存在一種叫做記憶體單元的東西。),但是資料有長有短,資料們有些存在1個記憶體單元裡面,有些存在多個記憶體單元裡面。
指標是為了指向一個數據,那麼,用什麼方法可以知道這個指標想要的,到底是幾個記憶體單元裡的資料呢?

C語言裡用了一種十分巧妙的設計——指標型別。一個指標指向一個位元組地址,這個指標的型別所代表的資料結構是8個位元組,那麼我們就把這8個位元組裡面的東西都讀出來,作為這個指標所指向的資料的值。

舉個栗子:比如說從地址是1000開始的記憶體是以下的一片樣子:
00000001 00000010 00000011 00000100 00000101 00000110 00000111 00001000
00001001 00001010 00001011 00001100 00001101 00001110 00001111 00010000
然後我有個指標a,它的值是1000。
如果這個指標是int *a。當我用*a去訪問資料的時候,就會返回【00000001 00000010 00000011 00000100】
這些資料。
但是如果這個指標是double *a。當我用*a去訪問資料,返回的就是【00000001 00000010 00000011 00000100 00000101 00000110 00000111 00001000】這些資料了。

不過這個指標值可是沒有變化的,變化的只是指標型別而已。

======分割線[2]======
再回到原來那個問題,我們現在用malloc取得了一片空間,但是要讓編譯器知道其中每個資料佔多少空間,就是由指標型別來確定了。
這就是為什麼malloc函式在賦值給指標之前要有一個強制型別轉換的原因。否則void *型別一般應該是讀不出資料的。
(此括號再次寫給較真的人們,直接使用void *指標是未定義行為,未定義行為是編譯器說了算的,它不想給你返回值就不給你返回值了。不過我們現在的編譯器都比較好心,一般是會給你返回1個位元組的值的,用起來大概就和char *一個感覺。)
比如說a=(int *)malloc(10240);
這一段程式碼就取得了10240個位元組(10KB)的可用空間,然後把首地址告訴了變數a。然後我打算存放的資料是整型的,一次要求程式在這段記憶體裡面讀4個位元組返回。所以我使用了(int *)來確定指標型別。

這樣,當我們使用*a時,就可以訪問到從a指向的地址開始的4個位元組裡面的資料了。


======分割線[3]======
可是我們申請了10240個位元組呢。。。能存2560個整型變數呢。只能訪問前4個位元組有什麼用?難道要每4個位元組申請一次?
怎麼訪問後面的記憶體空間呢?
我們可以移動指標,比如把指標往後移4個位元組。這樣就能訪問到這片區域裡面的第2個整型變量了。
(注意,如果是int *型別的指標a,把a往後移4個位元組的操作是a=a+1,千萬不要搞成a=a+4了。為什麼這麼做原因後面再講。)/*補充[0]*/
可是還是很麻煩,如果我要一次一次的遍歷這片區域,或者同時訪問裡面的第12個和第450個變數。那麼程式裡就必然存在2個或2個以上的指標。
為了簡化訪問方法,C語言使用了一種簡單的對指標運算——[ ]下標運算。

[ ]運算子是C語言幾乎最高優先順序的運算子。[ ]運算子需要兩個運算元,一個指標型別,一個整數。/*補充[1]*/
標準的寫法是這樣的:a[int]。這樣編譯器會返回 *(a+int) 的值。
這樣做的話相當於一個十分好用的臨時指標的移動。
如果我要訪問第12個變數只需要寫a[11]就好了。編譯器會理解這個運算的規則,自動的把a指標進行一次以下的操作:
int *temp;temp=a+11;return *temp;
嗯,大概就是這個樣子。


======分割線[4]======
該回到正題了。因為C語言為我們提供了這樣的方法,使得我們申請到的一片記憶體連續區域,可以使用這樣的方法,像陣列一樣的訪問到。
不過陣列明顯更加簡單。int a[2560]同樣是申請一片10KB的空間,這部分空間存放在棧空間裡面。記憶體地址也是完全連續的。
值得注意的是,陣列名a其實被宣告為常量指標const int *,它同樣儲存的是陣列的首地址。
(本括號寫給較真的人看,C/C++自動把陣列型別隱式宣告為常量指標,這個動作其實更類似於隱式轉換,而不是直接宣告那個指標。)
然後這麼說來[ ]。操作符在普通陣列上和用malloc生成的動態陣列上的操作是完全一樣的,都是類似於*(a+i)的操作。

所以從這層意義上來講,用malloc分配的空間本質上和陣列沒什麼區別。它們主要的區別還是存放的記憶體區域在作業系統對記憶體管理上的區別。
不過這層區別也不小,所以一般不把malloc分配的空間等同為陣列,而是用“動態陣列”來區別的對待它。
最重要的區別也許就是使用完了以後記著用free釋放掉。


======分割線[5]=完,下面是補充內容=====
補充[0]:作業系統給你分配的記憶體,一般只有棧空間是連續的。比如你申請一個10KB的堆空間區域,其實很少能申請到全連續的一段記憶體。一般都是中間會有斷開的方式。
作業系統是用類似於連結串列的方式來管理這些分片的記憶體空間的。
所以說,雖然指標本質就是個整數,但是指標的運算不是簡單的改變這個整數,而是指向下一儲存區這樣的意思。
因為如果是讓你簡單的改變這個整數,很可能這個指標指向的就是記憶體中其他程式的區域了。甚至是系統重要的程式碼區域。這是絕對不允許的,所以編譯器才會採用這樣的定義。即給int *a定義的指標a進行a++這種運算的過程實際上相當於:1.返回a的當前值 2.找到a當前的記憶體區域 3.在連結串列中查詢下4個位元組的存放區域,並把首地址賦值給a。

補充[1]:事實上ANSI C並沒有定義兩個運算元的順序。指標[整數]只是一種常用寫法。寫成整數[指標]也未嘗不可。
定義陣列int a[20]之後,使用5[a]一樣可以訪問到這個數組裡第6個整型變數的值。