第 17 講:指針和字符
前文我們提到了近乎所有的指針的用法,不過指針和數(shù)組用起來確實(shí)挺難的,今天要說到的知識(shí)點(diǎn)是字符串、字符數(shù)組和指針三者的關(guān)系。
什么是字符?
在之前,我們對字符提到的內(nèi)容很少,所以我們這里統(tǒng)一為大家介紹它們。
字符是以一個(gè)單引號(hào)包含起來,以區(qū)分?jǐn)?shù)值字面量的存在。比如下面的這個(gè)寫法:
這樣就表示 c
變量是一個(gè)字符類型的變量,存儲(chǔ)的元素是一個(gè)字符 1
。這個(gè)字符 1
和普通的 1 不同,字符 1
一般用于輸出才用到,而數(shù)字 1 則可以操作和計(jì)算。
在 C 語言里,用單引號(hào)把這些符號(hào)引起來可以和數(shù)字作出區(qū)分。但為了靈活使用,字符有時(shí)候也可以和數(shù)字進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換關(guān)系是通過一個(gè)叫做 ASCII 碼表來搞定的。比如,在這個(gè)表里規(guī)定字符 A
的對應(yīng)整數(shù)是 65,a
是 97,而字符 1
對應(yīng)數(shù)值則是 49。
這樣定義,來區(qū)分字符是有意義的。在計(jì)算機(jī)處理這些字符的時(shí)候,為了可以明確表達(dá)數(shù)字的行為是計(jì)算,而字符的行為是輸出,所以 C 語言發(fā)明了類型這個(gè)體系,來區(qū)分字符和整數(shù),所以字符 1
和數(shù)字 1 具有截然不同的用法,所以寫法和聲明(定義)語句上的賦值也是有不同的地方的。
如果你書寫的方式是這樣:
i
這樣只能表達(dá) c
變量表示的字符是 ASCII 碼表里編號(hào)為 1 的那個(gè)字符。
使用字符和數(shù)字進(jìn)行輸出
在前文里,我們提到過數(shù)字的輸出模式。如果是整數(shù),輸出則使用 %d
的格式化字符串,它會(huì)自動(dòng)幫我們改為字符串的形式書寫出來:
比如上面的這個(gè)寫法,顯然第一個(gè) 0 是沒有意義的,但用整數(shù)輸出的時(shí)候,你也就看不到這個(gè) 0 了,而只有 12
這個(gè)字符串。這就是數(shù)字的輸出模式。
字符的輸出使用的是 %c
這個(gè)格式化字符串。
1
它就好比是
1
,因?yàn)?1
這樣才可以得到一個(gè)字符 1
。
字符串
字符串(也經(jīng)常被簡稱為串,string),是字符的一組序列,即由多個(gè)字符構(gòu)成的這個(gè)序列,稱為字符串。它的表現(xiàn)形式有兩種,一種是字面量字符串,一種是字符數(shù)組。我們都來說一下,以及它們的使用。
字面量字符串
字符可以用字面量的形式呈現(xiàn),也可以用數(shù)字呈現(xiàn),這表現(xiàn)出字符的兩種形態(tài)。字符串也是這樣。第一種呈現(xiàn)形式是通過雙引號(hào)的形式出現(xiàn)的。比如 "Hello, world!\n"
就是一個(gè)合格的字面量字符串寫法,用雙引號(hào)把所有原封不動(dòng)的字符序列包括起來。
不過,字面量字符串如何賦值和輸出呢?賦值的話,它的類型應(yīng)該是什么呢?
之前說過,字符串是一系列字符,所以它肯定不是單純的 char
類型了。實(shí)際上,它可以使用字符指針來表示,也可以用字符數(shù)組表示:
C 語言允許第一種寫法,讓 char
類型的指針(干脆就叫 char *
類型)來“接收”這個(gè)字符串,這樣的話,這個(gè)指針將會(huì)指向這個(gè)字符串的首地址(和數(shù)組一致,指向數(shù)組的第 0 號(hào)元素,即 charPointer
本身就等價(jià)于 &charPointer[0]
)。這種表示方式的優(yōu)勢是,不用去數(shù)和關(guān)心字符串到底有多長。
第二種寫法也是允許的,C 語言會(huì)自動(dòng)把這個(gè)字符串拆解為一個(gè)字符數(shù)組,然后挨個(gè)存放進(jìn)去,最后得到這個(gè)數(shù)組的首地址。所以第二種寫法下,charArray
變量名依舊等價(jià)于數(shù)組的首地址。
不過,需要你額外注意一點(diǎn)。它和普通元素類型的數(shù)組不同,字符串會(huì)被處理成字符數(shù)組的形式存儲(chǔ),然后返回的首地址可以用字符指針變量名或字符數(shù)組變量名接收。但是,字符串會(huì)自動(dòng)為末尾添加一個(gè)結(jié)束標(biāo)記字符 \0
。這個(gè)字符是計(jì)算機(jī)內(nèi)表達(dá)這個(gè)字符的寫法,輸出并非是一個(gè)反斜杠和一個(gè)數(shù)字 0,而是啥都沒有。
\0
這個(gè)字符僅用于標(biāo)記字符串的結(jié)尾用,沒有其它的功能。而如果你非要把它當(dāng)整數(shù)形式表示的話,它在 ASCII 碼表里對應(yīng)的整數(shù)也就是 0。所以 0 對應(yīng) \0
這個(gè)字符。
所以,這個(gè)知識(shí)點(diǎn)告訴我們,Hello, world!
這個(gè)字符串整體長度是 14,除了字面量給定的 10 個(gè)英文字母、2 個(gè)符號(hào)、一個(gè)空格以外,還默認(rèn)在末尾帶有一個(gè)字符 \0
標(biāo)記,所以這個(gè)字符串長度是 14,并不是 13;而在第二種賦值格式里,這個(gè)中括號(hào)里就應(yīng)該是 14,而不是 13。
輸出字面量字符串的方式可以用 %s
的方式輸出:
s
這個(gè)字符串的所有內(nèi)容。另外,這樣書寫每次都必須寫上 %s
,所以你可以改寫為這樣:
即直接使用 puts
函數(shù)來輸出一個(gè)字符串。
字符數(shù)組
顯然,字面量字符串會(huì)被處理為字符數(shù)組,所以其實(shí)還可以書寫為字符數(shù)組。
但請注意,如果寫成字符數(shù)組,系統(tǒng)就不會(huì)自己給你補(bǔ)充 \0
字符了。所以這種寫法必須自己補(bǔ)充一個(gè) \0
符號(hào)在字符數(shù)組的末尾。
當(dāng)然,你把 \0
寫在字符數(shù)組的中間某處也可以,不過這個(gè)字符串就會(huì)從這個(gè)地方截?cái)啵?/span>
這樣的話,這個(gè)字符數(shù)組長度就是 7 而不是原本的 13 或者 14 了。也就是說,這個(gè) s
接收到的完整的字符數(shù)組序列到第一個(gè) \0
就結(jié)束了,后面的內(nèi)容都不在 s
里。不過在內(nèi)存里,這些字符確實(shí)是挨著存儲(chǔ)的,不過你必須通過語法的 bug 越界訪問數(shù)組,才看得到它們了。
同樣地,字符數(shù)組也具有退化賦值的特性,所以我們依舊可以使用這一點(diǎn),把字符數(shù)組賦值給一個(gè)指針。
字符串的使用
講完了兩種字符串的書寫格式后,我們來說明一下字符串的基本使用。
考慮如下實(shí)例。假設(shè)我們在寫一個(gè)代碼,輸出一個(gè)數(shù)字是否是質(zhì)數(shù),代碼大部分已經(jīng)寫好:
這就是前面的例子,只是我為了邏輯清晰,把 isPrime
從 int
改為了 bool
類型(只是這么做就只能讓代碼運(yùn)行在允許 C99 標(biāo)準(zhǔn)的地方了)。
現(xiàn)在,考慮輸出語句。可以從輸出語句里看到,它們大部分輸出信息都是相同的,只是多了一個(gè) not
?,F(xiàn)在我們可以考慮把代碼寫成一個(gè)三目運(yùn)算符的方式:
?printf("%d is %sa prime.\n", digit, isPrime ? "" : "not ");
注意輸出語句里有一個(gè) %sa
,其實(shí)它是 %s
和一個(gè)字符 a
而已。它想要表示的是,某某數(shù)字,是(或不是)一個(gè)質(zhì)數(shù),這樣的輸出格式。當(dāng) isPrime
為 true
的時(shí)候,我們就不需要 not
這個(gè)單詞的修飾,所以這個(gè)輸出一個(gè)空的字符串(只有一個(gè) \0
,反正里面其它東西都沒有,到時(shí)候計(jì)算機(jī)會(huì)幫我們作出處理);否則加一個(gè) not
修飾符來表示它”不是“質(zhì)數(shù)。
常見字符串函數(shù)
我們經(jīng)常要使用到一些字符串的處理模式,比如提取字符串里其中一部分字符、求長度等等。下面就來看看它們。
求字符串長度
我們需要實(shí)現(xiàn)一個(gè)函數(shù) strlen
,獲取一個(gè)字符串的長度。這個(gè)算法比較容易實(shí)現(xiàn):
可以從代碼里看到,這就是一個(gè)基本的計(jì)算方式。首先我們定義一個(gè)結(jié)果 count
變量來表示一共多少字符,然后一個(gè)指針變量指向它的第一個(gè)元素,這樣一會(huì)兒我們就可以使用 ++
將變量移動(dòng)指向,讓其指向下一個(gè)字符信息。然后最終當(dāng) cur
指向 \0
的時(shí)候,表示字符統(tǒng)計(jì)完畢,跳出 while
循環(huán),返回結(jié)果 count
即可。
另外,參數(shù)
str
在過程里都不用修改變量的本身指向和指向的內(nèi)容,所以我們可以對str
使用const
修飾。
這個(gè)算法可以改寫為這樣:
使用 for
循環(huán)就會(huì)簡單不少。另外,cur
指針如果不是指向 \0
字符就一直遍歷,這個(gè)條件是 *cur != '\0'
,而前文說過 \0
的 ASCII 碼表編碼數(shù)值是 0,所以就相當(dāng)于 *cur != 0
,而這種寫法在條件判斷一節(jié)的時(shí)候說過,它等價(jià)于不寫 != 0
部分,所以最終可以直接寫成 *cur
來表示 *cur != '\0'
。另外,str
就等價(jià)于 &str[0]
,所以初始賦值也可以不要地址符號(hào)和索引器部分 [0]
。
取出字符串的一部分字符
嘗試寫出一個(gè) strstr
函數(shù),來獲取某個(gè)字符串里指定起始點(diǎn)和長度的其中一部分字符。
首先判斷輸入的所有參數(shù)是否都符合條件。如果取出的長度 length
比 0 還小,或者比字符串長度還大,是不滿足要求的;同時(shí),如果獲取元素的起始索引 startIndex
比 0 小,或超過字符串長度,也依然是無效的。所以這個(gè)時(shí)候我們給用戶輸出一個(gè)空字符串,來表示錯(cuò)誤(當(dāng)然你也可以給它另外的東西,或者執(zhí)行指定的某個(gè)行為來表示輸入信息錯(cuò)誤)。
否則,我們嘗試讓 ptr
指向 str[startIndex]
處的元素,然后不斷更新 last
和 ptr
的數(shù)值。當(dāng) last
減到 0 的時(shí)候就是獲取結(jié)束的時(shí)候。
最后得到結(jié)果后,我們把 ptr
指向的字符改寫為 \0
表示從這里截?cái)?,表示這里是字符串的結(jié)束。最后返回 str[startIndex]
的地址就可以了。
當(dāng)然你也可以改寫一下:
const