天天看點

C語言指針

(一) 指針知識先導

int num=100;

計算機中資料都是存儲在記憶體中,是以讀寫資料的本質其實是讀寫記憶體,而目前讀寫記憶體的唯一方式就是通過變量名,這種方式被稱為“直接通路”記憶體。

在計算機中,記憶體空間的最小機關為位元組,作業系統會為每一個位元組記憶體空間編号,并且這個編号在目前程式中是唯一的。

假設圖是飯店中的一排房間,每個房間中都住着一個球員, 例如:101 号房間住着 7号球員、105 号房間住着 2 号球員、113 号房間住着 1 号球員。

C語言指針

如果想要在這排房間中找到 2 号球員,隻需知道他住 105 号房間即可。101 房号相當于記憶體位址、101 房間相當于記憶體空間、7 号球員相當于記憶體空間中的資料。

前面講過,要想找到 7 号球員,必須通過房間号來查找。同理,在計算機中,要想讀寫記憶體空間中的資料,也可以先通過記憶體位址找到該記憶體空間,然後進行讀寫操作。

讀寫記憶體的 2 種方式:

第 1 種 通過變量名讀寫記憶體。

變量本質上是一塊有名字的記憶體空間,通過變量名讀寫記憶體,如圖所示:

C語言指針

第 2 種 通過記憶體位址讀寫記憶體。

在計算機記憶體中,每一個位元組記憶體空間都有一個編号,這個編号被稱為記憶體位址。通過該位址可以讀寫對應的記憶體空間,如圖所示:

C語言指針

在上一節中,讨論了記憶體空間與記憶體位址的關系,為了更加深入了解這 2 者之間的關系,将使用 vs2012 自帶的工具,來更加形象的分析。

測試代碼如下:

【注意】

第 5 行代碼中,&(shift+7)是 c 語言中的取位址符。&num 表示計算變量 num 所對應記憶體空間的位址編号,也就是所謂的記憶體位址。%p 表示以 16 進制格式輸出記憶體位址。

編寫完上述程式後,下面就來通過工具一步步探索記憶體空間。

第 1 步 測試程式第 6 行,滑鼠單擊加斷點:

第 2 步 運作程式,控制台會輸出 16 進制的位址資料(每次運作可能都不一樣),

如圖所示:

C語言指針

第 3 步 依次點選菜單【調試】->【視窗】->【記憶體】->【記憶體 1】,打開記憶體視窗,如圖所示:

C語言指針

第 4 步 将控制台輸出的資料,輸入到【記憶體 1】視窗的【位址】欄中,然後按下Enter鍵,如圖所示:

C語言指針

第 5 步 在【記憶體 1】窗體内右鍵點選,然後選擇【4 位元組整數(4)】、【帶符号顯示】如圖所示:

C語言指針

【說明】

因為 num 是 int 類型,32 位系統下占 4 位元組,是以第 5 步選【4 位元組整數】,其他類型資料依次類推。

第 6 步 檢視【記憶體 1】視窗,可以看到整數+999,其實就是 999 隻是顯示了符号位+而已,如圖所示:

C語言指針

分析:

1、 以上整個過程,首先第 4 行,通過變量名 num 将整數 999 寫入記憶體空間中。

2、 第 5 行,使用&num 計算出變量 num 對應的記憶體空間位址 0019fe50。

3、 通過斷點調試的方式,檢視 0019fe50 位址空間中儲存的資料 999。

4、 經過以上分析,變量 num 完整記憶體模型如圖所示:

C語言指針

5、可以看到,通路一塊記憶體空間可以通過變量名,也可以記憶體位址。

【調試技巧】

為了友善使用【記憶體 1】檢視記憶體,建議将【列】選項設定為【自動】,如圖所示:

C語言指針

前面介紹過,變量的本質是一塊有名字的記憶體空間。其實這塊記憶體空間不僅有名字,而且有編号,在 32 位系統下,這個編号是一個 4 位元組的整數。通過(&變量名)的方式可以得到這個整數,例如:

int num=10;

printf(“%p\n”,&num); //以 16 進制格式輸出

這個編号與一塊記憶體空間是一一對應的,通過這個編号可以找到對應記憶體空間。類似于現實生活中,知道某個人的家庭位址,就可以通過位址找他家一樣。在 c 語言程式中将這個編号形象的稱呼為“記憶體位址”,通過記憶體位址就可以找到對應的記憶體空間。

