栈顶指针到底指向哪_被称为“程序员试金石”的指针真的没有那么难!不信的话你来看看
很多朋友放棄C語言都是因為指針,說什么指針的*號很討厭啦、分不清址與值啦,當然了,最煩的還是鏈表結點,本來鏈表操作就讓人煩了,再加上指針這個東西真是讓初學的朋友苦不堪言,最后放棄了C語言轉投其他語言的懷抱,但是只要理解指針的本質,那么掌握指針只是時間問題,這篇文章就跟大家一起來看看C語言的指針到底是何方神圣。文末放了一張c/c++入門學習大綱,有需要的朋友可以看一下。
有什么不理解的地方歡迎進群973961276來跟大伙一下交流,或者直接到課堂上面對面請教c/c++ 項目實戰,閑話少說,我們開啟正文。
一、指針到底是什么?
計算機中所有的數據都必須放在內存中,不同類型的數據占用的字節數不一樣,例如 int 占用 4 個字節,char 占用 1 個字節。為了正確地訪問這些數據,必須為每個字節都編上號碼,就像門牌號、身份證號一樣,每個字節的編號是唯一的,根據編號可以準確地找到某個字節。
下圖是 4G 內存中每個字節的編號(以十六進制表示):
?
我們將內存中字節的編號稱為地址(Address)或指針(Pointer)。地址從 0 開始依次增加,對于 32 位環境,程序能夠使用的內存為 4GB,最小的地址為 0,最大的地址為 0XFFFFFFFF。
下面的代碼演示了如何輸出一個地址:
#include <stdio.h> int main(){ int a = 100; char str[20] = "c.biancheng.net"; printf("%#X, %#Xn", &a, str); return 0; }
運行結果:
0X28FF3C, 0X28FF10
%#X表示以十六進制形式輸出,并附帶前綴0X。a 是一個變量,用來存放整數,需要在前面加&來獲得它的地址;str 本身就表示字符串的首地址,不需要加&。
C語言中有一個控制符%p,專門用來以十六進制形式輸出地址,不過 %p 的輸出格式并不統一,有的編譯器帶0x前綴,有的不帶,所以此處我們并沒有采用。一切都是地址
C語言用變量來存儲數據,用函數來定義一段可以重復使用的代碼,它們最終都要放到內存中才能供 CPU 使用。
數據和代碼都以二進制的形式存儲在內存中,計算機無法從格式上區分某塊內存到底存儲的是數據還是代碼。當程序被加載到內存后,操作系統會給不同的內存塊指定不同的權限,擁有讀取和執行權限的內存塊就是代碼,而擁有讀取和寫入權限(也可能只有讀取權限)的內存塊就是數據。
CPU 只能通過地址來取得內存中的代碼和數據,程序在執行過程中會告知 CPU 要執行的代碼以及要讀寫的數據的地址。如果程序不小心出錯,或者開發者有意為之,在 CPU 要寫入數據時給它一個代碼區域的地址,就會發生內存訪問錯誤。這種內存訪問錯誤會被硬件和操作系統攔截,強制程序崩潰,程序員沒有挽救的機會。
CPU 訪問內存時需要的是地址,而不是變量名和函數名!變量名和函數名只是地址的一種助記符,當源文件被編譯和鏈接成可執行程序后,它們都會被替換成地址。編譯和鏈接過程的一項重要任務就是找到這些名稱所對應的地址。
假設變量 a、b、c 在內存中的地址分別是 0X1000、0X2000、0X3000,那么加法運算c = a + b;將會被轉換成類似下面的形式:
0X3000 = (0X1000) + (0X2000);
( )表示取值操作,整個表達式的意思是,取出地址 0X1000 和 0X2000 上的值,將它們相加,把相加的結果賦值給地址為 0X3000 的內存
變量名和函數名為我們提供了方便,讓我們在編寫代碼的過程中可以使用易于閱讀和理解的英文字符串,不用直接面對二進制地址,那場景簡直讓人崩潰。
需要注意的是,雖然變量名、函數名、字符串名和數組名在本質上是一樣的,它們都是地址的助記符,但在編寫代碼的過程中,我們認為變量名表示的是數據本身,而函數名、字符串名和數組名表示的是代碼塊或數據塊的首地址。
關于程序內存、編譯鏈接、可執行文件的結構以及如何找到名稱對應的地址我們后面會講。
光看文字無法完全理解的朋友可以看這個視頻c/c++《指針詳解》,以便更好的理解
二、指針變量的定義和使用
數據在內存中的地址也稱為指針,如果一個變量存儲了一份數據的指針,我們就稱它為指針變量。
在C語言中,允許用一個變量來存放指針,這種變量稱為指針變量。指針變量的值就是某份數據的地址,這樣的一份數據可以是數組、字符串、函數,也可以是另外的一個普通變量或指針變量。
現在假設有一個 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 = 100; int *p_a = &a;
在定義指針變量 p_a 的同時對它進行初始化,并將變量 a 的地址賦予它,此時 p_a 就指向了 a。值得注意的是,p_a 需要的一個地址,a 前面必須要加取地址符&,否則是不對的。
和普通變量一樣,指針變量也可以被多次寫入,只要你想,隨時都能夠改變指針變量的值,請看下面的代碼:
//定義普通變量 float a = 99.5, b = 10.6; char c = '@', d = '#'; //定義指針變量 float *p1 = &a; char *p2 = &c; //修改指針變量的值 p1 = &b; p2 = &d;
*是一個特殊符號,表明一個變量是指針變量,定義 p1、p2 時必須帶*。而給 p1、p2 賦值時,因為已經知道了它是一個指針變量,就沒必要多此一舉再帶上*,后邊可以像使用普通變量一樣來使用指針變量。也就是說,定義指針變量時必須帶*,給指針變量賦值時不能帶*。
假設變量 a、b、c、d 的地址分別為 0X1000、0X1004、0X2000、0X2004,下面的示意圖很好地反映了 p1、p2 指向的變化:
?
需要強調的是,p1、p2 的類型分別是float*和char*,而不是float和char,它們是完全不同的數據類型,讀者要引起注意。
指針變量也可以連續定義,例如:
注意每個變量前面都要帶*。如果寫成下面的形式,那么只有 a 是指針變量,b、c 都是類型為 int 的普通變量:
通過指針變量取得數據
指針變量存儲了數據的地址,通過指針變量能夠獲得該地址上的數據,格式為:
*pointer;
這里的*稱為指針運算符,用來取得某個地址上的數據,請看下面的例子:
#include <stdio.h> int main(){ int a = 15; int *p = &a; printf("%d, %dn", a, *p); //兩種方式都可以輸出a的值 return 0; }
運行結果:
15, 15
假設 a 的地址是 0X1000,p 指向 a 后,p 本身的值也會變為 0X1000,*p 表示獲取地址 0X1000 上的數據,也即變量 a 的值。從運行結果看,*p 和 a 是等價的。
上節我們說過,CPU 讀寫數據必須要知道數據在內存中的地址,普通變量和指針變量都是地址的助記符,雖然通過 *p 和 a 獲取到的數據一樣,但它們的運行過程稍有不同:a 只需要一次運算就能夠取得數據,而 *p 要經過兩次運算,多了一層“間接”。
假設變量 a、p 的地址分別為 0X1000、0XF0A0,它們的指向關系如下圖所示:
?
程序被編譯和鏈接后,a、p 被替換成相應的地址。使用 *p 的話,要先通過地址 0XF0A0 取得變量 p 本身的值,這個值是變量 a 的地址,然后再通過這個值取得變量 a 的數據,前后共有兩次運算;而使用 a 的話,可以通過地址 0X1000 直接取得它的數據,只需要一步運算。
也就是說,使用指針是間接獲取數據,使用變量名是直接獲取數據,前者比后者的代價要高。
指針除了可以獲取內存上的數據,也可以修改內存上的數據,例如:
#include <stdio.h> int main(){ int a = 15, b = 99, c = 222; int *p = &a; //定義指針變量 *p = b; //通過指針變量修改內存上的數據 c = *p; //通過指針變量獲取內存上的數據 printf("%d, %d, %d, %dn", a, b, c, *p); return 0; }
運行結果:
99, 99, 99, 99
*p 代表的是 a 中的數據,它等價于 a,可以將另外的一份數據賦值給它,也可以將它賦值給另外的一個變量。*在不同的場景下有不同的作用:*可以用在指針變量的定義中,表明這是一個指針變量,以和普通變量區分開;使用指針變量時在前面加*表示獲取指針指向的數據,或者說表示的是指針指向的數據本身。
也就是說,定義指針變量時的*和使用指針變量時的*意義完全不同。以下面的語句為例:
int *p = &a; *p = 100;
第1行代碼中*用來指明 p 是一個指針變量,第2行代碼中*用來獲取指針指向的數據。
需要注意的是,給指針變量本身賦值時不能加*。修改上面的語句:
int *p; p = &a; *p = 100;
第2行代碼中的 p 前面就不能加*。
指針變量也可以出現在普通變量能出現的任何表達式中,例如:
int x, y, *px = &x, *py = &y; y = *px + 5; //表示把x的內容加5并賦給y,*px+5相當于(*px)+5 y = ++*px; //px的內容加上1之后賦給y,++*px相當于++(*px) y = *px++; //相當于y=(*px)++ py = px; //把一個指針的值賦給另一個指針
【示例】通過指針交換兩個變量的值。
#include <stdio.h> int main(){ int a = 100, b = 999, temp; int *pa = &a, *pb = &b; printf("a=%d, b=%dn", a, b); /*****開始交換*****/ temp = *pa; //將a的值先保存起來 *pa = *pb; //將b的值交給a *pb = temp; //再將保存起來的a的值交給b /*****結束交換*****/ printf("a=%d, b=%dn", a, b); return 0; }
運行結果:
a=100, b=999
a=999, b=100
從運行結果可以看出,a、b 的值已經發生了交換。需要注意的是臨時變量 temp,它的作用特別重要,因為執行*pa = *pb;語句后 a 的值會被 b 的值覆蓋,如果不先將 a 的值保存起來以后就找不到了。
關于 * 和 & 的謎題
假設有一個 int 類型的變量 a,pa 是指向它的指針,那么*&a和&*pa分別是什么意思呢?*&a可以理解為*(&a),&a表示取變量 a 的地址(等價于 pa),*(&a)表示取這個地址上的數據(等價于 *pa),繞來繞去,又回到了原點,*&a仍然等價于 a。&*pa可以理解為&(*pa),*pa表示取得 pa 指向的數據(等價于 a),&(*pa)表示數據的地址(等價于 &a),所以&*pa等價于 pa。
對星號*的總結
在我們目前所學到的語法中,星號*主要有三種用途:
- 表示乘法,例如int a = 3, b = 5, c; c = a * b;,這是最容易理解的。
- 表示定義一個指針變量,以和普通變量區分開,例如int a = 100; int *p = &a;。
- 表示獲取指針指向的數據,是一種間接操作,例如int a, b, *p = &a; *p = 100; b = *p;
這里推薦一本書以便更好的理解《C和指針》,有需要的朋友可以評論區留言或者進群獲取。
三、C語言指針變量的運算(加法、減法和比較運算)
指針變量保存的是地址,而地址本質上是一個整數,所以指針變量可以進行部分運算,例如加法、減法、比較等,請看下面的代碼:
#include <stdio.h> int main(){ int a = 10, *pa = &a, *paa = &a; double b = 99.9, *pb = &b; char c = '@', *pc = &c; //最初的值 printf("&a=%#X, &b=%#X, &c=%#Xn", &a, &b, &c); printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc); //加法運算 pa++; pb++; pc++; printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc); //減法運算 pa -= 2; pb -= 2; pc -= 2; printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc); //比較運算 if(pa == paa){ printf("%dn", *paa); }else{ printf("%dn", *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,這是為什么呢?
以 a 和 pa 為例,a 的類型為 int,占用 4 個字節,pa 是指向 a 的指針,如下圖所示:
?
剛開始的時候,pa 指向 a 的開頭,通過 *pa 讀取數據時,從 pa 指向的位置向后移動 4 個字節,把這 4 個字節的內容作為要獲取的數據,這 4 個字節也正好是變量 a 占用的內存。
如果pa++;使得地址加 1 的話,就會變成如下圖所示的指向關系:
?
這個時候 pa 指向整數 a 的中間,*pa 使用的是紅色虛線畫出的 4 個字節,其中前 3 個是變量 a 的,后面 1 個是其它數據的,把它們“攪和”在一起顯然沒有實際的意義,取得的數據也會非常怪異。
如果pa++;使得地址加 4 的話,正好能夠完全跳過整數 a,指向它后面的內存,如下圖所示:
?
我們知道,數組中的所有元素在內存中是連續排列的,如果一個指針指向了數組中的某個元素,那么加 1 就表示指向下一個元素,減 1 就表示指向上一個元素,這樣指針的加減運算就具有了現實的意義,我們將在《C語言數組指針》一節中深入探討。
不過C語言并沒有規定變量的存儲方式,如果連續定義多個變量,它們有可能是挨著的,也有可能是分散的,這取決于變量的類型、編譯器的實現以及具體的編譯模式,所以對于指向普通變量的指針,我們往往不進行加減運算,雖然編譯器并不會報錯,但這樣做沒有意義,因為不知道它后面指向的是什么數據。
下面的例子是一個反面教材,警告讀者不要嘗試通過指針獲取下一個變量的地址:
#include <stdio.h> int main(){ int a = 1, b = 2, c = 3; int *p = &c; int i; for(i=0; i<8; i++){ printf("%d, ", *(p+i) ); } return 0; }
在 VS2010 Debug 模式下的運行結果為:
3, -858993460, -858993460, 2, -858993460, -858993460, 1, -858993460,
可以發現,變量 a、b、c 并不挨著,它們中間還參雜了別的輔助數據。
指針變量除了可以參與加減運算,還可以參與比較運算。當對指針變量進行比較運算時,比較的是指針變量本身的值,也就是數據的地址。如果地址相等,那么兩個指針就指向同一份數據,否則就指向不同的數據。
上面的代碼(第一個例子)在比較 pa 和 paa 的值時,pa 已經指向了 a 的上一份數據,所以它們不相等。而 a 的上一份數據又不知道是什么,所以會導致 printf() 輸出一個沒有意義的數,這正好印證了上面的觀點,不要對指向普通變量的指針進行加減運算。
另外需要說明的是,不能對指針變量進行乘法、除法、取余等其他運算,除了會發生語法錯誤,也沒有實際的含義。
就先寫這么多吧,如果有人看的話后面再更新,最后給大家發個福利,這是一本理解指針的電子書?《C和指針》
因為很多朋友學習c/c++都不得門而入,所以最后再給大家貼個學習大綱?
?
好了,這篇文章就到這里了,如果這篇文章沒有解決到你的問題,可以進群來咨詢老師和領取優質視頻跟電子書資料哦,覺得寫的還不錯的話請關注點贊收藏三連!
?
總結
以上是生活随笔為你收集整理的栈顶指针到底指向哪_被称为“程序员试金石”的指针真的没有那么难!不信的话你来看看的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么new一个指针_【译】Rust与智能
- 下一篇: poi word插入图片_豌豆BI工具看