一:背景
1. 讲故事
高级语言玩多了,可能很多人对指针或者汇编都淡忘了,本篇就和大家聊一聊指针,虽然c#中是不提倡使用的,但你能说指针在c#中不重要吗?你要知道fcl内库中大量的使用指针,如string,encoding,filestream
等等数不胜数,如例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
的栈上地址, 一图胜千言。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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. ++、–运算符
这种算术操作常常用在数组或者字符串等值类型集合,比如下面代码:
1
2
|
fixed ( int * ptr = new int [3] { 1, 2, 3 }) { } fixed ( char * ptr2 = "abcd" ) { } |
首先ptr默认指向数组在堆上分配的首地址,也就是1的内存地址,当ptr++后会进入到下一个整形元素2的内存地址,再++后又进入下一个int的内存地址,也就是3,很简单吧,我举一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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是不可变的,你就是对它吐口痰它都会生成一个新字符串,NB的是用指针就不一样了,你可以先找到替换字符的内存地址,然后将新字符直接赋到这个内存地址上,对不对,我来写一段代码,把abcgef
替换成 abcdef
, 也就是将 g
替换为 d
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
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# 指针的资料请关注服务器之家其它相关文章!
原文链接:https://www.imooc.com/article/304713