現階段目前,在程式中得到記憶體位址的唯一方式就是:&變量名。由于這種方式得到的記憶體位址就是變量所對應的記憶體空間位址,又是通過變量名得到的,是以可以稱為“變量位址”。這裡務必清楚,變量位址本質上就是記憶體位址。

用于儲存記憶體位址的變量,稱為指針變量。在 c 語言程式中不僅變量有類型,資料也是有類型的,例如:1(整數)、3.14(浮點數)、’c’(字元),需要使用與之比對的類型變量進行儲存。同理,記憶體位址也是一種資料,這種資料都是指針類型,是以需要指針類型變量來儲存這種資料。

定義指針變量的一般形式為:

類型名 *變量名;

類型名表示該指針變量隻能儲存該類型變量的位址,*表示該變量是指針變量隻能儲存位址資料,變量名即該變量的名稱。

例如:int *p_a;

int 表示該指針變量隻能儲存 int 類型變量的位址,*表示變量 p_a 是指針變量隻能儲存位址資料,p_a 即指針變量的名稱。

指針變量和普通變量初始化方式相同,可以在變量定義時初始化,也可以先定義後初始化。例如:

在 c 語言程式中,将某個變量的位址指派給指針變量,就認為該指針變量指向了某個變量,例如:

int a=10;

int*p_a=&a;

上述程式中,将整數變量 a 的位址指派給指針變量 p_a,就認為 p_a 指向了變量 a,如圖所示:

C語言指針

可以看到,變量 a 中存儲的是整數 10,而變量 p_a 中存儲的是變量 a 的位址。有點像現實生活中的中介,想要通路資料 10,必須先找到指針變量 p_a,通過變量 p_a 中的資料&a,再找到變量 a,最後通路資料 10。

指針變量的引用分 2 種情況:

第 1 種 引用指針變量。

運作結果如圖所示(每次運作結果可能都不一樣):

C語言指針
C語言指針

第 2 種 引用指針變量指向的變量。

運作結果如圖所示:

C語言指針

【易混】在定義變量的時候,*放到變量前,表名變量是指針類型;在使用變量的時候用來讀寫指針變量指向的值。

速記:

&取變量位址;

*定義時表示是指針變量;*使用時表示讀寫指針變量的值。

在 c 語言中,函數參數不僅可以是字元型、整型、浮點型……等,還可以是指針類型,作用是将變量位址傳遞給函數形參。

主要目的就是:函數内部修改外部變量的值。

下面通過兩個例子來說明指針變量做函數參數的用法。

案例 1,函數内部改變外部變量的值

案例 2: 封裝函數,交換兩個整型變量的值。

scanf 還可以接收多個輸入資料,例如:

輸入:1 2 (1 和 2 之間以空格隔開),然後按下Enter鍵,運作結果如圖所示:

C語言指針

scanf 中資料類型一定不能用錯,float 類型必須使用%f、double 類型必須用%lf、int 類型必須使用%d,如果使用錯了就會發現結果很奇怪。

使用 scanf 需要注意的問題

(1)scanf 函數中應該傳入變量位址,而不是變量名,例如:

int a,b;

scanf(“%d %d”,a,b);

這種寫法是錯誤的,應該将“a,b”修改為“&a,&b”。

(2)從鍵盤擷取多個資料時,相鄰資料之間可以使用空格、回車、tab 鍵作為兩個資料之間的分隔符。例如:

scanf(“%d %d”,&a,&b);

第 1 種輸入方式:

1 2 //1 和 2 之間以空格分隔

第 2 種輸入方式:

1 2 //1 和 2 之間以 tab 鍵分隔

第 3 種輸入方式:

1

2   //1 和 2 之間以回車分隔

(3)*如果在 scanf 函數中的格式控制字元串中除了占位符之外,還有其他字元,則在輸入時也必須在對應的位置上輸入相同的字元。例如:

int a,b,c;

scanf(“%d,%d,%d”,&a,&b,&c); //注意 scanf 中%d 之間以“,”分隔

輸入:

1,2,3 (輸入資料時,也必須以“,”分隔)

(4)使用 scanf 擷取字元串時,隻需傳入字元數組名即可,取位址符&可以省略不寫。例如:

char c[10];

