一:背景

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是不可变的,你就是对它吐口痰它都会生成一个新字符串,用指针就不一样了,你可以先找到替换字符的内存地址,然后将新字符直接赋到这个内存地址上,对不对,我来写一段代码,把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];
           }
       }

有图有真相哈,直接走指针比走数组下标要快近一倍。

四:总结

希望本篇能给在框架上奔跑的您一个友情提醒,不要把指针忘啦,别人提倡不使用的指针在底层框架可都是大量使用的哦~


©著作权归作者所有:来自51CTO博客作者mb5fd86a704dffe的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. Linux学习笔记总结(九十二)
  2. 指针经典笔试题解析(C语言)
  3. 指针进阶(一)(详细讲解1,2,3)(零基础学习C语言)
  4. LVS 负载均衡服务器搭建
  5. C语言中的指针
  6. 破解助手、资源下载,2019年九月最新实用油猴脚本推荐,一定要看到最
  7. 一套简单通用的Java后台管理系统,拿来即用,非常方便(附项目地址)
  8. C语言学习 4.0
  9. 「网工收藏必备」100个路由基础知识大全,掌握了秒变大神

随机推荐

  1. ImageView.setScalType参数详解
  2. LinearLayout、RelativeLayout、FrameLay
  3. Android(安卓)studio和码云步骤
  4. Android中实现日期时间选择器(DatePicker
  5. Android传感器编程带实例
  6. android延续按两次返回退出程序(完整代码)
  7. Android Timer的使用
  8. Android对接webService接口
  9. Android中贪吃蛇游戏的学习(二)
  10. 打开Android Studio报错"required plugin