如何在C#中使用指针

一:背景

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 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

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}");
   }

看输出结果没毛病,接下来用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];
   }
  }

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

四:总结

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

以上就是如何在C#中使用指针的详细内容,更多关于C# 指针的资料请关注我们其它相关文章!

(0)

相关推荐

  • 如何在C#中使用指针

    一:背景 1. 讲故事 高级语言玩多了,可能很多人对指针或者汇编都淡忘了,本篇就和大家聊一聊指针,虽然C#中是不提倡使用的,但你能说指针在C#中不重要吗?你要知道FCL内库中大量的使用指针,如String,Encoding,FileStream等等数不胜数,如例代码: private unsafe static bool EqualsHelper(string strA, string strB) { fixed (char* ptr = &strA.m_firstChar) { fixed (

  • 分享如何在VB中调用VC编写的DLL

    一般来说,VB和VC共同编程有3种方式:一种是VC生成DLL,在VB中调用DLL:一种是VC生成ActiveX控件(.ocx),在VB中插入:还有一种是在VC中生成ActiveX Automation服务器,在VB中调用.相对而言,第一种方法对VC编程者的要求最低,但要求你的伙伴进行配合,我推荐这种方法. 先说说VC++的编程.首先在VC++中生成Win32 DLL工程.在这个工程中添加几个函数供VB用户调用.一个DLL中的函数要想被VB调用,必须满足两个条件:一是调用方式为stdcall,另一

  • 如何在PHP中使用数组

    1.PHP如何获取数组里元素的个数实例 在 PHP 中,使用 count()函数对数组中的元素个数进行统计. 例如,使用 count()函数统计数组元素的个数,示例代码如下: <?php header("Content-Type:text/html; charset=utf-8"); $arr = array("php","thinkphp","laravel"); echo count($arr); 输出结果为: 3

  • 如何在PHP中读写文件

    在PHP中读写文件,可以用到一下内置函数: 1.fopen(创建文件和打开文件) 语法: fopen(filename,mode) filename,规定要打开的文件.mode,打开文件的模式,可能的值见下表. mode 说明 "r" 只读方式打开,将文件指针指向文件开头. "r+" 读写方式打开,将文件指针指向文件开头. "w" 写入方式打开,将文件指针指向文件开头并将文件大小截为零.如果文件不存在则尝试创建. "w+" 读

  • 如何在c#中使用opencv函数库

    这个demo用c#实现图片裁剪和半透明融合的功能演示程序.功能挺简单的,就是把一张固定大小的图片先做边缘羽化,然后贴到一个圆形泡泡形状的底图上,最后把结果半透明融合到一张背景图上. C#实现图像的羽化.将图片裁剪复制到一个圆形图片这些都挺简单的,最后一步融合到背景图上需要用到opencv的seamlessClone方法.网上搜索c#使用opencv的方法有很多,一种是直接使用opencv的C#版本,一种是先把opencv的方法封装到一个dll然后用c#调用这个dll导出的方法.对于我这个需求,后

  • 如何在python中实现capl语言里的回调函数(推荐)

    CAPL:回调函数 CAPL是一种程序语言,其中程序块的执行由事件控制. 这些程序块被称为事件程序.在事件程序中定义的程序代码在事件发生时执行.换句话说,事件程序就是事件函数,当事件函数关联的事件被触发时,会自动执行此事件函数函数体.事件函数也称为回调函数 事件函数的标志就是关键字on,比如: on key 表示当键盘按下小写字母a时触发此事件函数执行 on message 表示当接收到消息时触发此事件函数执行 on start 表示当canoe软件运行时触发此事件函数执行 on sysvar

  • 如何在AngularJs中调用第三方插件库

    在AngularJs中我们会不可避免的使用第三方库,例如jquery插件库.我们不能散乱的在AngularJS中引入这些库,例如在controller中.那么应该怎么在Angular中使用第三方库呢? 如何使用? 很简单,给插件写一个directive. 在这里,我会使用一个简单的jquery插件Toolbar.js 的DEMO. 这是我们如何在jquery中创建一个tooltip的: <!-- Click this to see a toolbar --> <div id="

  • Swift中的指针操作和使用详细介绍

    Apple期望在Swift中指针能够尽量减少登场几率,因此在Swift中指针被映射为了一个泛型类型,并且还比较抽象.这在一定程度上造成了在Swift中指针使用的困难,特别是对那些并不熟悉指针,也没有多少指针操作经验的开发者(包括我自己也是)来说,在Swift中使用指针确实是一个挑战.在这篇文章里,我希望能从最基本的使用开始,总结一下在Swift中使用指针的一些常见方式和场景.这篇文章假定你至少知道指针是什么,如果对指针本身的概念不太清楚的话,可以先看看这篇五分钟C指针教程(或者它的中文版本),应

  • Go语言中的指针运算实例分析

    本文实例分析了Go语言中的指针运算方法.分享给大家供大家参考.具体分析如下: Go语言的语法上是不支持指针运算的,所有指针都在可控的一个范围内使用,没有C语言的*void然后随意转换指针类型这样的东西.最近在思考Go如何操作共享内存,共享内存就需要把指针转成不同类型或者对指针进行运算再获取数据. 这里对Go语言内置的unsafe模块做了一个实验,发现通过unsafe模块,Go语言一样可以做指针运算,只是比C的方式繁琐一些,但是理解上是一样的. 下面是实验代码: 复制代码 代码如下: packag

  • C语言中函数指针的三种使用方法总结

     C语言中函数指针的三种使用方法总结 在这里分享一下自己的心得,希望和大家一起分享技术,如果有什么不足,还请大家指正.写出这篇目的,就是希望大家一起成长,我也相信技术之间没有高低,只有互补,只有分享,才能使彼此更加成长. 定义方式:int (*p)(int x, int y); 实现代码: #include <stdio.h> int sum(int x, int y){ return x + y; } int reduce(int x, int y){ return x - y; } int

随机推荐