本文主要介紹C語(yǔ)言中指針的基本概念與用法,本博文從零開(kāi)始講解指針,只要你了解基本的C語(yǔ)法,相信你看完本文后一定會(huì)有所收獲。
一. What(什么是指針)
1. 地址初了解
要搞明白什么是指針,首先我們得先了解一個(gè)概念:地址。
什么是地址呢?
我們知道,計(jì)算機(jī)的內(nèi)存是一塊連續(xù)的大塊存儲(chǔ)空間,為了提高 CPU 查找數(shù)據(jù)的效率,我們將這塊連續(xù)的存儲(chǔ)空間以字節(jié)為單位進(jìn)行編號(hào),每個(gè)字節(jié)都一一對(duì)應(yīng)了一個(gè)編號(hào),這個(gè)編號(hào)就叫做地址。
舉個(gè)形象的例子:
如果我們把一座宿舍樓看做是內(nèi)存,那么宿舍樓里的房間我們就可以看成是內(nèi)存里的每一個(gè)字節(jié),那么每個(gè)房間的門(mén)牌號(hào)就可以看做是地址。
為什么要有地址?
通過(guò)上面的例子,我們很容易了解到地址存在的意義。試想,如果宿舍樓里的房間都沒(méi)有門(mén)牌號(hào),我們就很難描述一指定房間的具體位置。CPU 對(duì)內(nèi)存的處理也是如此,如果沒(méi)有地址,CPU 就沒(méi)有辦法確定指定內(nèi)存的具體位置,從而正確且快速的訪問(wèn)到該內(nèi)存空間。
2. 指針概念
- 指針就是地址
- 指針就是地址
- 指針就是地址
重要的事情說(shuō)三遍,我們必須明確的知道一件事,指針就是地址,可以理解為,指針是地址在C語(yǔ)言里的一種特殊叫法。
#include <stdio.h> int main() { int a = 10; int *p = &a; // p是不是指針? printf("%p\n", p); // %p 可以格式化輸出地址。 return 0; }
既然如此,上述代碼中 p 是不是指針呢?
我們說(shuō)過(guò)指針就是地址,而 p 是不是地址?當(dāng)然不是!地址是一個(gè)單獨(dú)的數(shù)據(jù),就像 int a = 10; 我們能說(shuō) a 就是 10 嗎?當(dāng)然不能,10是一個(gè)整形常量,而 a 是一個(gè)整形變量,我們只能說(shuō) a 里面存放的值是 10,而不能簡(jiǎn)單的說(shuō) a 就是 10。
同理,上述代碼中的 p 是一個(gè)變量,里面存放的是 a 的地址,我們稱它為指針變量。所以嚴(yán)格意義上來(lái)說(shuō),p 不是指針,而是指針變量。就像 a 不是整數(shù)10,而是整形變量一樣。
那么什么是指針呢,我們把這段代碼運(yùn)行起來(lái):
沒(méi)錯(cuò),嚴(yán)格意義上來(lái)說(shuō),這段數(shù)字才真正的叫做指針 (地址值在計(jì)算機(jī)中一般以16進(jìn)制顯示)。
3. 指針與指針變量
通過(guò)上面的講解,相信大家可以理解,指針是地址,是一個(gè)常量,而指針變量是一個(gè)變量,里面存放的是指針,這兩者之間有著本質(zhì)區(qū)別。
看到這,相信有些同學(xué)就有疑問(wèn)了:明明上課的時(shí)候,或者某些書(shū)上都將指針變量統(tǒng)稱為指針,就像 上例中的 p 平時(shí)都叫做指針,為什么現(xiàn)在又說(shuō) p 不是指針呢?
請(qǐng)大家注意,我說(shuō) p 不是指針是在嚴(yán)格意義上來(lái)說(shuō)。
在日常使用中,我們經(jīng)常把指針和指針變量混為一談。這似乎成為了一種習(xí)慣,甚至我在下面的表述中都可能將指針變量表述為指針。
結(jié)論是:我們?cè)谌粘J褂弥校梢阅:羔樑c指針變量的概念,把指針變量統(tǒng)稱為指針,但是,我們心里必須清楚的意識(shí)到,指針和指針變量的本質(zhì)區(qū)別,以及你叫的這個(gè)東西到底是指針還是指針變量。
二. Why(為什么要有指針)
經(jīng)過(guò)上面的講解,相信這個(gè)問(wèn)題就非常簡(jiǎn)單了。指針就是地址,那么為什么要有地址?究其原因就是,為了提高查找效率。
試想如果沒(méi)有地址的存在,我們寫(xiě)下 int a = 10; 的時(shí)候,系統(tǒng)會(huì)自動(dòng)在內(nèi)存中開(kāi)辟一4字節(jié)的空間存放10這個(gè)數(shù)值。而我們 CPU 在訪問(wèn)的時(shí)候,由于沒(méi)有地址的存在只能在內(nèi)存中一塊一塊的遍歷對(duì)比查找,而有了地址,內(nèi)存就可以把地址值告訴 CPU,CPU 就可以根據(jù)地址值直接定位到該內(nèi)存,大大提高了 CPU 訪問(wèn)內(nèi)存的效率與準(zhǔn)確性。
三. How(如何使用指針)
1. 基本定義
指針變量的定義格式較簡(jiǎn)單,只要在類型和變量名之間加上 * 操作符,就可以定義該類型的指針變量。
int *a = NULL; //定義整形指針變量 double *d = NULL; //定義雙精度浮點(diǎn)型指針變量 struct stdnt *ps = NULL; //定義結(jié)構(gòu)體指針變量
注: NULL 是指針變量初始化的時(shí)候常用的宏定義,稱其為空指針,本質(zhì)為 0 值。
如果定義時(shí)不初始化,如:
int *a;
我們知道,變量如果不初始化,其中的值就是隨機(jī)值,也就是此時(shí)的指針變量 a 里面存放的是隨機(jī)值,如果此時(shí)訪問(wèn) a 變量就會(huì)以這個(gè)隨機(jī)值作為地址訪問(wèn)對(duì)應(yīng)的內(nèi)存,這種操作是非法的。這種不初始化的指針我們稱之為野指針。
所以,為了避免野指針的出現(xiàn),我們?cè)诙x指針變量時(shí)盡量對(duì)其進(jìn)行初始化。
2. 取地址操作符 &
我們使用指針就是使用地址,那么地址從何而來(lái)?我們可不可以這么寫(xiě)代碼:
int *a = 0x11223344;
我們把 0x11223344 看做是地址值交給整形指針變量 a 來(lái)存儲(chǔ)。語(yǔ)法上沒(méi)有問(wèn)題,但是這樣寫(xiě)毫無(wú)意義!因?yàn)?0x11223344 這個(gè)地址對(duì)于我們來(lái)說(shuō)是未知的,我們并沒(méi)有讀取和訪問(wèn)的權(quán)限,這時(shí)候的 a 變量就相當(dāng)于一個(gè)野指針。
事實(shí)上,我們的地址是由 & 取地址符取出來(lái)的。
#include <stdio.h> int main() { int a = 10; int *p = &a; return 0; }
這里我們定義了整形變量 a,取地址符取出了 a 的地址并把它賦給整形指針變量 p。
這里就有一個(gè)疑問(wèn),a 是一個(gè)整形變量,占4字節(jié)的空間,而通過(guò)上面的介紹,我們知道內(nèi)存的地址編號(hào)是以字節(jié)為單位的,這就意味著變量 a 其實(shí)存在4個(gè)地址,那么 &a 究竟取出的是哪一個(gè)地址呢?
上結(jié)論:
在C語(yǔ)言中,對(duì)任意類型的變量或者數(shù)組取地址,永遠(yuǎn)取得是數(shù)值上最小的那個(gè)地址。
我們畫(huà)一下上面代碼的內(nèi)存分布圖:
假設(shè)從左向右地址依次升高,那么 p 里面存放的就是 a 變量4字節(jié)中地址最低的那個(gè)字節(jié)的地址。
3. 解引用操作符 *
我們上面說(shuō)到,對(duì)變量取地址取得是地址最低的字節(jié)即首字節(jié)地址,那么我們?nèi)绾瓮ㄟ^(guò)這一個(gè)字節(jié)的地址訪問(wèn)到原變量里的數(shù)據(jù)呢,這里就需要使用 * 操作符:
#include <stdio.h> int main() { int a = 10; int *p = &a; *p = 20; // 等價(jià)于 a = 20 printf("%d\n", *p); printf("%d\n", a); return 0; }
看上面的代碼,我們把 a 變量的地址賦值給指針變量 p,通過(guò) *p 即可訪問(wèn)到該變量的內(nèi)容。
那么 * 操作符到底是如何使用的呢,下面給出結(jié)論:
對(duì)指針解引用,就是指針指向的目標(biāo)。
就像上例中 *p 是什么?*p 就是 a,*p 就是 a,*p就是a,重要的事情說(shuō)三遍。所以,如果我改變 *p 的值,完全等價(jià)于直接改變 a 變量的值。
上面的代碼運(yùn)行結(jié)果:
那么,我們看下面這段代碼:
int main() { int a = 0x11223344; int *p = &a; printf("0x%x\n", *(char*)p); return 0; }
注意,我們說(shuō) *p 就是 a,但是,這里的 *p 被強(qiáng)制類型轉(zhuǎn)化為 char* 類型之后再進(jìn)行的解引用操作,此時(shí)的 p 就是一個(gè)字符類型的指針,因?yàn)樽址?char 類型在C中只占一個(gè)字節(jié),對(duì)其進(jìn)行解引用只能訪問(wèn)一個(gè)字節(jié)的內(nèi)容。而我們說(shuō)過(guò) p 中存放的是 a 變量的首字節(jié)的地址,即 44 的地址 (至于為什么,請(qǐng)讀者自己了解大小端的內(nèi)容),解引用就只訪問(wèn)到了 44:
4. 結(jié)構(gòu)體指針
下面定義一個(gè)結(jié)構(gòu)體:
typedef struct Student { int age; char name[20]; char sex[2]; }STD;
C語(yǔ)言中任何數(shù)據(jù)類型都有其對(duì)于的指針類型,結(jié)構(gòu)體也不例外,如下:
int main() { STD s = { 18, "張三", "男" }; STD *ps = &s; // 定義結(jié)構(gòu)體指針 printf("%s\n%d\n%s\n", s.name, s.age, s.sex); }
我們定義一個(gè)結(jié)構(gòu)體變量并初始化,通過(guò) . 操作符可以很容易訪問(wèn)到結(jié)構(gòu)體的內(nèi)容。那么我們?nèi)绾瓮ㄟ^(guò)結(jié)構(gòu)體指針變量 ps 來(lái)訪問(wèn)結(jié)構(gòu)體的內(nèi)容呢?
我們說(shuō)過(guò)對(duì)指針解引用,就是指針指向的目標(biāo),那么請(qǐng)讀者思考,*ps 是什么呢?沒(méi)錯(cuò) *ps 就是 s變量,既然如此,我們就可以這么訪問(wèn):
printf("%s\n%d\n%s\n", (*ps).name, (*ps).age, (*ps).sex); //注:因 . 操作符優(yōu)先級(jí)高于 * 操作符,故 (*ps) 必須加括號(hào)
C語(yǔ)言可能認(rèn)為這樣寫(xiě)太麻煩,于是對(duì)于結(jié)構(gòu)體指針我們可以使用 -> 操作符直接訪問(wèn)到結(jié)構(gòu)體的內(nèi)容:
printf("%s\n%d\n%s\n", ps->name, ps->age, ps->sex);
ps->name 寫(xiě)法完全等價(jià)于 (*ps).name 這樣的寫(xiě)法。
注:-> 操作符只在結(jié)構(gòu)體指針變量訪問(wèn)結(jié)構(gòu)體成員時(shí)使用。
5. 多級(jí)指針
首先問(wèn)大家一個(gè)問(wèn)題,指針變量是不是變量?是變量。既然是變量,那么就有地址,我們依然可以把指針變量的地址存入一個(gè)新的變量,此變量我們可以稱為二級(jí)指針變量:
int a = 10; int *pa = &a; int **ppa = &pa; // 定義二級(jí)指針變量 *ppa; //等價(jià)于 pa **ppa; //等價(jià)于 *pa,即等價(jià)于 a
二級(jí)指針大家不要看的多么神秘,所謂二級(jí)指針其實(shí)也就是一個(gè)指針變量,只不過(guò)里面存放的是另一個(gè)指針變量的地址而已。
既然如此,二級(jí)指針變量是不是變量呢?它能不能取地址存入另外一個(gè)三級(jí)指針變量呢?那么,三級(jí)指針變量是不是變量呢?… 如果你愿意,可以一直這么套娃下去~
6.指針變量的命名規(guī)范
之所以把這塊單獨(dú)分出來(lái)講,是因?yàn)閷?duì)指針變量進(jìn)行一個(gè)好的命名規(guī)范,不僅有利于提高代碼的可讀性,更有助于我們理解一些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)的代碼。
指針的命名習(xí)慣上在原變量名前加字母 p。 如定義整形變量 a ,其指針變量就命名為 pa, 定義結(jié)構(gòu)體變量 std 其對(duì)應(yīng)指針變量命名為 pstd,如果對(duì) pstd 再次取地址存入二級(jí)指針變量,那么該二級(jí)指針變量就應(yīng)該命名為 ppstd。
四. 我對(duì)指針的一些理解
都說(shuō)指針是C語(yǔ)言的靈魂,那么指針的魅力究竟在何處?
我們看這段交換兩個(gè)數(shù)的代碼:
#include <stdio.h> void Swap(int a, int b) { int t = a; a = b; b = t; } int main() { int a = 10; int b = 20; Swap(a, b); printf("a = %d\n", a); printf("b = %d\n", b); return 0; }
我們應(yīng)該知道,這段代碼是無(wú)法完成 a, b 的交換的,因?yàn)?Swap() 里的 a, b 只不過(guò)是 main() 函數(shù)里 a, b 的一份拷貝。即,Swap() 函數(shù)里 a, b 的改變是不會(huì)影響 main() 函數(shù)的。這種傳參的方式我們稱之為傳值。
可是我們這么寫(xiě):
#include <stdio.h> void Swap(int *pa, int *pb) { int t = *pa; *pa = *pb; *pb = t; } int main() { int a = 10; int b = 20; Swap(&a, &b); printf("a = %d\n", a); printf("b = %d\n", b); return 0; }
main() 傳參時(shí)傳 a, b 的地址,Swap() 函數(shù)使用指針進(jìn)行接收,內(nèi)部使用解引用方式進(jìn)行交換,我們就可以完成真正的交換功能。這種傳參的方式我們稱之為傳址。
我在一篇文章上看到這樣一個(gè)說(shuō)法,可謂是對(duì)指針理解到了本質(zhì):
- 傳值就是傳值。
- 傳址就是傳值本身。
值和值本身兩個(gè)概念希望大家能夠深刻理解。
如同我們 Swap() 函數(shù)的第一種寫(xiě)法,main() 函數(shù)僅僅是將 a, b 的值傳遞給了 Swap() 函數(shù)進(jìn)行處理,可無(wú)論 Swap() 函數(shù)對(duì)其做什么樣的處理,也依然無(wú)法改變?cè)瓉?lái) a, b 的值。
而 Swap() 的第二種寫(xiě)法,main() 函數(shù)傳的是 a, b 的地址,相當(dāng)于傳的是 a, b 本身,這樣 Swap() 在進(jìn)行處理時(shí)便可直接改變?cè)瓉?lái)的 a, b 的值。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注服務(wù)器之家的更多內(nèi)容!
原文鏈接:https://blog.csdn.net/qq_55135139/article/details/120151642