C語言的核心--指標
前言
計算機中所有的資料都必須放在記憶體中,不同型別的資料佔用的位元組數不一樣,如 int 佔用 4 個位元組,char 佔用 1 個位元組。為了正確地訪問這些資料,必須為每個位元組都編上號碼,就像門牌號、身份證號一樣,每個位元組的編號是唯一的,根據編號可以準確地找到某個位元組。
將記憶體中位元組的編號稱為地址(Address)或指標(Pointer)。地址從 0 開始依次增加,對於 32 位環境,程式能夠使用的記憶體為 4GB,最小的地址為 0,最大的地址為 0XFFFFFFFF
#include <stdio.h> int main() { int a = 100; char str[20] = "I love U!"; printf("%#X, %#X\n", &a, str); return 0; } 執行結果: 0X28FF3C, 0X28FF10
%#X
表示以十六進位制形式輸出,並附帶字首0X。a 是一個變數,用來存放整數,需要在前面加&來獲得它的地址;str 本身就表示字串的首地址,不需要加&。
C語言中有一個控制符%p,專門用來以十六進位制形式輸出地址,不過 %p 的輸出格式並不統一,有的編譯器帶0x字首,有的不帶,所以此處我們並沒有採用。
一切都是地址
資料和程式碼都以二進位制的形式儲存在記憶體中,計算機無法從格式上區分某塊記憶體到底儲存的是資料還是程式碼。當程式被載入到記憶體後,作業系統會給不同的記憶體塊指定不同的許可權,擁有讀取和執行許可權的記憶體塊就是程式碼,而擁有讀取和寫入許可權(也可能只有讀取許可權)的記憶體塊就是資料。CPU 只能通過地址來取得記憶體中的程式碼和資料,程式在執行過程中會告知 CPU 要執行的程式碼以及要讀寫的資料的地址。如果程式不小心出錯,或者開發者有意為之,在 CPU 要寫入資料時給它一個程式碼區域的地址,就會發生記憶體訪問錯誤。CPU 訪問記憶體時需要的是地址,而不是變數名和函式名!變數名和函式名只是地址的一種助記符,當原始檔被編譯和連結成可執行程式後,它們都會被替換成地址。
什麼是指標
資料在記憶體中的地址也稱為指標,如果一個變數儲存了一份資料的指標,我們就稱它為指標變數。
舉個栗子:
現在假設有一個 char 型別的變數 c儲存了字元 'K'(ASCII碼為十進位制數 75),並佔用了地址為 0X11A 的記憶體(地址通常用十六進位制表示)。
有一個指標變數 p,它的值為 0X11A,正好等於變數 c 的地址,這種情況我們就稱 p 指向了 c,或者說 p 是指向變數 c 的指標。
指標變數的定義
定義指標變數與定義普通變數非常類似,不過要在變數名前面加星號*,格式為:
datatype *name; 或者 datatype *name = value;
*表示這是一個指標變數,datatype表示該指標變數所指向的資料的型別 。
例如:
int *p1;//p1 是一個指向 int 型別資料的指標變數,至於 p1 究竟指向哪一份資料,應該由賦予它的值決定。
int a = 521;
int *pa = &a;
//在定義指標變數 pa 的同時對它進行初始化,並將變數 a 的地址賦予它,此時 pa 就指向了 a。
//值得注意的是,pa 需要的一個地址,a 前面必須要加取地址符&,否則是不對的。
注意點:
指標變數也可以被多次寫入
Eg:
//定義普通變數
float a = 99.5, b = 10.6;
//定義指標變數
char *p2 = &c;
//修改指標變數的值
p1 = &b;
*
是一個特殊符號,表明一個變數是指標變數,定義 p1、p2 時必須帶。而給 p1、p2 賦值時,因為已經知道了它是一個指標變數,就沒必要多此一舉再帶上,後邊可以像使用普通變數一樣來使用指標變數。也就是說,定義指標變數時必須帶,給指標變數賦值時不能帶。
假設變數 a、b、c、d 的地址分別為 0X1000、0X1004、0X2000、0X2004,如圖所示:
指標變數也可以連續定義
int *a, *b, *c; //a、b、c 的型別都是 int*
注意每個變數前面都要帶*.
通過指標變數取得資料
指標變數儲存了資料的地址,通過指標變數能夠獲得該地址上的資料:
格式為:
*pointer;
這裡的*稱為指標運算子,作用是取得某個地址上的資料.
eg:
#include <stdio.h>
int main()
{
char a = 'Q';
char *p = &a;
printf("%c, %c\n", a, *p); //兩種方式都可以輸出a的值
return 0;
}
執行結果:
Q,Q
CPU 讀寫資料必須要知道資料在記憶體中的地址,普通變數和指標變數都是地址的助記符,雖然通過 *p 和 a 獲取到的資料一樣,但它們的執行過程稍有不同:a 只需要一次運算就能夠取得資料,而 *p 要經過兩次運算,多了一層“間接”。
假設變數 a、p 的地址分別為 0X1000、0XF0A0
程式被編譯和連結後,a、p 被替換成相應的地址。使用 *p 的話,要先通過地址 0XF0A0 取得變數 p 本身的值,這個值是變數 a 的地址,然後再通過這個值取得變數 a 的資料,前後共有兩次運算;而使用 a 的話,可以通過地址 0X1000 直接取得它的資料,只需要一步運算。
使用指標是間接獲取資料,使用變數名是直接獲取資料,前者比後者的代價要高
這裡應該注意:
*
在不同的場景下有不同的作用:可以用在指標變數的定義中,表明這是一個指標變數,以和普通變數區分開;使用指標變數時在前面加表示獲取指標指向的資料,或者說表示的是指標指向的資料本身。
定義指標變數時的和使用指標變數時的意義完全不同
關於 * 和 &
假設有一個 char 型別的變數 a,pa 是指向它的指標,那麼&a和&pa分別是什麼意思呢?
&a可以理解為(&a),&a表示取變數 a 的地址(等價於 pa),*(&a)表示取這個地址上的資料(等價於 pa),繞來繞去,又回到了原點,&a仍然等價於 a。
&pa可以理解為&(pa),pa表示取得 pa 指向的資料(等價於 a),&(pa)表示資料的地址(等價於 &a),所以&*pa等價於 pa。
對*的總結
星號*主要有三種用途:
- 表示乘法,例如a = 3,b = 5,c = a * b;
- 表示定義一個指標變數,以和普通變數區分開
- 表示獲取指標指向的資料,是一種間接操作
指標運算
指標變數儲存的是地址,地址本質上也是一個整數,所以指標變數可以進行部分運算,包括加法,減法和比較運算,從使用來說,乘法和除法沒有意義,我們不再贅述。
#include <stdio.h>
int main()
{
int a , *pa = &a, *paa = &a;
double b , *pb = &b;
char c , *pc = &c;
//初始值,%x用來進行十六進位制輸出
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//加法運算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//減法運算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//比較運算
if(pa == paa)
{
printf("%d\n", *paa);
}
else
{
printf("%d\n", *pa);
}
return 0;
}
執行結果:
&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784
從結果可看出:pa、pb、pc 每次加 1,它們的地址分別增加 4、8、1,正好是 int、double、char 型別的長度;
減 2 時,地址分別減少 8、16、2,正好是 int、double、char 型別長度的 2 倍
原因其實很簡單,陣列中的所有元素在記憶體中是連續排列的,如果一個指標指向了陣列中的某個元素,那麼加 1 就表示指向下一個元素,減 1 就表示指向上一個元素,這樣指標的加減運算就具有了現實的意義。如果不是跳過相應型別的對應的位元組,那麼就會造成資料的混亂, 在使用過程中沒有任何現實意義。
對於其他運算,當對指標變數進行比較運算時,比較的是指標變數本身的值,也就是資料的地址。如果地址相等,那麼兩個指標就指向同一份資料,否則就指向不同的資料。不能對指標變數進行乘法、除法、取餘等其他運算,除了會發生語法錯誤,也沒有實際的含義。
陣列指標
先回憶一下陣列的定義:陣列(Array)是一系列具有相同型別的資料的集合,每一份資料叫做一個數組元素(Element)。陣列中的所有元素在記憶體中是連續排列的,整個陣列佔用的是一塊記憶體。我們在定義陣列時,要給出陣列名和陣列長度,陣列名可以認為是一個指標,它指向陣列的第 0 個元素。在C語言中,我們將第 0 個元素的地址稱為陣列的首地址。
陣列名的本意是表示整個陣列,也就是表示多份資料的集合,但在使用過程中經常會轉換為指向陣列第 0 個元素的指標,所以上面使用了“認為”一詞,表示陣列名和陣列首地址並不總是等價。
這裡我們給出一個例子,供大家體會用指標遍歷整個陣列的一種方法:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5 };
int len = sizeof(arr) / sizeof(int); //求陣列長度
//這個技巧在以後會經常使用到
int i;
for(i=0; i<len; i++)
{
printf("%d ", *(arr+i) ); //*(arr+i)等價於arr[i]
}
printf("\n");
return 0;
}
執行結果:
1 2 3 4 5
//程式碼分析:
`sizeof(arr) / sizeof(int)`用來求陣列的長度,sizeof(arr) 會獲得整個陣列所佔用的位元組數,
sizeof(int) 會獲得一個數組元素所佔用的位元組數,它們相除的結果就是陣列包含的元素個數,也即陣列長度
在獲取數值時,我們使用了*(arr+i)表示式,arr 是陣列名,指向陣列的第 0 個元素,表示陣列首地址,
arr+i 指向陣列的第 i 個元素,*(arr+i) 表示取第 i 個元素的資料,它等價於 arr[i]。
//arr 是int*型別的指標,每次加 1 時它自身的值會增加 sizeof(int),加 i 時自身的值會增加 sizeof(int) * i
我們還可以定義一個指標:
int arr[] = { 1,2,3,4,5 };
int *p = arr;
如果一個指標指向了陣列,我們就稱它為陣列指標(Array Pointer)。
在這個定義中,arr 本身就是一個指標,可以直接賦值給指標變數 p。arr 是陣列第 0 個元素的地址,所以int *p = arr;也可以寫作int *p = &arr[0];。即arr、p、&arr[0] 這三種寫法都是等價的,它們都指向陣列第 0 個元素,或者說指向陣列的開頭。陣列指標指向的是陣列中的一個具體元素,而不是整個陣列,所以陣列指標的型別和陣列元素的型別有關,上面的例子中,p 指向的陣列元素是 int 型別,所以 p 的型別必須也是int *
下面我們嘗試用陣列指標遍歷整個陣列:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for(i=0; i<len; i++)
{
printf("%d ", *(p+i) );
}
printf("\n");
return 0;
}
執行結果:
1 2 3 4 5
程式分析:
陣列在記憶體中只是陣列元素的簡單排列,沒有開始和結束標誌,在求陣列的長度時不能使用sizeof(p) / sizeof(int),
因為 p 只是一個指向 int 型別的指標,編譯器並不知道它指向的到底是一個整數還是一系列整數(陣列),
所以 sizeof(p) 求得的是 p 這個指標變數本身所佔用的位元組數,而不是整個陣列佔用的位元組數。
在瞭解了陣列指標之後,我們在使用陣列時,就有兩種方式訪問陣列元素了:
- 使用下標
也就是採用 arr[i] 的形式訪問陣列元素。如果 p 是指向陣列 arr 的指標,那麼也可以使用 p[i] 來訪問陣列元素,它等價於 arr[i]。 - 使用指標
也就是使用 *(p+i) 的形式訪問陣列元素。另外陣列名本身也是指標,也可以使用 *(arr+i) 來訪問陣列元素,它等價於 *(p+i)。
在這裡,我們給出一個挺有意思的問題:
假設 p 是指向陣列 arr 中第 n 個元素的指標,那麼 p++、++p、(*p)++ 分別是什麼意思呢?
解答:
*p++ 等價於 *(p++),表示先取得第 n 個元素的值,再將 p 指向下一個元素。
*++p 等價於 *(++p),會先進行 ++p 運算,使得 p 的值增加,指向下一個元素,整體上相當於 *(p+1),所以會獲得第 n+1 個數組元素的值。
(*p)++ 就非常簡單了,會先取得第 n 個元素的值,再對該元素的值加 1。假設 p 指向第 0 個元素,並且第 0 個元素的值為 99,執行完該語句後,第 0 個元素的值就會變為 100。
字串指標
在C語言中,沒有專門儲存字串的資料型別(c++ STL中有string型別),於是我們通常使用字元陣列完成字串的相應操作:
例如:
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "I Love you!";
int len = strlen(str), i;
//直接輸出字串
printf("%s\n", str);
//每次輸出一個字元
for(i=0; i<len; i++)
{
printf("%c", str[i]);
}
printf("\n");
return 0;
}
執行結果:
I Love you!
I Love you!
由於字串使用字元陣列完成的操作,所以字串不嚴謹的說也是一個數組,那麼指標就可以進行相應的操作:
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "I Love you!";
char *pstr = str; //定義一個數組指標
int len = strlen(str), i;
//使用*(pstr+i)
for(i=0; i<len; i++)
{
printf("%c", *(pstr+i));
}
printf("\n");
//使用pstr[i]
for(i=0; i<len; i++)
{
printf("%c", pstr[i]);
}
printf("\n");
//使用*(str+i)
for(i=0; i<len; i++)
{
printf("%c", *(str+i));
}
printf("\n");
return 0;
}
執行結果:
I Love you!
I Love you!
I Love you!
那麼第一種指標的使用就是和上面的陣列指標的使用是一樣的,那麼我們在這裡介紹的是第二種:直接使用一個指標指向字串。
定義如下:
char *str = "I Love you!";
字串中的所有字元在記憶體中是連續排列的,str 指向的是字串的第 0 個字元;
通常將第 0 個字元的地址稱為字串的首地址。
字串中每個字元的型別都是char,所以 str 的型別也必須是char *
而這種輸出方法和陣列指標的輸出如出一轍,但並不代表這兩種方式完全一樣。
指標表示的字串和陣列表示的字串最根本的區別是在記憶體中的儲存區域不一樣,
字元陣列儲存在全域性資料區或棧區,第二種形式的字串儲存在常量區。
全域性資料區和棧區的字串(也包括其他資料)有讀取和寫入的許可權,而常量區的字串(也包括其他資料)只有讀取許可權,沒有寫入許可權。
記憶體許可權的不同導致的一個結果,字元陣列在定義後可以讀取和修改每個字元,而對於第二種形式的字串,一旦被定義後就只能讀取不能修改,任何對它的賦值都是錯誤的。這也是為什麼第二種被稱之為字串常量的原因。