scanf(“%s”,c); //可以省去&

輸入:

hello

注意使用%s 的時候,字元串中不要有空格,否則行為很怪異。

scanf 有很多怪異的行為和坑,但是深入的東西研究價值不大,是以隻要會正常的用法即可。

(二) 數組與指針

數組本質上是一片連續的記憶體空間,每個數組元素都對應一塊獨立的記憶體空間,它們都有相應的位址。是以,指針變量既然可以指向變量,也就可以指向數組元素。

在 c 語言中數組可以看作是相同類型變量的集合。通俗點講,數組中每個元素類型都是相同的。例如:

char ch[10] //數組 ch 可以看作是由 10 個 char 變量組成

int a[10] //數組 a 可以看作是由 10 個 int 變量組成

float f[10] //數組 f 可以看作是由 10 個 float 變量組成

數組本質上是一片連續的記憶體空間,數組元素又可以看作是單獨的記憶體空間,數組就好像是一排房間,數組元素是單獨的一個房間。是以,每個數組元素也都有自己的記憶體空間位址,簡稱數組元素位址。

可以使用指針變量來儲存數組元素位址,例如:

int a[5]={1,2,3,4,5}; //定義長度為 5 的 int 數組

int* p_a; //定義指向 int 變量的指針變量 p_a

p_a=&a[0] //把 a 數組第 0 個元素位址賦給指針變量 p_a。相當于&(a[0])

p_a 中儲存了數組 a 第 0 個元素位址,可以認為指針變量 p_a 指向數組 a 第 0 個元素,

C語言指針

因為數組元素本質上可以看作是單獨的變量,是以引用指向數組元素的指針變量與引用指向變量的指針變量方式相同,直接使用*指針變量名即可。

和通路變量方式類似,可以将通過數組名通路元素方式稱為“直接通路”,将通過數組元素位址通路元素方式稱為“間接通路”。

在計算機中記憶體的最小機關是位元組,每個位元組都對應一個位址。如果一個變量占用多個位元組,就會占用多個記憶體位址。例如:char 類型變量占 1 位元組就對應 1 個位址、short 類型變量占 2 位元組對應 2 個位址、int 類型變量占 4 位元組對應 4 個位址…..其他類型依次類推。同理,數組元素類型不同占用的記憶體位址也不同。

C語言指針

在 c 語言中,數組名與數組首元素位址等價。

C語言指針

指針本質上就是記憶體位址,在 32 位作業系統下,記憶體位址隻是 4 位元組的整數。既然是整數,就可以進行加、減、乘、除…..等算術運算。不過需要注意,在 c 語言中一般隻讨論指針加、減運算,乘、除等其他算術運算是沒有意義。

在實際開發中,指針加、減多用于數組(或者連續記憶體空間)。當指針變量 p 指向數組元素時,p+1 表示指向下一個數組元素,p-1 表示指向上一個數組元素。注意加減運算都不是“移動一個位元組”,而是移動一個“單元”,對于 int 來講一個單元是 4 個位元組。

C語言指針

指針的加減法是指針和普通整數運算才有意義,兩個指針加法沒意義:p=p+n 表示 p 向下指 n 個單元,p=p-1 表示 p 向上指 n 個單元。

下面通過例子來了解一下指針減法。

C語言指針

當指針變量 p 指向數組元素時,p+1、p-1 分别表示指向下一個、上一個數組元素。依次類推,p+i、p-i 分别表示指向下 i 個元素,上 i 個元素。

p+i 不能超過數組最後一個元素,p-i 不能小于數組第一個元素。否則就會發生數組越界。

兩個指針的加法沒意義,兩個指針的減法表示相差的單元的個數。

還經常使用到兩個指針相減,例如:p2-p1。

當 p1 和 p2 都指向同一個數組中的元素時,p2-p1 才有意義。以數組 int a[5]為例:假設p2 指向元素 a[2],p1 指向元素 a[0],執行 p2-p1 時不是表示隔了多少個位元組,而是表示 p2所指向的元素與 p1 所指向的元素之間隔了多少個元素。

下面通過例子來了解兩個指針相減。

C語言指針

總結:

兩個指針之間的減法表示:相差的單元的個數。

兩個指針之間的加法,沒有意義;

指針+普通整數表示指針向挪 n 個單元;指針-普通整數表示指針後挪 n 個單元;

