第 16 講:數(shù)組和指針
上文我們基本上了解清楚了指針的基本用法和操作,不過,光是變量操作的指針模型可能過于單一,所以今天我們要看的是數(shù)組和指針的用法。
一維數(shù)組和指針
首先講一下簡單的一維數(shù)組和指針。首先你要明白一個東西,數(shù)組它其實除了是一系列數(shù)據(jù)的統(tǒng)一管理的集合外,它還有一個用的方式:指針形式。
我們可以使用指針,將數(shù)組改變?yōu)橹羔?,然后對變量名操作來達(dá)到操作內(nèi)部數(shù)據(jù)的方式。比如之前我們講到的數(shù)組的聲明和使用。我們先來看一下基本的使用數(shù)組的方式。
這樣寫起來很輕松,不過呢,我們除了使用中括號來找出數(shù)據(jù)外,還有一種寫法:
注意看清楚,*p
和 b
變量,一個是指針變量,另一個則是一個普通的存數(shù)值的變量。我們再看右側(cè),a
只是一個數(shù)組名啊,為什么數(shù)組名字還可以加一個 2 和 3 呢?
這是因為,數(shù)組名本身除了表示它是一個數(shù)組的名字外,它也是一個指針,且這個指針指向 a[0]
。換句話說,數(shù)組名除了表示數(shù)組的名稱外,它還是一個指針變量,這個指針的位置上存的數(shù)據(jù)是 a[0]
的地址。
那么,對于地址加 2 來說,我們就很清楚了。因為數(shù)組是順序存儲數(shù)據(jù)的,每一個數(shù)據(jù)都是挨著放的,所以加 2 的話,自然就表示 a[2]
了。不過,a + 2
是真的 a[2]
嗎?當(dāng)然不是。剛說到,a
是指針,所以加了 2,還是一個指針,這個性質(zhì)是沒有變的。不會因為它是指針,加了數(shù)值之后變成了數(shù)值。所以 a + 2
其實是 &a[2]
。換句話說:
p
和 q
p
和 q
也是一樣的,只要沒有找出數(shù)組存儲的范圍(如果數(shù)組只有 5 個元素,那么這里的 C
就最好別超過 4,如果 C
= 5 甚至大于 5,都會出現(xiàn)超出訪問數(shù)組的范圍的隱藏 bug)。
所以,為了取出數(shù)值,還需要取值符號 *
。所以相當(dāng)于這樣:
看懂上面的寫法了嗎?那么現(xiàn)在來做個等效代替:
因為取地址和取值是一對相反的運(yùn)算,所以可以同時去掉。
另外,它還可以寫作
你可以理解一下,這一點是為什么。
一維數(shù)組配合自增自減運(yùn)算符
一個復(fù)雜但重要的例子
我們來看一下,指針變量在配合自增自減運(yùn)算符后的意思的理解。我們先來看示例和運(yùn)行后的結(jié)果,來理解它們。
這里提到了七個不同的語句寫法:
*p++
(*p)++
*(p++)
*(p+1)
*p+1
*++p
*(++p)
最后發(fā)現(xiàn)數(shù)值除了一個數(shù)有些奇怪(*t + 1
)外,其他都還算比較正?!,F(xiàn)在我們來一個一個理解。
解釋
第一個為什么是 1 呢?因為 *p++
的自增符號在后,所以先不會改變值,而是先被取值。p
最開始指向了 a
,就表示語句 p = &a[0]
。由于自增符號在后,所以輸出的應(yīng)為 a[0]
,即 1。
第二個為什么還是 1 呢?因為剛說過了,自增符號在后,所以打不打括號其實都是先取值。所以輸出的值也是 1。
第三個為什么是 2 呢?好像怎么看,自增運(yùn)算符都不執(zhí)行啊,即使有這個括號。這其實是因為,剛才 (*q)++
式子最開始取出了值,但因為自增在后,所以輸出原來的 1,但后面的自增符號使得 a[0]
從 1 變?yōu)榱?2。所以 *(r++)
雖然打了括號,但由于在后的關(guān)系,數(shù)值不會發(fā)生改變,但 a[0]
已經(jīng)變?yōu)榱?2,所以自然取出數(shù)值時,就是 2 了。
第四個則比較好理解,*(s + 1)
自然取的是 a[1]
的值,故為 6。
第五個,這里括號不見了,可是為什么變成3了呢?*t + 1
語句下,是先取了 a[0]
的值,然后對值再加 1,所以自然是 2 + 1(注意是 2 哈,剛才已經(jīng)改為 2 了,不要帶 1 進(jìn)去)。
第六個,*++u
,自增符號在前,所以自然是對原本指向的 a[0]
往后移動一位,變?yōu)橹赶?a[1]
。或者換句話說,++u
其實也就是 a + 1
,或者是 &a[1]
。所以加星號表示取其值,所以是 a[1]
的值,也就是 6 了。
第七個,和剛才不加括號是一樣的效果。
可能你會問第一個,為什么 *p++
和 (*p)++
不同。我是這么解釋的:*p++
是相當(dāng)于 *(p++)
的,這個自增符號不管在哪里,始終要記得,因為它挨著變量,所以它是先會被打括號的;而取值符號 *
,不一定會直接挨著變量,所以有時候并不會先計算。所以我們有一個結(jié)論:*p++
和*(p++)
一樣,*++p
和*(++p)
一樣。剛才我們在計算第六個的時候,就知道了,自增是挨著變量的,不論前后的關(guān)系,既然挨著,就肯定是先計算的咯。
所以,優(yōu)先級什么的都喂狗去吧!根本不需要去死記硬背的。
從中我們可以總結(jié)以下內(nèi)容:
*p++
等價于*(p++)
等價于*((&a[0])++)
等價于*(&a[0]++)
等價于兩個語句a[0]; &a[1];
,自增符號在后,顯示數(shù)據(jù)時,自增符號不會起作用,即使使用完畢后,因為后面是&a[1]
,對后續(xù)變量的操作也沒有任何影響(指針變動而不是數(shù)值變動),所以并沒有用。*++p
等價于*(++p)
等價于*(++(&a[0]))
等價于*(++&a[0])
等價于*(&a[1])
等價于a[1]
,自增符號在前,取a[1]
。*p + 1
等價于*(&a[0]) + 1
等價于a[0] + 1
。(*p)++
等價于(*(&a[0]))++
等價于(a[0])++
等價于a[0]++
,即a[0]
使用完畢后自增 1。
最后考一個問題:
看一下,注釋表示當(dāng)前位置輸出的數(shù)值。請嘗試?yán)斫庖幌聻槭裁础?/span>
二維數(shù)組和指針
二維數(shù)組是既有行又有列的數(shù)組,所以相當(dāng)于行、列均有數(shù)組的指針。
和剛才一樣,我們先定義一個數(shù)組。
然后,這個 a
變量除了是數(shù)組的變量名外,還是一個指針。不過因為是二維的,就稍顯麻煩。
我們可以嘗試將這里的a理解成一個二重指針。它是指向指向數(shù)值的指針的指針。很繞對吧~
其實是這個意思:
因為說的是二重指針,所以在聲明指針變量時,需要兩個星號。它等效于這句話:
首先我們使用一重指針指向行上的位置,因為數(shù)組名的緣故,所以指針指向的是最初的位置 a[0]
;但是因為 a[0]
在二維數(shù)組下并不是一個數(shù),而是一系列數(shù)據(jù)(可以看作是一個數(shù)組,數(shù)組內(nèi)各自又有一個單獨(dú)的數(shù)組),所以,&a[0]
實際還不夠,還要取出列下標(biāo)為 0 的情況,所以有兩層括號和兩層取地址符號。
那么,普通的指針可以這么表示:
a[0][1]
*a
表示是 a
數(shù)組的第一行的所有數(shù)據(jù),即 &a[0]
,所以上述語句可以等效為
但是依然不夠。因為 a[0]
我們剛才說過,a[0]
是一組數(shù)據(jù),雖然光禿禿在這里寫著,沒有地址符也沒有取值符,但始終記得它相當(dāng)于是一個數(shù)組名叫 a[0]
。
然后 a[0] + 1
表示將列往后移動一位,即相當(dāng)于一維數(shù)組的指針 a + 1
這樣的存在。
所以這里就表示為了 &a[0][1]
??蔀槭裁催€帶個指針呢?因為剛才就說過,一維數(shù)組的指針就帶有取地址符號,即 a + 1
等效于 &a[1]
,所以自然二維數(shù)組的指針也有這樣的符號了,故 a[0] + 1
就表示 &a[0][1]
了。所以取出值還得需要一個取值符號,故 *(&a[0][1])
就變?yōu)榱?a[0][1]
了。
所以上述語句就變?yōu)椋?/span>
明白意思了嗎?
所以第 i 行第 j 列的數(shù)值用指針表示為
三維數(shù)組?
三維數(shù)組也是,只是指針變?yōu)槿刂羔樍T了,只是就不多闡述了。它的表示可以類比這個推導(dǎo)出來:
把數(shù)組賦值給指針的退化賦值形式
前文提到了很多有關(guān)數(shù)組的聲明(定義)和賦值方式。不過數(shù)組是可以賦值給一個指針的。比如下面這樣:
這樣表示出來的 series
是一個指針,它依然指向的是 series
賦值的這個整型數(shù)組的首元素地址。所以這種方式幾乎可以認(rèn)為是等價于 int series[] = { 1, 3, 5, 7, 9 };
,唯一的區(qū)別是,數(shù)組格式的聲明可以為中括號指定元素總數(shù)(5),而指針形式則不可能這么寫了。
我們常把數(shù)組賦值給指針的這種賦值行為被稱為退化賦值(Degenerated Assignment),因為它損失了數(shù)組長度這一個信息。不過,這種退化賦值卻在 C 語言里使用得非常廣泛。下面要講到的數(shù)組傳參就是典型的其中一種使用方式。
數(shù)組傳參
數(shù)組是可以用于傳參的。換句話說,數(shù)組是可以當(dāng)形式參數(shù)(形參)傳遞的。不過,古老的 C 語言只允許數(shù)組的指針寫法傳遞,而且還有著另外的寫法。因為這里用的類型不多,所以只講解一維和二維數(shù)組的傳參。
不過在此之前,我們要搞清楚數(shù)組指針和指針數(shù)組的區(qū)別。這個名字有些繞,我們不用管名字,看它怎么用。
數(shù)組指針和指針數(shù)組
首先我們來看以下兩種寫法:
p[20]
換句話說,我們借用本文最開頭的第三種理解思路:
其實就看得很清楚了。它是一個數(shù)組,有 20 個元素組成。不過這個數(shù)組全存放的是指針罷了。
這種數(shù)組稱為指針數(shù)組(存放指針的數(shù)組)。
下面這個呢?被打了括號,所以完全不同。
因為括號的關(guān)系,星號不可被拆開。所以它是什么呢?它是一個可以指向一個數(shù)組的指針。換句話說,如果我有100 個數(shù)據(jù),被認(rèn)為劃分為了 5 個數(shù)組,每一個數(shù)組都有 20 個元素,并且它們是隨便劃分的,所以毫無關(guān)聯(lián)。
這個時候,我們可以讓這個指向數(shù)組的指針(即數(shù)組指針)分別去指向這不同的 5 個數(shù)組,來測試這些數(shù)據(jù)。
有了這些基礎(chǔ)的理論后,我們來看一下示例。
一維數(shù)組傳參
首先我們試想一個場景,求一個數(shù)組下的最大數(shù)值。這個時候,我們可以這么做:
這樣,一個數(shù)組的最大值的算法就寫好了。接下來我們來看一下,指針寫法(因為 C 語言要用數(shù)組的指針寫法來傳遞形參)。
所以首先,我們要把上面的 a[]
這個詭異的中括號里值都沒有的寫法給它改掉,改為指針寫法:
就可以了。所以在聲明時,按照可以去掉變量名稱的手段,我們可以將剛才的程序的完整版寫成這樣:
當(dāng)然,max
內(nèi)的 a[i]
等使用到數(shù)組的地方的,也可以改寫為指針寫法。當(dāng)然,這里不改也沒有關(guān)系。
注意數(shù)組傳參的過程,直接把數(shù)組名稱寫上去,而不要地址符號,也不要中括號,因為它傳遞的是一個地址數(shù)值,表示
&a[0]
。
&a[0]
、a
和 &a
?
或許有小伙伴會對一維數(shù)組的指針三種寫法提出問題:如果我們把數(shù)組命名為 a
的話,它同時等價于首元素的地址,即 &a[0]
,那么,&a
表示什么?按照道理來說,&a
表示的是這個數(shù)組變量本身的地址,而這個變量的地址不應(yīng)該就是 &a[0]
嗎?所以這么說來的話,a
、&a
和 &a[0]
三者不是就完全一樣了嗎?
我想告訴你的是,這個說法目前來說確實是正確的。我們也確實經(jīng)常寫 a
來表示首元素地址,也有時候會用 &a
來“嚴(yán)謹(jǐn)?shù)亍北磉_(dá)數(shù)組變量本身的地址,而由于數(shù)組變量本身而言,從這里開始存放的一系列元素就是這數(shù)組里的東西,所以 &a
和 a
這么看是等價的。不過……
在使用 +
和 -
的運(yùn)算符的時候,就可以看出它們的區(qū)別。
來看這個執(zhí)行邏輯,x
、y
和 z
都得到什么結(jié)果?第一個是把數(shù)組地址加上 1,它表示往下移動 1 個單位,所以等價于 &a[0] + 1
,即 &a[1]
;而 z
也就是這個結(jié)果。那么中間的 y
呢?它其實表示的是,以數(shù)組整體大小為單位,往下跨越一個完整大小的空間,所以它一口氣就相當(dāng)于往后移動到指向不存在的 a[4]
上去了。所以,請不要這樣使用指針來操作數(shù)組,會立馬超出訪問范圍,出現(xiàn)致命的 bug。
二維數(shù)組傳參
二維數(shù)組依然要取出最大值,就不容易了,因為它有兩個維度。于是我們先要改寫上面的 max
函數(shù)。
注意這里,傳參的寫法,是一個數(shù)組指針,即指向數(shù)組的指針。這一點也很好理解。因為二維數(shù)組本身應(yīng)當(dāng)在處理成指針寫法時可以改為二重指針,但因為傳參的緣故,它的行列都可以變動,所以我們不能在處理函數(shù)時,行、列同時變動,即必須是“一靜一動”,不然程序就不受控了。所以我們要保證其中一個維度是靜止的(即數(shù)組形式),而另外一個維度則用指針來操作,就很輕松。所以寫成了數(shù)組指針寫法。剛才也告訴大家,數(shù)組指針是一種指針,不過這個指針是指向一個數(shù)組而不是一個數(shù)值的。當(dāng)改變指針的值時,也就相當(dāng)于移動了指針,達(dá)到測試和使用二維數(shù)組之中“一個維度下的數(shù)組”的操作。
那么它的聲明就省略變量名即可,但是因為只能省略變量名,所以括號是不能少的。
令人遺憾的返回數(shù)組的函數(shù)
聽說你想返回數(shù)組?
我們有時候也嘗試把一個數(shù)組作為返回值返回到外界來,但很遺憾的是,我們無法把返回數(shù)組的函數(shù)寫成符合我們原本理解的可能寫法:
或
這兩種寫法都是不允許的。所以我們?nèi)绾伪WC返回一個數(shù)組呢?
返回數(shù)組只能使用前文提到的退化賦值的方式。如果你有一個數(shù)組需要返回,我們只能寫成指針的方式返回,所以實際上的寫法是這樣的
這種寫法沒有問題,不過,很遺憾的是,它少了一個提示信息,即數(shù)組的長度,這樣返回的數(shù)組很容易出現(xiàn)越界的問題,所以你可能還會在這種函數(shù)里加上一個 out 參數(shù)表示數(shù)組長度。
不過…… 還有一個遺憾的地方。在 C 語言里,所有在函數(shù)里聲明的變量都是跟著棧內(nèi)存走的。這有什么意義呢?跟著棧走的變量聲明,會在函數(shù)執(zhí)行完畢后自動銷毀,所以,如果你嘗試返回一個函數(shù)內(nèi)定義出來的數(shù)組的話,只能很遺憾地通知你,在函數(shù)執(zhí)行完畢后,這塊內(nèi)存會被銷毀,所以你返回的這個指針其實是銷毀前的這個結(jié)果的這塊內(nèi)存,但它目前已經(jīng)被銷毀。
所以,目前我們只靠前文學(xué)到的知識點還無法做到不銷毀這些自己聲明和定義的內(nèi)容。等我們接觸到結(jié)構(gòu)體和堆內(nèi)存的時候,我們才會提到了。
總結(jié)、懸空指針和野指針的概念
所以,這一節(jié)的所有文字其實就想告訴你兩個點:第一,返回數(shù)組的函數(shù)是做不到的。非得返回數(shù)組,你也只能退化賦值,返回一個指針;第二,返回的數(shù)組對象的這個指針,即使被成功返回了,也逃不過函數(shù)銷毀時立刻銷毀內(nèi)部定義的所有變量的厄運(yùn)。所以,不要嘗試在函數(shù)里聲明(定義)一個數(shù)組,然后用指針形式返回這個數(shù)組(的首地址),這樣依然會失敗。而且,這個指針由于失去了原本指向的內(nèi)容,所以這個時候這個對象還被稱為懸空指針(Dangling Pointer)。
另外,還存在一個說法,叫做野指針(Wild Pointer),它表示一個沒有初始化的指針變量。這個指針由于沒有初始化,就和普通變量一樣,它內(nèi)部存儲的值是不明的。不過由于它是指針的關(guān)系,內(nèi)部的數(shù)值很可能是隨機(jī)的,所以這意味著這個指針表示的意思就是“隨機(jī)指向”。這是一個很可怕的概念。隨機(jī)指向很有可能破壞電腦。
static
修飾數(shù)組參數(shù)
當(dāng)數(shù)組傳入函數(shù)里的時候,我們常??梢詫@個函數(shù)的參數(shù)加以修飾。在 C99 標(biāo)準(zhǔn)里,我們甚至可以對一個數(shù)組函數(shù)的長度作出修飾。
我們?nèi)绻褦?shù)組參數(shù)用數(shù)組格式書寫到參數(shù)上的時候,我們可以使用 static 數(shù)字
的方式來說明,這個數(shù)組參數(shù)傳入的元素總數(shù)最多只能有 10 個。當(dāng)然,這個用法有時候很有用,但有時候很雞肋。
指針的運(yùn)算
在 C 語言里,我們?yōu)橹羔樢蔡峁┝朔奖愕倪\(yùn)算符,使得我們可以通過運(yùn)算符來操作和計算內(nèi)存地址。
上文我們其實已經(jīng)用到了一部分這樣的運(yùn)算符,諸如 ++
、--
這些符號,下面要提到的是這樣的一些運(yùn)算符:
+
:計算兩個地址的和,或是為一個地址增加偏移量。比如a
是一個數(shù)組,a + 3
將會把當(dāng)前地址往后移動 3 個元素單位。-
:和上面的算法相反,這里是減法。*
:將地址數(shù)值相乘。這個用法很少。/
:將地址數(shù)值相除。這個用法也很少。%
:把兩個地址數(shù)值以整數(shù)方式執(zhí)行取模運(yùn)算。這個用法也很少。
distance
是直接把兩個指針相減,得到的是 p
和 q
它們兩個在內(nèi)存里相差的距離,而 valueDifference
表示的是 p
和 q