第 26 講:指針(三):指針和數(shù)組
前文我們說過了堆內(nèi)存和棧內(nèi)存的基本存儲姿勢,現(xiàn)在我們說一下對于指針相關(guān)的數(shù)組操作。
Part 1 棧內(nèi)存數(shù)組
什么?數(shù)組元素少?是的,那么我們是可以允許使用別的語法來完成這一點的。我們使用關(guān)鍵字 stackalloc
來創(chuàng)建棧內(nèi)存的數(shù)組。
比如這個例子里,我們使用 stackalloc int[10]
來創(chuàng)建一個長度 10 的棧內(nèi)存數(shù)組。稍微注意兩個地方。第一個地方是,stackalloc
替換為 new
的時候,和原始的創(chuàng)建數(shù)組的寫法是一樣的;第二個地方是,左側(cè)變量 arr
的類型是 int*
而不是 int[]
。這一點可能你需要注意了。因為棧內(nèi)存分配的結(jié)果必然是一個指針表達的數(shù)組,因此必須用指針表示。
另外,在 C 語言里我們知道,數(shù)組和指針基本上沒有啥大的區(qū)別,因此索引器 []
可以用在指針上。比如 a[1]
和 p[1]
都是可以的語法。
stackalloc
和 new
有一點不一樣的是,stackalloc
反饋的是一個指針,因此我們不能使用初始化的語法,即那個大括號,寫初始數(shù)值的列表,你必須寫成這樣:
比如這樣。
數(shù)組是長條形的存儲結(jié)構(gòu),那么我們怎么通過指針來取元素呢?在講數(shù)組的指針操作之前,我們先來說一下指針的加減法。
我們定義的指針變量:
注意 ptr
的初始化語句。我們使用 arr + 1
表示的是取 arr[1]
元素的地址。C# 里,我們定義兩種運算:
arr + n
表示取arr[n]
的地址,即等價于&arr[n]
;arr - n
表示取arr[-n]
的地址,即等價于 。
可能減法不是很好理解。你將這里 arr
當(dāng)成一個指針,加 n 就表示往后移動,那么減去 n 就是往前移動了。舉個例子。
我們試著看一下這兩個例子。ptr[-1]
表示以 arr[3]
為基準(zhǔn)往前移動 1 個單位,因此此時指向的位置是 ,因此第一個輸出語句是輸出 arr[2]
的數(shù)值 100;而第二個的話,它和 arr[-2]
是一個意思,即指向 arr - 2
這個地方,然后取這個地方的數(shù)值。顯然是 10,因此輸出的就是 10。
正是因為如此,我們需要為指針定義指向變量的類型。因為指針是可以執(zhí)行加減法運算的。如果類型是
void*
的話,由于類型無法確定,因此程序并不知道我們到底需要往前或往后偏移單位的時候,走多遠(yuǎn)的距離。不同的數(shù)據(jù)類型占據(jù)的內(nèi)存空間大小是不一樣的,這就導(dǎo)致了使用void*
接收會無法確保移動長度的問題。
Part 3 固定語句
在使用棧內(nèi)存數(shù)組的過程中,因為它是在棧內(nèi)存里,因此變量是不會受到 GC 的影響的;但相反,由于數(shù)組被放在堆內(nèi)存里,因此如果我們使用指針取得變量的地址的話,因為是間接取值的關(guān)系,GC 萬一回收了這個數(shù)組的內(nèi)存,這個指針不就產(chǎn)生很嚴(yán)重的內(nèi)存問題了嗎?
因此,C# 發(fā)明了一種機制,叫做數(shù)組的固定(Fix)。固定數(shù)組后,數(shù)組在使用指針運算期間就無法被 GC 回收。
3-1 數(shù)組的固定
固定語句是這么寫的:
fixed
來表示,下面的 arr
我需要固定;而等號左側(cè)的變量 p
是 arr
這個堆內(nèi)存數(shù)組的首地址。然后,使用大括號就可以在大括號內(nèi)部使用這個 p
。但是,我們無法修改 p
,只能通過賦值給別的指針變量來修改。比如說 int* q = p; q++;
類似這樣的形式來修改 q
的數(shù)值,p
只能讀取用。
舉個例子,我們要計算一個數(shù)組每一個元素加起來的和。
ptr
來作為游標(biāo),移動 ptr
的指向來達到遍歷整個數(shù)組的過程。稍微注意一點的地方是,數(shù)組即使固定了,我們也無法通過指針本身來確定數(shù)組的大小。因此,我們還需要一個叫 times
的臨時變量來表示到底移動了多少次。
稍微注意一點的是,它和 C 語言不同,對數(shù)組本身使用地址符號和對數(shù)組的元素使用地址符號都是 C 語言允許的寫法,但是 C# 里,我們只能寫
int* p = arr
或int* p = &arr[0]
這種格式,而不能寫成int* p = &arr
。
3-2 字符串的固定
和普通數(shù)組稍顯不同的地方是,字符串的固定。字符串的固定被 C# 特殊處理過,因此我們?nèi)绻潭艘粋€字符串的話,那么這個字符串必然是“一個字符數(shù)組,外帶一個終止字符”。
終止字符(Terminator),專門標(biāo)記一個字符串是否結(jié)尾。它寫成 '\0'
,但我們一般書寫字符串字面量的時候,都不寫它。另外,C# 有特殊處理,即使字符串的中間有這個終止字符都是可以的,它只是在固定字符串的時候才會發(fā)生作用,表示字符串的結(jié)尾。
字符串的固定和數(shù)組的固定寫法完全一樣。
比如這樣。我們直接將字符串的字面量(或者變量)寫到 fixed
語句的小括號里,等號左側(cè)則使用 char*
類型的指針變量來接收。然后,下面使用 for
循環(huán)來遍歷整個字符串。另外,這里我們可以直接以 *ptr != '\0'
'\0'
的時候,我們就可以認(rèn)為整個字符串結(jié)束了;否則 ptr
不斷往后移動。
稍微注意一下。字符串在 C# 里是不可變的。換句話說,我們無法通過前文介紹字符串的那些內(nèi)容來改變字符串里的字符;相反地,我們怎么調(diào)用那些方法,最終都是產(chǎn)生一個新的字符串,來作為結(jié)果。但是,我們使用指針的話,字符串不可變的特征就會被打破。
'a'
。
