1. 程式人生 > >追了多年的開發框架,你還認識指標嗎?

追了多年的開發框架,你還認識指標嗎?

## 一:背景 ### 1. 講故事 高階語言玩多了,可能很多人對指標或者彙編都淡忘了,本篇就和大家聊一聊指標,雖然C#中是不提倡使用的,但你能說指標在C#中不重要嗎?你要知道FCL內庫中大量的使用指標,如`String,Encoding,FileStream`等等數不勝數,如例程式碼: ``` C# private unsafe static bool EqualsHelper(string strA, string strB) { fixed (char* ptr = &strA.m_firstChar) { fixed (char* ptr3 = &strB.m_firstChar) { char* ptr2 = ptr; char* ptr4 = ptr3; while (num >= 12) {...} while (num > 0 && *(int*)ptr2 == *(int*)ptr4) {...} } } } public unsafe Mutex(bool initiallyOwned, string name, out bool createdNew, MutexSecurity mutexSecurity) { byte* ptr = stackalloc byte[(int)checked(unchecked((ulong)(uint)securityDescriptorBinaryForm.Length))] } private unsafe int ReadFileNative(SafeFileHandle handle, byte[] bytes, out int hr) { fixed (byte* ptr = bytes) { num = ((!_isAsync) ? Win32Native.ReadFile(handle, ptr + offset, count, out numBytesRead, IntPtr.Zero) : Win32Native.ReadFile(handle, ptr + offset, count, IntPtr.Zero, overlapped)); } } ``` 對,你覺得的美好世界,其實都是別人幫你負重前行,退一步說,指標的理解和不理解,對你研究底層原始碼影響是不能忽視的,指標相對比較抽象,考的是你的空間想象能力,可能現存的不少程式設計師還是不太明白,因為你缺乏所見即所得的工具,希望這一篇能幫你少走些彎路。 ## 二:windbg助你理解 指標雖然比較抽象,但如果用windbg實時檢視記憶體佈局,就很容易幫你理解指標的套路,下面先理解下指標的一些簡單概念。 ### 1. &、* 運算子 `&`取址運算子,用於獲取某一個變數的記憶體地址, `*`運算子,用於獲取指標變數中儲存地址指向的值,很抽象吧,看windbg。 ``` C# unsafe { int num = 10; int* ptr = # var num2 = *ptr; Console.WriteLine(num2); } 0:000> !clrstack -l OS Thread Id: 0x41ec (0) Child SP IP Call Site 0000005b1efff040 00007ffc766208e2 *** WARNING: Unable to verify checksum for ConsoleApp4.exe ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 25] LOCALS: 0x0000005b1efff084 = 0x000000000000000a 0x0000005b1efff078 = 0x0000005b1efff084 0x0000005b1efff074 = 0x000000000000000a ``` 仔細觀察 `LOCALS` 中三組鍵值對。 ##### <1> `int* ptr = # => 0x0000005b1efff078 = 0x0000005b1efff084` `int* ptr`叫做指標變數,既然是變數必須得有自己的棧上地址 `0x0000005b1efff078` ,而這個地址上的值為 `0x0000005b1efff084`,這不就是num的棧地址嘛,嘿嘿。 #### <2> `var num2 = *ptr; => 0x0000005b1efff074 = 0x000000000000000a` `*ptr` 就是用ptr的value `[0x0000005b1efff084]` 獲取這個地址指向的值,所以就是10啦。 如果不明白,我畫一張圖,這可是重中之重哦~ ![](https://huangxincheng.oss-cn-hangzhou.aliyuncs.com/img/20200516081943.png) ### 2. **運算子 `**` 也叫二級指標,指向一級指標變數地址的指標,有點意思,如下程式:`ptr2`指向的就是 `ptr`的棧上地址, 一圖勝千言。 ``` C# unsafe { int num1 = 10; int* ptr = &num1; int** ptr2 = &ptr; var num2 = **ptr2; } 0:000> !clrstack -l ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 26] LOCALS: 0x000000305f5fef24 = 0x000000000000000a 0x000000305f5fef18 = 0x000000305f5fef24 0x000000305f5fef10 = 0x000000305f5fef18 0x000000305f5fef0c = 0x000000000000000a ``` ![](https://huangxincheng.oss-cn-hangzhou.aliyuncs.com/img/20200515220335.png) ### 3. ++、--運算子 這種算術操作常常用在陣列或者字串等值型別集合,比如下面程式碼: ``` C# fixed (int* ptr = new int[3] { 1, 2, 3 }) { } fixed (char* ptr2 = "abcd") { } ``` 首先`ptr`預設指向陣列在堆上分配的首地址,也就是1的記憶體地址,當`ptr++`後會進入到下一個整形元素2的記憶體地址,再++後又進入下一個int的記憶體地址,也就是3,很簡單吧,我舉一個例子: ``` C# unsafe { fixed (int* ptr = new int[3] { 1, 2, 3 }) { int* cptr = ptr; Console.WriteLine(((long)cptr++).ToString("x16")); Console.WriteLine(((long)cptr++).ToString("x16")); Console.WriteLine(((long)cptr++).ToString("x16")); } } 0:000> !clrstack -l LOCALS: 0x00000070c15fea50 = 0x000001bcaac82da0 0x00000070c15fea48 = 0x0000000000000000 0x00000070c15fea40 = 0x000001bcaac82dac 0x00000070c15fea38 = 0x000001bcaac82da8 ``` ![](https://huangxincheng.oss-cn-hangzhou.aliyuncs.com/img/20200515225846.png) 一圖勝千言哈,Console中的三個記憶體地址分別存的值是`1,2,3`哈, 不過這裡要注意的是,C#是託管語言,引用型別是分配在託管堆中,所以堆上地址會存在變動的可能性,這是因為GC會定期回收記憶體,所以vs編譯器需要你用fixed把堆上記憶體地址固定住來逃過GC的打壓,在本例中就是 `0x000001bcaac82da0 - (0x000001bcaac82da8 +4)`。 ## 三:用兩個案例幫你理解 古語說的好,一言不中,千言無用,你得拿一些例子活講活用,好吧,準備兩個例子。 ### 1. 使用指標對string中的字元進行替換 我們都知道string中有一個replace方法,用於將指定的字元替換成你想要的字元,可是C#中的string是不可變的,你就是對它吐口痰它都會生成一個新字串,