C語言自學之指標理解
阿新 • • 發佈:2018-11-01
目的: 通過以下學習,希望能理解指標的概念,理解指標和陣列的關係,理解指標的定義,掌握指標的用法。
1. 簡述
用C語言寫的程式碼基本上都用到指標,掌握好指標的概念對學好C有很大幫助。 為了方便理解我們可以把指標稱作某一塊記憶體的名字(指標的值是某塊記憶體的地址),通常計算機的記憶體會被分成許多小塊,而每塊都可以有一個名字,而實際上它每一塊記憶體(一般一位元組為一塊)是有個編號的。為了更為直觀,我們可以把全部的記憶體比作一個大櫃子, 這個櫃子裡面都是抽屜,每一個抽屜都有一個序號(記憶體地址編號),而抽屜名稱(指標)則是我們為某個序號(記憶體編號)起的外號。
說明: (1)上圖中,記憶體地址的編號的綽號除了可以是指標變數外,還可以是其它變數,如int。如果地址的編號的綽號是指標變數,那麼在這個地址裡住著的就必須是另外一個記憶體地址編號,如果是一個int,裡面住著的就是int資料。區別這些就需要我們去理解變數概念。 變數的要素: <1> 變數名 <2>變數名在記憶體的位置 <3>變數的值 <4>變數的值的位置 <5>普通變數和指標變數
<1> 變數名是變數的稱呼,如人名。
<2> 變數名就是記憶體位置的外號,記憶體中其實是不存在這個名稱的,主要是通過編譯器把變數名和一塊記憶體空間的編號關聯起來,如上圖,可以關係0X00000002為變數pi 。 <3>變數的值就是變數名所代表的記憶體空間裡存放的資料,例如0X00000002裡存放著資料"xyz",那麼變數pi的值就等於"xyz"。
<4>普通變數可以直接取值,變數名和變數值之間沒有間隔,例如宣告 int i = 10, 假定i代表的是0X00000002編號, 要取值時,直接拉開0X00000002抽屜就可以取到i的數值10,因為10就是存放在0X00000002這個記憶體空間裡的資料。指標變數名和變數值之間存在著間隔,指標代表的記憶體空間裡的值並不是最終的我們所期待的值,真正的值需要使用指標操作符(*)簡接取值。
<5>為什麼要設計指標這種簡接操作的變數呢?其中涉及到記憶體空間高可用、資料複製效率、程式執行效率等方面的問題,假如我們分別為0X00000002和0X00000001這兩個空間複製0X00000000的資料,0X00000002使用指標方法,只需要把0X00000000這幾個數複製到0X00000002裡面的空間;0X00000001用普通方法,是要把0X00000000裡面的所有資料搬過來,如果0X00000000裡資料非常大,兩種方法的工作量和效率從中可見。 (2)當我們宣告int * pi時, 系統會把一個記憶體編號的外號記作pi, 我們假設是上圖中的0X00000002,然後我們再在0X00000002裡裝入另一個地址,這就完成了int * pi宣告的作用,我們怎樣打0X00000002裝入另外一個地址呢? 很簡單,直接賦地址值就可以了,假定我們知道了0X00000001儲存的是int數值(假定是100),我們就可以將0X00000001賦值給pi,賦值之後,0X00000002裡面的資料是0X00000001,可以理解為0X00000002這號抽屜裡面裝了0X00000001。 我們要對pi取值時,就用 * pi,這時候取出的值不是 0X00000002裡裝的值(0X00000001),而是0X00000002裡面的值所指向的值,*pi == 100, *取值中間是隔了一層的,因此我們說*是間接操作符。
(3)另外,當宣告int * pi時, 是沒有對pi賦值的, 我們繼續假定0X00000002的外號是pi,當int * pi時,並沒有在0X00000002裡面放入任何東西,這種情況下0X00000002裡面可能是空的,也可能存在著未知的東西,因此我們不能隨意給*pi賦值,如果這樣做了就等於給一個沒有確定的地址寫資料,這樣的危害可大可小,小則沒事,大則程式崩潰。例如,0X00000002在沒有賦值的前提下,它的資料可能是0X00000001(當然這有無數可能),而0X00000001裡面裝著程式的重要資料,我們在不期待的情況下給0X00000002賦值,就會修改了0X00000001的資料。
(4)指標的另外一個操作符 & ,這個卻是個翻譯操作符,給定一個變數,"&變數"能將變數翻譯成地址,就如你有綽號時,用"&綽號"可以得出真名。這個&的執行原理,是根據編譯的一個表去對號入座的,每一個符號在記憶體裡都有一席之地,編譯時會對這些做好記錄,當你要使用&,就會對照這個記錄給出關聯的席位(地址)。
2. 實驗 #include <stdio.h>
int main(void) { int i = 10;
int * pi;
printf("i take the place of %p\n", &i);
printf("pi take the place of %p\n", &pi);
printf("i data is %d\n", i);
printf("pi data is %p\n", pi);
pi = &i;
printf("pi take the place of %p\n", &pi);
printf("pi data is %p\n", pi);
printf("pi mean %d\n", *pi);
return 0;
}
編譯後執行結果如下:
i take the place of 0x7ffd94da2c4c i是0x7ffd94da2c4c的外號,代表的是0x7ffd94da2c4c,如人名代表的是人 pi take the place of 0x7ffd94da2c40 pi是0x7ffd94da2c40的外號 i data is 10 儲存在i裡面,0x7ffd94da2c4c這個記憶體地址空間裡的是數字 10 pi data is 0x7ffd94da2d30 沒有賦值前,pi裡面的資料是0x7ffd94da2d30,即0x7ffd94da2c40空間內是0x7ffd94da2d30 pi take the place of 0x7ffd94da2c40 通過pi=&i賦值後,pi所代表的空間編號沒有發生變化,發生變化的是裡面的資料 pi data is 0x7ffd94da2c4c pi裡面裝的是0x7ffd94da2c4c,它是一個裡面裝有int數值10的資料地址 pi mean 10 通過間接運算子*取pi的值,得到的正是我們期待的值。 3. 進階
通過以上的學習我們基本上明白了int * p, 和 &p 是什麼意思了, 下面我們來進一步區分 const char *p, char * const p, 注意:const char 和 char const 其實是一樣的,const都是對char進行修飾。
(1) const char *p 表示 char is const, 根據上面的抽屜比喻, p是一個抽屜名, char則為抽屜裡的抽屜的東西(指標是間接,所以要隔一個抽屜), 而const則限定了抽屜裡只能為不變的東西,何為不變呢? 假定抽屜裝了五個蘋果,如果用const去限定了,則不能增加和減少這數量,更不能把蘋果換成雪梨。雖然char不能改變,但我們卻是可以改變p的值, 假如p裡面裝的是1號抽屜,我們可以改裝2號抽屜, 如: <1>const char * p; char g[] = "GG"; p = g; <2>char m[] = "MM"; <3>p = m 這樣p就由GG變成了MM。 這就是const char *p的理解。
(2)我們很容易理解 char * const p, 意思就是 p 的指向(p裡面裝的值)不能變更了,如p裡面裝著1號抽屜,那不成改裝2號、3號、4號等等抽屜了,而至於1號抽屜裡裝什麼東西,這卻是可以變更的,只要是裝著char型別的東西就可以,例如裝著2個蘋果可以加裝3個、4個、5個······ 但是不能將蘋果改為裝雪梨。
(3) 依此類推,我們可以掌握 const char * const p等宣告的含義。
4. 不得不說的指標與陣列
(1)什麼是陣列呢? 陣列就是記憶體裡的一段固定的連續的區域,固定說的是位置已定,用抽屜去解釋就是編號連在一起的抽屜群,從上而下(相對來說,可從左到右、從下到上)相疊在一塊的N個抽屜,且裡面裝都是同一型別物品,我們稱它們為組。
(2)我們知道指標是某一塊記憶體的別名,而陣列又是記憶體的N個塊的組合,兩者都和記憶體地址有關,這就攆上了關係啦。我們可以把指標指到數組裡,如果指標裡裝著的剛好是陣列的開頭元素的記憶體地址,我們就可以把這個指標和這個陣列對等起來,而實際上我們也是這樣定義,指標變數可以等於陣列的名字, 如char a[5]; char * p; 然後 p = a , 不過,如果a = p反過來賦值卻是不可以的, 因為陣列的位置已經固定,是個常量,而指標是變數,我們不能夠把變數賦值給常量,即是說可以變化的東西不能賦給不變的,比如"麵包=包粉",是不可以的,因為麵包做不了麵粉,但"麵粉=麵包"卻是允許的,這就是常量和變數的區別。我們不要奇怪變數p可以p++, 常量a卻不能a++,因為++是個增量操作符,是對它的左值或者右值進行修改操作的符號,只適用於變數。
(3)陣列的名字是陣列所在的記憶體空間的地址名的外號,因此陣列的第一個元素的地址和它的名字是所代表的地址一樣,如: 由 int i[4] 得出 i = &i[0],我們再宣告一個指標 int * p, 將i賦值p,指標p就可以頂著陣列i的名頭辦事。
(4)我們通過宣告int * p , 然後將int[n]陣列的地址賦給 p,這是因為兩者型別相同。當我們遇到的是二維陣列或者更多維陣列時,指標的宣告就必須要與陣列的型別、長度都相同,(這裡的長度是指資料佔有的記憶體空間),比如我們要將int i[2][4]的地址賦給一個指標,首先我們要理解i[2][4]含義——它是一個有兩個元素的陣列,每個元素都包含了4個int值,即是說這個陣列的長度是4個int。所以我們宣告指向這個陣列的指標的長度必須是4個int, 宣告如下: int (* p) [4], 為什麼要用括號將(*p)括著?是因為[]的優先順序高於*,如果沒有括號(),p就會先和[4]結合,這就變成了宣告的是一個指標陣列,兩者區別是: (*p)[4]只有一個指針變數, 而*p[4]有4個指標變量。 (*p)[4]記憶體只需要保留一個空間來存放包含有4個int值的記憶體地址,就是說只需要保留一個抽屜位,而*p[4]是需要預留4個抽屜的。
(5)一維數線可用線性表示,二維就是一個平面,三維則是立體,至於四維、五維這些多維度的陣列就很難想象和理解了,所以非不必要千萬不要使用超過三維的陣列。
5. 空指標和記憶體洩漏
(1) 空指標是指:"指標的指向還沒有確定"。明確的空指標會直接把指標設為NULL,但現實中會出現很多不明確的宣告。如:char *p ; 編譯後記憶體只會將一個記憶體地址對映為p,而這個p裡面究竟裝著什麼東西,就無法確定,這需要你做下一步的指向工作,如: char e[] = "我思故我在”; char *p ; p = e ; 這樣p就有了明確的指向。在未確定p裡面是什麼前,我們千萬不要往p裡寫東西,如:char * p; 然後直接 *p="XX",這是錯誤的,不過這樣宣告卻是可以的: char * p = "XX"。
(2) 記憶體洩漏是指:"某個區域的記憶體空間被遺忘了"。這個遺忘有如“幾十個中抽屜中的一個藏著一百元,但卻無法找到”般悲哀。當然這100元你可能在以後的某個時間裡找了出來,但在當前你想要用時,卻沒得用,這是種痛苦。記憶體的洩漏也一樣,一段已給分配給你的記憶體卻忘記使用,白白的浪費,它只能在程式退出後被系統回收去。導致記憶體洩漏的原因的前提是自已想管理記憶體,如通過malloc申請得到了一段記憶體空間,為了記憶,你必須給這空間起個名字,比如叫 swap, swap跟你一段時間後,你覺得不夠用了,於是又申請了一段空間,你習慣性把新分到的空間又命名為swap,這就造成前一次申請的空間丟失,因為所有需要寫入或修改的資料都寫入這個新swap中,舊的swap在程式後面的日子被閒置和遺忘。像電話號碼,你把某人的號碼忘記了,又或者某人的號碼被他人用了,導致你再也找不到某人。
(3)記憶體洩漏的危害遠遠不是忘記這麼簡單,它會令到程式崩潰,更為可怕的是你永遠不知道究竟什麼原因導致崩潰,因為記憶體洩漏的排查是一項很艱辛的工作,編譯器是不會直接告訴你這是記憶體洩漏。
6. 指標運算
(1)指標是可以進行加減運算的,和指標進行運算的必須是整數,而且指標加減後所得的結果是和指標指向型別有關聯的,如int * p,p+1的結果是p加上int所佔的位元組,這個位元組數可能是4、8、16,不同體系有不同定義,反正指標的加減等於指標和指標所指型別的位元組和整數的乘積的加減。 (2)指標變數可以進行自增減運算,如int * p ; p++, p--,--p,++p等是可以進行的。 (3)指向同一陣列內元素的指標是可以進行差值運算的,如: int i[20]; int * p1, * p2; p1=&i[1]; p2=&i[5]; p2 - p1是可以運算的。 (4) 具有相同型別的指標值,可以用關係運算符進行比較。
7. 練習
宣告一個數組:int i[4][2] = {{2,4},{6,8},{1,3},{5,7}}; 分析: 1. i 是什麼? 2. i + 2 ? 3. *(i+2) ? 4. *(i+2)+1 ? 5. *(*(i+2)+1) ? 答: 1. i 是二維陣列i[4][2]的第一個大小為2個int元素的地址
2. i+2 是二維陣列i[4][2]的第三個大小為2個int元素的地址
3. *(i+2) 是(i+2)地址裡的值,也是一個地址,它是陣列第三個元素裡的第一個int值的地址,即是&i[2][1]
4. *(i+2) + 1 是數組裡第三個元素的第二個int值的地址,即是&i[2][2]
5. *(*(i+2)+1) 是數組裡第三個元素的每二個int值的值,即是i[2][1] 的值。