第 15 講:指針
這是最為頭疼的 C 語言專題,比起之前的內(nèi)容來說,指針確實(shí)較難理解一些。更別說使用了。不過,看一下講解,應(yīng)該會(huì)比較清楚一些它的基本用途。
指針的定義
指針(Pointer),就像是一個(gè)桌面上的“快捷方式”。你打開程序?yàn)榱瞬蝗ッ恳粋€(gè)盤找到對(duì)應(yīng)的應(yīng)用程序然后打開,就建立了一個(gè)桌面快捷方式,這樣統(tǒng)一管理這些東西,會(huì)比較輕松一些。
我們可以將指針理解為一個(gè)變量的快捷方式。為了統(tǒng)一管理數(shù)據(jù),所以建立起了這些指針,來“間接”操控?cái)?shù)據(jù)的數(shù)值。
我們使用這樣的符號(hào)來聲明一個(gè)指針:
它們的名字和普通的變量名字都是一樣的規(guī)范:數(shù)字、字母、下劃線(且首字符不可是數(shù)字)。當(dāng)然了,因?yàn)橹羔樣⑽拿?pointer,所以取名為這三個(gè)寫法,是指針最常見的取名。我們可以用星號(hào) *
表示它是一個(gè)指針變量。注意哦,這個(gè)星號(hào)要寫在變量名字的左邊才奏效;寫在變量的右側(cè)則可能會(huì)被處理成乘法符號(hào)而報(bào)錯(cuò)。
另外,在 C 語言之中,星號(hào)是挨著變量名字的,當(dāng)然你也可以寫作
全部都可以,它們都和第一句 int *p
是一樣的意思,不過有些時(shí)候?yàn)榱朔奖憷斫獯a呢,你可能需要借助后面兩種思維來理解指針。現(xiàn)在來看看如何理解指針。
指針的賦值和使用
我們使用這兩個(gè)符號(hào)來使用指針:星號(hào) *
和 and 符號(hào) &
。注意了哈,這里又出現(xiàn)了星號(hào),而這個(gè)星號(hào)和定義是指針變量的含義不同。
星號(hào)
*
(取值符):取變量對(duì)應(yīng)的數(shù)值;and 符號(hào)
&
(取地址符):取變量的地址。
來看下這三行代碼。第一行是定義變量 a
,并且 a
的存儲(chǔ)空間上放了個(gè)數(shù)值是 25;第二行是說,定義一個(gè)指針變量,賦的值是 a
的地址(因?yàn)檫@里 a
前有一個(gè)取地址的符號(hào),表示是取出了地址值)。
說成大白話,就是有一個(gè)快捷方式,取名叫 p
,這個(gè) p
它總歸是一個(gè)變量,但怎么和 a
產(chǎn)生關(guān)聯(lián)呢?就讓 p
這個(gè)變量存的東西是 a
的地址,就 OK 了。換句話說,p
這個(gè)變量的存儲(chǔ)空間上,它的數(shù)值并不是 a
的這個(gè)值 25,而是 a
變量的“位置”。
p
變量處存放的不是別的,就是這個(gè)地址的數(shù)值。
第三句話則是輸出 p
變量存的值(a
的地址)和 p
上地址所對(duì)應(yīng)的那個(gè)變量的值(也就是 a
的值),所以會(huì)輸出一個(gè)十六進(jìn)制數(shù)(具體是多少我也不知道,定義變量的時(shí)候,電腦隨機(jī)分配的內(nèi)存區(qū)域,所以根本不清楚到底是哪里,每一臺(tái)電腦的數(shù)據(jù)有差異)和 25。那個(gè) %x
就是輸出一個(gè)十六進(jìn)制數(shù)(詳見之前寫的“格式化字符串”)。
這樣說,你可能就明白了指針了。它是一個(gè)快捷方式,是一個(gè)變量的快捷方式,它也是一個(gè)變量,不過變量存的值并不是數(shù)字,而是和它形成對(duì)應(yīng)的變量的地址(可以理解成快捷方式本身其實(shí)也只是使用文件位置(路徑)封裝成文件的程序,它本身是一個(gè)空殼,本身沒有用途,每次雙擊它,都是找到路徑,然后打開對(duì)應(yīng)的程序)。
所以,一定要記清楚使用指針的符號(hào):取地址符號(hào) &
、取值符號(hào) *
和聲明指針變量(還是用 *
符號(hào))的三大用途。
那么,下面的寫法都是對(duì)的(只要順序執(zhí)行語句的話),用兩次星號(hào)表示是“二重指針”,即指針指向一個(gè)指針變量,指針變量再指向數(shù)值變量的存在。
輸入語句 scanf
用得非常少,在參數(shù)列表里,我們從字符串后開始寫上字符串里所有格式化字符串的對(duì)應(yīng)變量的序列,但需要賦值的是這些變量的所有地址,這是因?yàn)椋?/span>scanf
要求函數(shù)給定一個(gè)地址數(shù)值,來找到變量獲取元素信息,如果只有一個(gè)數(shù)字的話,就無法取得地址和數(shù)值的關(guān)聯(lián)。
雖說這一點(diǎn)也可以被設(shè)計(jì)成只需要傳入數(shù)值,然后直接放上去這樣的模式,但 scanf
函數(shù)在最初的設(shè)計(jì)就用的是指針,所以需要取地址符號(hào)來操作。不過,如果本身變量就已經(jīng)是一個(gè)指針變量了,這樣就不需要加入地址符號(hào),否則就會(huì)變?yōu)楫嬌咛碜愕男Ч?/span>&a
表示獲取 a
變量的地址,那么 &p
表示獲取 p
指針變量它自己的地址,這是沒有意義的。而針對(duì)于二級(jí)指針的話,我們肯定需要獲取的是它指向的變量 p
存儲(chǔ)的值(a
的地址數(shù)值),來找到 a
的位置,所以需要寫上的是 *q
,來取指向的變量 p
的存的內(nèi)容。
這一點(diǎn)有點(diǎn)繞,請(qǐng)你反復(fù)理解這段內(nèi)容。
指針的用途
那么,這樣復(fù)雜和難過的東西有什么用呢?
引用參數(shù)(ref 參數(shù))
還記得之前說到的函數(shù)的銷毀嗎?
當(dāng)我們交換變量時(shí),因?yàn)樵谡{(diào)用了變量交換,雖然值確實(shí)被交換了,但函數(shù)會(huì)被銷毀的緣故,最終的變量依然沒有發(fā)生任何變化:
這個(gè)時(shí)候我們使用指針,來改變地址,就改變了數(shù)值。
如果寫作指針,那么函數(shù)此時(shí)傳入的就不是簡(jiǎn)單的數(shù)值了,而是一個(gè)地址數(shù)值。
當(dāng)我們嘗試把 i
和 j
變量傳遞到 swap
里的時(shí)候,它此時(shí)并不是傳遞進(jìn)去了 1 和 5 兩個(gè)數(shù),而是這兩個(gè)變量自己的地址數(shù)值。
然后進(jìn)入 swap
函數(shù)開始交換。首先我們把 a
這個(gè)指針變量的指向變量數(shù)值取出,因?yàn)?a
傳入進(jìn)來的是 i
變量的地址,所以此時(shí) a
和 &i
可以說是等價(jià)的,所以取出 i
變量地址,然后取出這個(gè)地址上的數(shù)據(jù),所以就得到了 main
函數(shù)里那個(gè) i
變量的數(shù)值 1 了;然后把這個(gè) 1 賦值到 temp
里。
然后把 b
變量指向的變量數(shù)值取出,然后賦值到 a
變量指向的內(nèi)存區(qū)域上。左值 *a
可能現(xiàn)在還不好理解。你可以認(rèn)為,把取內(nèi)容符號(hào) *
放在等號(hào)左側(cè)的變量上,指代這個(gè)變量指向的那塊內(nèi)存(變量),然后嘗試把右側(cè)的數(shù)值放入到這個(gè)內(nèi)存(變量)里去。
然后最后一步就是把 temp
取出來,放入 b
指向的那塊內(nèi)存。由于過程里 a
和 b
分別指向的是 main
里的 i
和 j
,所以 swap
函數(shù)整體執(zhí)行完畢后,銷毀的也是 a
和 b
這兩個(gè)指針變量,即它們的指向是被銷毀了,但指向銷毀肯定是不影響到原本數(shù)據(jù) i
和 j
的,所以交換就起效了。
我們借用這個(gè)示例為大家解釋了如何使用指針傳參的模式來同步在兩個(gè)函數(shù)之間操作的過程,這種操作可以影響到另外一個(gè)函數(shù)里的變量數(shù)值,我們稱此時(shí) swap
里的指針變量(a
和 b
)為引用參數(shù)(有時(shí)候也叫 ref 參數(shù),ref 是 reference 的縮寫),這種參數(shù)跨 swap
和 main
引用是高級(jí)編程語言里的一種說法,它指代的是兩個(gè)指針變量同時(shí)指向相同的內(nèi)存區(qū)域,所以可以認(rèn)為兩個(gè)指針調(diào)用同一個(gè)變量的寫法格式除了變量名不同外,其它都一樣,所以稱為引用一致。比如
例如上述寫法里,
p
和q
都指向了a
變量。
和
使用的是完全一樣的書寫格式(
變量 = *指針
),而且指向同一塊內(nèi)存區(qū)域(同一個(gè)變量),所以我們認(rèn)為p
和q
具有一致的引用。而在上面 ref 參數(shù)里,我們使用的正是引用不變這一個(gè)點(diǎn),來從
swap
里修改到main
里的信息,因?yàn)榇藭r(shí)swap
的參數(shù)(指針變量)的引用是和main
函數(shù)調(diào)用swap
函數(shù)時(shí)傳入的參數(shù)是一樣的:&i
和&j
表示兩個(gè)地址數(shù)值,而在swap
的參數(shù)里,本應(yīng)該獲取的是兩個(gè)指針變量,但這里傳入的地址數(shù)值可以用來表示一個(gè)指針變量的信息。這就叫引用。不過,C 語言沒有引用一說,是因?yàn)橐貌⒉皇窍裆厦嬲f得這么簡(jiǎn)單,由于原理的實(shí)現(xiàn),它更類似于為變量取別名,顯然
p
和i
不是別名關(guān)系,它們差了一個(gè)地址符號(hào)。你不用理解這么詳細(xì),你大概知道,引用是啥樣的就可以了。
輸出參數(shù)(out 參數(shù))
再來考慮一個(gè)問題。我們?nèi)绻尫祷刂捣祷囟鄠€(gè)數(shù)值信息怎么辦?函數(shù)是只可能返回一個(gè)數(shù)值的,這是數(shù)學(xué)規(guī)定的,因?yàn)橐粋€(gè)函數(shù)(不考慮多值函數(shù)這個(gè)廣義理解下)是不允許有多個(gè)返回值的。C 語言沿用了這一點(diǎn),所以從函數(shù)返回值角度出發(fā),受到語義制約,C 語言是不允許返回多個(gè)返回值的。
那么,如果我們從參數(shù)返回呢?這是個(gè)好辦法,可是從參數(shù)返回,在函數(shù)銷毀的時(shí)候,變量不就跟著沒有了么?是的,所以我們需要指針來搞定。
考慮如下操作。我們嘗試對(duì)一個(gè)學(xué)生計(jì)算數(shù)學(xué)、英語、C 語言三個(gè)科目的成績(jī)的平均數(shù)值,并要求同時(shí)返回是否平均成績(jī)及格(一個(gè)布爾型 bool
數(shù)值或用一個(gè) int
表示)和三個(gè)科目的平均分。
這個(gè)題目難點(diǎn)在于需要同時(shí)返回是不是及格和平均分兩個(gè)數(shù)據(jù)。我們可以這么書寫代碼:
我們?cè)?IsPassOfAverage
函數(shù)里的唯一一個(gè)指針變量 avg
賦上了平均結(jié)果,然后返回了 avg
是否超過 60 分這個(gè)比較結(jié)果,而由于是一個(gè)指針的方式,在執(zhí)行完整個(gè) IsPassOfAverage
函數(shù)后,avg
會(huì)銷毀,但它是指針變量的關(guān)系,它指向被銷毀,但指向的那個(gè)變量并不會(huì)被銷毀。然后,執(zhí)行完畢前,計(jì)算結(jié)果就已經(jīng)得到了。所以輸出結(jié)果信息都是全部成立的。
這樣我們就模擬出了返回多個(gè)結(jié)果的要求。我們稱這里 avg
變量?jī)H用于輸出用,所以稱為輸出參數(shù)(有時(shí)候也稱為 out 參數(shù))。但別忘了,在計(jì)算平均數(shù)的時(shí)候要為 avg
(賦值語句的左值)添加取內(nèi)容符號(hào)。
還有其它的用法嗎?
實(shí)際上,指針并不只有這些用法,還有很多用法。不過今天作為入門指針的講解,我不打算放在這里說,下一部分內(nèi)容我們將會(huì)詳細(xì)分析和研究指針的運(yùn)算符,以及它和數(shù)組的關(guān)系。
參數(shù)修飾符
在函數(shù)傳遞參數(shù)的過程中,我們可以使用很多修飾符(一部分關(guān)鍵字)來告訴編譯器很多信息,豐富我們的使用。
const
修飾符
最難用的要數(shù)這個(gè)關(guān)鍵字了。這個(gè)關(guān)鍵字放置到參數(shù)上,可以讓每一個(gè)參數(shù)的信息只用來讀取,而執(zhí)行期間不允許修改這個(gè)變量的數(shù)據(jù)。
數(shù)值變量的修飾
考慮如下操作。如果我們嘗試把上面計(jì)算平均成績(jī)的代碼稍加改動(dòng)??梢钥吹饺齻€(gè)成績(jī)信息是沒有必要去改動(dòng)的,我們?yōu)榱苏Z法約定,可以增加 const
修飾符來保證這些數(shù)值可以通過不被修改的方式完成執(zhí)行邏輯。
這樣我們就無法在代碼里修改這些變量了。
指針變量的修飾
當(dāng)然,這樣的操作也可以修飾指針變量。如果一個(gè)傳入的指針的數(shù)值(即地址)不用修改,可以嘗試為變量修飾 const
。
這樣就可以保證,我們傳入的指針變量的地址信息不被修改。
指針變量的全只讀
const
是很靈活的關(guān)鍵字??梢钥吹?,指針是暗含地址信息和指向變量的信息兩個(gè)內(nèi)容的。如果我們修飾的是變量本身,那么這個(gè)變量本身的內(nèi)容就不可以被修改。針對(duì)于數(shù)值變量而言就是它自己的數(shù)值不能修改,而針對(duì)于指針變量就是它存入的地址不可以被修改(即指向不能變)。但此時(shí),指向的變量的內(nèi)容是可能變化的。
如果我們甚至想要保證指向的變量的內(nèi)容也不可以改變,那么還需要在指針符號(hào) *
和變量名之間插入 const
關(guān)鍵字來保證。當(dāng)然,這一點(diǎn)不好舉例,這里簡(jiǎn)單考慮如下寫法。
const
的位置
可以看到,這個(gè)修飾符放在很多地方都可以,所以考慮如下四種寫法:
const int *p
int const *p
int *const p
const int *const p
這四種寫法都是合理和允許的。不過,前面兩種是等價(jià)的,因?yàn)樗鼈兌紱]有放在定義符號(hào)后,所以只能保證指向不變(自身存儲(chǔ)的數(shù)值,即地址信息不變)。而第三種只能保證內(nèi)容不可以被修改,但指向是可以修改的(這一點(diǎn)不好理解,但被允許,請(qǐng)盡量少這么用它)。第四種則是都不可以變。
restrict
修飾符
在前文我哦們提到了引用參數(shù),這種參數(shù)利用了“引用一致”的說法,來達(dá)到影響原數(shù)據(jù)的效果。
a
,篡改了數(shù)據(jù)后,p
和 q
雖然指向不變,但最終 a
的數(shù)值發(fā)生變化后,p
和 q
由于執(zhí)行到 *b = 6
的時(shí)候,它們操作的可能是同一個(gè)內(nèi)存(同一個(gè)指向的變量),所以返回值還真不一定是預(yù)期的 11(可能因?yàn)?a
和 b
指向同一個(gè)變量而導(dǎo)致結(jié)果是 12)。
但,如果我們預(yù)期就可以認(rèn)為和保證,這兩個(gè)變量不應(yīng)是同一個(gè)指向的話,我們就為指針和變量名中間添加 restrict
修飾符,來保證它們肯定不能指向同一個(gè)變量:
總之,restrict
關(guān)鍵字修飾在變量名和指針定義符號(hào) *
之間,可以表示這些指針指向的變量一定不同,進(jìn)而可以把一些執(zhí)行語句按照規(guī)范調(diào)整執(zhí)行順序,來達(dá)到減少執(zhí)行步驟,以優(yōu)化的目的。
這個(gè)關(guān)鍵字在 C99 標(biāo)準(zhǔn)后才可以用。
還有其它的嗎?
其實(shí),還有 static
等等修飾符,但這里不作闡述,因?yàn)樗鼈兌汲V了。
NULL
常量
在 C 語言里甚至為我們規(guī)定了指針的默認(rèn)值。之前學(xué)到的很多數(shù)據(jù)類型都具有自己的默認(rèn)數(shù)值,比如 int
的默認(rèn)數(shù)值是 0,而 bool
類型的默認(rèn)數(shù)值是 false
,字符的默認(rèn)數(shù)值是 \0
(講解字符和指針一節(jié)的時(shí)候會(huì)說明這一點(diǎn))。指針也具有默認(rèn)數(shù)值。不過,因?yàn)橹羔樁际且粋€(gè)地址信息,所以不論指針的指向元素類型,它們統(tǒng)一都有一個(gè)默認(rèn)數(shù)值是 NULL
。注意,這個(gè)默認(rèn)數(shù)值是全部字母都大寫的,而且它的意義是指向 0 號(hào)內(nèi)存的地址數(shù)值。好比是把數(shù)值進(jìn)行“數(shù)值變指針的強(qiáng)制轉(zhuǎn)換”。
而具體如何使用它,我們將在內(nèi)存分配里才會(huì)講到。