一:背景
1. 講故事
高級語言玩多了,可能很多人對指標或者匯編都淡忘了,本篇就和大家聊一聊指標,雖然C#中是不提倡使用的,但你能說指標在C#中不重要嗎?你要知道FCL內庫中大量的使用指標,如String,Encoding,FileStream等等數不勝數,如例代碼:
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,
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啦,
如果不明白,我畫一張圖,這可是重中之重哦~

2. **運算子
** 也叫二級指標,指向一級指標變數地址的指標,有點意思,如下程式:ptr2指向的就是 ptr的堆疊上地址, 一圖勝千言,
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

3. ++、--運算子
這種算術操作常常用在陣列或者字串等值型別集合,比如下面代碼:
fixed (int* ptr = new int[3] { 1, 2, 3 }) { }
fixed (char* ptr2 = "abcd") { }
首先ptr默認指向陣列在堆上分配的首地址,也就是1的記憶體地址,當ptr++后會進入到下一個整形元素2的記憶體地址,再++后又進入下一個int的記憶體地址,也就是3,很簡單吧,我舉一個例子:
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

一圖勝千言哈,Console中的三個記憶體地址分別存的值是1,2,3哈, 不過這里要注意的是,C#是托管語言,參考型別是分配在托管堆中,所以堆上地址會存在變動的可能性,這是因為GC會定期回收記憶體,所以vs編譯器需要你用fixed把堆上記憶體地址固定住來逃過GC的打壓,在本例中就是 0x000001bcaac82da0 - (0x000001bcaac82da8 +4),
三:用兩個案例幫你理解
古語說的好,一言不中,千言無用,你得拿一些例子活講活用,好吧,準備兩個例子,
1. 使用指標對string中的字符進行替換
我們都知道string中有一個replace方法,用于將指定的字符替換成你想要的字符,可是C#中的string是不可變的,你就是對它吐口痰它都會生成一個新字串,????的是用指標就不一樣了,你可以先找到替換字符的記憶體地址,然后將新字符直接賦到這個記憶體地址上,對不對,我來寫一段代碼,把abcgef 替換成 abcdef, 也就是將 g 替換為 d,
unsafe
{
//把 'g' 替換成 'd'
string s = "abcgef";
char oldchar = 'g';
char newchar = 'd';
Console.WriteLine($"替換前:{s}");
var len = s.Length;
fixed (char* ptr = s)
{
//當前指標地址
char* cptr = ptr;
for (int i = 0; i < len; i++)
{
if (*cptr == oldchar)
{
*cptr = newchar;
break;
}
cptr++;
}
}
Console.WriteLine($"替換后:{s}");
}
----- output ------
替換前:abcgef
替換后:abcdef
執行結束啦!
看輸出結果沒毛病,接下來用windbg去執行緒堆疊上找找當前有幾個string物件的參考地址,可以在break處抓一個dump檔案,

從圖中 LOCALS 中的10個變數地址來看,后面9個有帶地址的都是靠近string首地址: 0x000001ef1ded2d48,說明并沒有新的string產生,
2. 指標和索引遍歷速度大比拼
平時我們都是通過索引對陣列進行遍歷,如果和指標進行碰撞測驗,您覺得誰快呢?如果我說索引方式就是指標的封裝,你應該知道答案了吧,下面來一起觀看到底快多少???
為了讓測驗結果更加具有觀賞性,我準備遍歷1億個數字, 環境為:netframework4.8, release模式
static void Main(string[] args)
{
var nums = Enumerable.Range(0, 100000000).ToArray();
for (int i = 0; i < 10; i++)
{
var watch = Stopwatch.StartNew();
Run1(nums);
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds);
}
Console.WriteLine(" -------------- ");
for (int i = 0; i < 10; i++)
{
var watch = Stopwatch.StartNew();
Run2(nums);
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds);
}
Console.WriteLine("執行結束啦!");
Console.ReadLine();
}
//遍歷陣列
public static void Run1(int[] nums)
{
unsafe
{
//陣列最后一個元素的地址
fixed (int* ptr1 = &nums[nums.Length - 1])
{
//陣列第一個元素的地址
fixed (int* ptr2 = nums)
{
int* sptr = ptr2;
int* eptr = ptr1;
while (sptr <= eptr)
{
int num = *sptr;
sptr++;
}
}
}
}
}
public static void Run2(int[] nums)
{
for (int i = 0; i < nums.Length; i++)
{
int num = nums[i];
}
}

有圖有真相哈,直接走指標比走陣列下標要快近一倍,
四:總結
希望本篇能給在框架上奔跑的您一個友情提醒,不要把指標忘啦,別人提倡不使用的指標在底層框架可都是大量使用的哦~
如您有更多問題與我互動,掃描下方進來吧~


轉載請註明出處,本文鏈接:https://www.uj5u.com/net/38333.html
標籤:C#
下一篇:C#微信分賬功能