函數參數不僅可以是變量,也可以是數組,它的作用是将數組首元素位址傳給函數形參。

在 c 語言中,數組做函數參數時,是沒有副本機制的,隻能傳遞位址。也可以認為,數組做函數參數時,會退化為指針。

下面通過例子來了解數組做形參時,退化為指針。

C語言指針

可以看到 2 次輸出結果不一樣。這是因為當數組做函數形參時,會退化為指針。

void getsize(int nums[5])

退化為:

void getsize(int *nums)

在 32 位系統下,所有指針變量都占 4 個位元組,是以第 5 行輸出結果為 4。

由于數組做函數參數時,會退化為指針,導緻無法在被調函數中計算傳入的數組大小以及長度。為了解決這種問題,規定數組做函數參數時,必須傳入數組長度,例如:

void getsize(int *nums,int length);

其中形參 length 表示數組 nums 的長度。

隻有在數組聲明的函數中才能通過sizeof(數組名)算出來數組的位元組數;

int nums[]={1,5,8,9,666}; int *p=nums;這種情況sizeof(p)=4,因為p是指針,為啥sizeof(nums)就能算出20呢,因為編譯器特殊對待。

因為c編譯器比較低級,函數參數聲明中即使使用數組類型void dy(int data[]),也會被退化成指針類型:void dy(int *data),是以在給函數傳遞數組的時候,要傳遞數組的名字,同時要在聲明數組的函數中通過sizeof把數組元素個數算出來,穿進去,函數内部是算不出數組有幾個元素的。

在 c 語言中,數組名等價于數組首元素位址。例如:int a[5],a 與&a[0]完全等價。可以認為 a+i 等價于&a[i],a+i 指向 a[i],那麼*(a+i)就是 a+i 所指向的數組元素 a[i]。是以,*(a+i)與 a[i]等價。

無論是對于數組名來講還是對于指針來講:*(a+i)與 a[i]等效

除了在聲明數組時候 sizeof(數組名)和 sizeof(指針變量名)的不同,其他時候“數組名”和“指針變量名”用法都是一樣的。

(三) 字元串與指針

在 c 語言中字元串本質上就是采用字元數組形式進行存儲。前面介紹過,指針可以指向數值類型數組元素,也就可以指向字元類型的數組元素。本節将介紹指向字元數組元素的指針。

在 c 語言中,字元串存放在字元數組中,要想引用字元串有兩種方式:

使用字元數組存放字元串,通過數組名引用字元串,通過下标引用字元串中的字元

使用字元指針變量指向字元串,通過字元指針變量引用字元串、字元串中的字元。

下面的代碼很簡單:

當然也可以這樣使用:

兩個 sizeof 一樣嗎?

需要注意的是,不管是通過字元數組,還是通過字元指針引用字元串。編譯器都會自動在字元串末尾添加 0,下面通過記憶體工具,來檢視字元串在記憶體中是如何存儲的。

第 1 步 編寫測試程式

第 2 步 在 getchar 添加斷點。

第 3 步 運作程式,記錄 str 指向字元串位址。

第 4 步 在【記憶體 1】中輸入字元串位址,然後按下Enter鍵,如圖所示:

C語言指針

如果沒有顯示出如圖的效果,可以參考以下步驟配置檢視記憶體方式。

右鍵視窗任意位置,依次選擇【1 位元組整數】、【不帶符号顯示】、【ansi 文本】。

C語言指針

可以看到字元串在記憶體中,是按照字元的 ascii 碼進行存儲的,并且最後一位是 0 作為字元串結束标志。

(四) 字元串處理函數

在 c 語言中字元串是非常重要的概念,字元串處理函數是針對字元串進行操作的一系列函數。主要包含在頭檔案中,本節将介紹常用的字元串處理函數。

strcpy 和 memcpy 的差別:strcpy 是把源和目标都看過字元串類型,是以會碰到'\0'停止;而 memcpy 則會原樣複制。

ansi 标準規定,傳回值為正數、負數、0 。而确切數值是依賴不同的 c 實作的,比如有的平台就是傳回 1、-1、0。

字元串比較大小,不能使用算術運算符進行比較,例如:

str1>str2、str1==str2、str1<str2

使用算術運算符比較的是字元串首元素的位址,并不是比較字元串的内容。

繼續閱讀