追了多年的开发框架,你还认识指针吗?

网友投稿 228 2022-06-23


一:背景

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 = &num; => 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是不可变的,你就是对它吐口痰它都会生成一个新字符串,


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:手把手教你用Python网络爬虫获取网易云音乐歌曲(python网易云音乐下载)
下一篇:C# 数据操作系列 - 7. EF Core 导航属性配置(曹海涛)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~