C語言基礎——指標
一、指標的定義
在程式中定義了一個變數,在對程式進行編譯時,系統就會為這個變數分配記憶體單元。編譯系統根據程式中定義的變數型別分配一定長度的空間。記憶體的基本單元是位元組,一位元組有8位。每位元組都有一個編號,這個編號就是“地址”。
實際上,變數名經過編譯之後再系統內部就“消失”了,只留下地址和型別,地址是該變數空間的起始地址,型別是這個變數的型別。這個型別指示編譯器怎樣操作特定地址上的記憶體區域:該區域包含多少連續的位元組,資料儲存的格式,以及可以實施哪些基本操作。這個型別成為地址的基型別。
直接引用:通過儲存空間的名稱引用資料的方式,而直接引用方式的內部實現機制是間接引用。以一組變數為例:
int n,m; //定義兩個整型變數,n和m是變數名
double x; //定義一個雙浮點型變數,x是變數名。
n=5,m=5; //直接引用,給n賦值5,將n中的值賦給m
x=3.1415; //直接引用,給x賦值3.1415
間接引用:通過地址來訪問資料空間。地址的基型別就是通過該地址去間接引用的資料型別。如下表所示:
變數名 |
變數值 |
地址 |
型別 |
X |
3.1415 |
0x12ff70 |
Double |
m |
5 |
0x12ff78 |
int |
N |
5 |
0x12ff7c |
int |
地址0x12ff78的基型別為整型,通過這個地址去間接引用時,系統將連續4個位元組0x12ff78~0x12ff7b當作整型變數空間;地址0x12ff70的基型別為雙浮點型,通過這個地址,編譯器將0x12ff70~0x12ff77的8個位元組視為雙浮點型空間。
同一個地址,由於地址的基型別不通,編譯器對該地址上的空間的解釋也不同,同理,同樣的空間,其地址的基型別不同,編譯器會將其看作不通型別的資料空間。在C語言中,資料要有型別,型別是資料的“身份”。型別決定了資料儲存空間的大小、儲存格式和對資料可以試試的基本操作。從儲存空間的大小和儲存格式來說,地址相當於無符號整型數,即按無符號整型格式儲存。但地址的基本操作與無符號整型數不通,它域間接引用的資料有關,因此地址需要新型別——指標。
一個變數的地址就稱為該變數的指標。
記憶體中一個單元指的是一位元組,一位元組有 8 位。每根地址匯流排都有兩種狀態:0 和 1。兩根地址匯流排就有 4 種組合,能控制4個記憶體單元;三根地址匯流排就有 8 種組合,能控制 8 個記憶體單元;n 根地址匯流排就有 2n 種組合,能控制 2n 個記憶體單元。那麼 CPU 總共是通過幾根地址匯流排對記憶體進行處理的?一般的計算機是 32 位的,即 32 根地址匯流排,那麼就能夠控制 232 個記憶體單元,即 232 位元組。232B=4GB,所以 32 位系統的計算機只能控制 4GB 的記憶體。
指標型別是一個複合型別,指標型別的名稱是一個複合名稱。它由指標型的基型別名稱再加“*”組成。例如,整型指標型別用“int*”表示,雙浮點型指標型別用“double*”表示。“*”表示指標型別,“int”表示指標型的基型別。
如:int *i;//“*” 表示該變數的型別為指標型別。指標變數名為 i 和 j,而不是 *i 和 *j。
“int*i;”表示定義了一個指標變數 i,它可以指向 int 型變數的地址。但此時並沒有給它初始化,即此時這個指標變數並未指向任何一個變數。此時的“*”只表示該變數是一個指標變數,至於具體指向哪一個變數要在程式中指定。這個就跟定義了“int j;”但並未給它賦初值一樣。
指標變數也是變數,是變數就有地址,所以指標變數本身也是有地址的。只要定義了一個變數,程式在執行時系統就會為它分配記憶體空間。但指標變數又是存放地址的變數,所以這裡有兩個地址大家一定要弄清楚:一個是系統為指標變數分配的地址,即指標變數本身的地址;另一個是指標變數裡面存放的另一個變數的地址。這兩個地址一個是“指標變數的地址”,另一個是“指標變數的內容”。
地址也是可以進行運算的,我們後面會學到指標的運算和移動。比如“使指標向後移 1 個位置”或“使指標加 1”,這個 1 與指標變數的基型別是直接相關的。指標變數的基型別佔幾字節,這個 1 代表的就是幾。比如指標變數指向一個 int 型變數,那麼“使指標移動 1 個位置”就意味著移動 4 位元組,“使指標加 1”就意味著使地址加 4。所以必須指定指標變數所指向的變數的型別,即指標變數的基型別。某種基型別的指標變數只能存放該種基型別變數的地址。
兩個指標變數相減的結果是一個常量,而不是指標型變數。如兩個“int*”型的指標變數相減,結果是 int 型常量。此時要是把相減的結果賦給“int*”型就會報錯。而且兩個指標變數相減的結果是這兩個地址之間元素的個數,而不是地址的個數。
比如說,兩個“int*”型的指標變數相減,第一個指標變數裡面存放的地址是 1245036,第二個指標變數裡面存放的地址是 1245032,那麼這兩個地址相減的結果是 1,而不是 4。因為 int 型變數佔 4 位元組,所以一個 int 元素就佔 4 位元組,兩個地址之間相差 4 個地址,正好是一個 int 元素,所以結果就是 1。
二、指標的初始化
要初始化一個指標變數,使指標變數指向另一個變數,則可以用賦值語句使一個指標變數得到另一個變數的地址,從而是它指向該變數。如:
int i,*j;
j=&i;
這樣就將變數 i 的地址放到了指標變數 j 中,通過 i 的地址,j 就能找到 i 中的資料,所以 j 就“指向”了變數 i。其中 & 是“取地址運算子”,與 scanf 中的 & 是一樣的概念;* 為“指標運算子”,功能是取其內部所存變數地址所指向變數中的內容。因為 j 是定義成指標型變數,所以 j 中只能存放變數的地址,所以變數i前一定要加 &。需要注意的是,指標變數中只能存放地址,不要將一個整數或任何其他非地址型別的資料賦給一個指標變數。
注意, j 不是 i,i 也不是 j。修改j的值不會影響i的值,修改 i 的值也不會影響 j 的值。j 是變數 i 的地址,而 i 是變數 i 裡面的資料。一個是“記憶體單元的地址”,另一個是“記憶體單元的內容”。要區分兩者的區別。
定義指標變數時的“*j”和程式中用到的“*j”含義不同。定義指標變數時的“*j”只是一個宣告,此時的“*”僅表示該變數是一個指標變數,並沒有其他含義。而且此時該指標變數並未指向任何一個變數,至於具體指向哪個變數要在程式中指定,即給指標變數初始化。而當指定 j 指向變數 i 之後,*j 就完全等同於 i 了,可以相互替換。
下面以一個例子來說明:
# include <stdio.h>
int main(void)
{
int i = 3, *j; //*j表示定義了一個指標變數j
j = &i;
printf("*j = %d\n", *j); //此時*j完全等同於i
printf("j = %d\n", j); //j裡面儲存的是變數i的地址
return 0;
}
輸出結果是:*j = 3 j = 1245052
上面例子是先定義指標,再進行初始化,因此是j=&i,如果定義指標變數時對它進行初始化,即定義時初始化,則應該是int *j=&i;通過這個對比我們可以更鮮明地看出定義指標變數時的“*j”和程式中用到的“*j”含義的不同。
指標變數和指標變數之間也可以相互賦值,如下面例子所示:
# include <stdio.h>
int main(void)
{
int *i, *j;
int k = 3;
i = &k;
j = i; //直接指標變數名之間進行賦值
printf("*j = %d\n", *j); //此時*j完全等同於k
printf("j = %d\n", j); // j裡面儲存的是變數k的地址
return 0;
}
輸出結果是:
*j = 3
j = 1245044
可見,可以直接將一個指標變數賦給另一個指標變數,只要將指標變數名賦給另一個指標變數名即可。但是需要注意的是:這兩個指標變數的基型別一定要相同;在賦值之前,賦值運算子“=”右邊的指標變數必須是已經初始化過的。也就是說,切忌將一個沒有初始化的指標變數賦給另一個指標變數。這是非常嚴重的語法錯誤。
同樣,也可以在定義指標變數時就給它賦初值:
# include <stdio.h>
int main(void)
{
int k = 3;
int *i = &k;
int *j = i;
printf("*j = %d\n", *j); //此時*j完全等同於k
printf("j = %d\n", j); //j裡面儲存的是變數k的地址
return 0;
}
輸出結果是:
*j = 3
j = 1245048
注意,“int*j=i;”千萬不要寫成“int*j=*i;”。因為此時 *i 不是定義指標變數 i,而是完全等同於變數 k。所以 int 型變數不能賦給 int* 型的變數。
三、指標的基本操作——間接引用
通過指標字面值常量去除基型別空間的地址(這是直接引用),然後加間接引用運算子“*”,得到與基型別空間名稱等價的表示式,成為間接引用表示式。如上面提過的表為例子:
*(double*)0x12ff70=x;*(int*)0x12ff78=m;*(int*)0x12ff7c=n
通過這些間接引用表示式,可以訪問基型別空間,這稱為間接引用。下面為測試例子:
#include <stdio.h>
{
int n,m;
double x;
print("%x,%x,%x\n",&x,&m,&n);
return 0;
}
假設執行輸出結果仍為0x12ff70,0x12ff78,0x12ff7c,通過一下程式碼,可以通過指標間接修改變數n,m,x的值。
*(double*)0x12ff70=3.1415; //相當於x=3.1415
*(int*)0x12ff78=*(int*)0x12ff7c; //相當於m=n
*(int*)0x12ff7c=5; //相當於n=5
print("n=%d,m=%d,x=%f\n",n,m,x);
執行結果則為n=5,m=5,x=3.1415;
四、指標常見錯誤
1) 引用未初始化的指標變數
試圖引用未初始化的指標變數是初學者最容易犯的錯誤。未初始化的指標變數就是“野”指標,它指向的是無效的地址。
那麼如果指標變數未初始化,編譯器的設計人員是如何處理這個問題的呢?肯定不可能讓它亂指。以VC++6.0這個編譯器為例,如果指標變數未初始化,那麼編譯器會讓它指向一個固定的、不用的地址。而如果在 VS 2008 這個編譯器中,程式雖然能編譯通過,但是在執行的時候直接出錯,它並不會像 VC++6.0 那樣還能輸出所指向的記憶體單元的地址。
2) 往一個存放NULL地址的指標變數裡面寫入資料
# include <stdio.h>
int main(void)
{
int i = 3;
int *j = NULL;
*j = i;
return 0;
}
之前是沒有給指標變數j初始化,現在初始化了,但是將它初始化為指向 NULL。NULL 也是一個指標變數。NULL 指向的是記憶體中地址為 0 的記憶體空間。以 32 位作業系統為例,記憶體單元地址的範圍為 0x00000000~0xffff ffff。其中 0x00000000 就是 NULL 所指向的記憶體單元的地址。但是在作業系統中,該記憶體單元是不可用的。凡是試圖往該記憶體單元中寫入資料的操作都會被視為非法操作,從而導致程式錯誤。同樣,這種錯誤在編譯的時候也不會報錯,只有在執行的時候才會出錯。這種錯誤也屬於“段錯誤”。
然而雖然這麼寫是錯誤的,但是將一個指標變數初始化為指向 NULL,這在實際程式設計中是經常使用的。就跟前面講普通變數在定義時給它初始化為 0 一樣,指標變數如果在定義時不知道指向哪裡就將其初始化為指向 NULL。只是此時要注意的是,在該指標變數指向有效地址之前不要往該地址中寫入資料。也就是說,該指標變數還要二次賦值。
建議大家將指標變初始化為 NULL,就同前面將普通變數在定義時初始化為 0 一樣。這是很好的一種程式設計習慣。