如何在C#中使用指针
作者:一线码农上海 发布时间:2022-07-02 16:09:47
一:背景
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];
}
}
有图有真相哈,直接走指针比走数组下标要快近一倍。
四:总结
希望本篇能给在框架上奔跑的您一个友情提醒,不要把指针忘啦,别人提倡不使用的指针在底层框架可都是大量使用的哦~
来源:https://www.imooc.com/article/304713


猜你喜欢
- 有时候我们从数据库中查询出来数据之后,需要按照DataTable的某列进行分组,可以使用下面的方法实现,代码如下:using System;
- Maven可以使用mvn package指令对项目进行打包,如果使用java -jar xxx.jar执行运行jar文件,会出现"
- .NET开发人员首选的方法,通过COM组件调用Office软件本身来实现文件的创建和读写,但是数据量较大的时候异常缓慢;如下代码所示已经做了
- 本文实例讲述了Android自定义ActionBar的实现方法。分享给大家供大家参考。具体实现方法如下:Android 3.0及以上已经有了
- 日常工作中 Map 绝对是我们 Java 程序员高频使用的一种数据结构,那 Map 都有哪些遍历方式呢?这篇文章阿粉就带大家看一下,看看你经
- 1、多个线程对同一个队列进行读写操作,要注意进行读写控制,某个线程在读取的时候,不允许其它线程读、写;某个线程在写的时候,不允许其它线程进行
- 所谓文件的断点续传,就是一个线程传输文件,另一个线程控制传输标识,以达到暂停文件效果、恢复文件上传的效果。本demo使用最基本的线程之间的通
- 本文实例讲述了java实现的DES加密算法。分享给大家供大家参考,具体如下:一、DES加密算法介绍1、要求密钥必须是8个字节,即64bit长
- maven运行依赖于 JAVA_HOME如果各位还没有配置 JAVA_HOME,可以参考我的另一篇博客 JDK环境变量配置 JDK 环境变量
- 一、导航栏UINavigationBar1、导航栏的使用在iOS开发中,我们通常会使用导航控制器,导航控制器中封装了一个UINavigati
- 前言: 在命令行中输入可以输入各类参数,本文将针对这些参数做一个小结。基于命令行输入参数测试程序如下:import java.util.Ar
- 前提准备:1. 项目中至少需要引入的jar包,注意版本: a) core-renderer.jar&nb
- Java提供一种机制叫做序列化,通过有序的格式或者字节序列持久化java对象,其中包含对象的数据,还有对象的类型,和保存在对象中
- Android事件处理机制是基于Listener实现的,比如触摸屏相关的事件,是通过OnTouchListener实现的;而手势是通过OnG
- 序言使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:一、基于注解(@Scheduled)二、基于接口(Schedu
- 本文实例讲述了C#监控文件夹变化的方法。分享给大家供大家参考。具体实现方法如下:当需要监控某一文件,FileSystemWatcher类提供
- 介绍:淡入淡出动画(也称为“叠化”)逐渐淡出一个 View 或 ViewGroup,同时淡入另一个。此动画适用于您希望在应用中切换内容或视图
- 本文为大家分享了C#导入导出Excel数据的具体代码,供大家参考,具体内容如 * :对于实体类对象最好新建一个并且继承原有实体类,这样可以将类
- File类概述File类能新建、删除、重命名文件和目录,但不能访问文件内容本身,如果需要访问文件内容本身,则需要使用后续的输入/输出流。要在
- 前言最近,在给项目组使用Spring搭建Java项目基础框架时,发现使用Spring提供的BeanPostProcessor可以很简单方便地