目录
前言
一, 对象引用
二, 值传递和引用传递
三, 初识托管指针和非托管指针
四, 非托管指针
1, 非托管指针不能指向对象引用
2, 类成员指针
五, 托管指针
前言
本文主要是以 C# 为例介绍 .NET 中的三种指针类型(本文不包含对于函数指针的介绍): 对象引用, 非托管指针 , 托管指针.
学习是一个不断深化理解的过程, 借此博客, 把自己关于 .NET 中指针相关的理解和大家一起讨论一下, 若有表述不清楚, 理解不正确之处, 还请大家批评指正.
开始话题之前, 我们不妨先对一些概念作出定义.
变量: 给存储单元指定名称, 即定义内存单元的名称或者说是标识.
指针: 一种特殊的变量, 其存储的是值的地址而不是值本身.
一, 对象引用
对于对象引用, 大家都不会陌生.
与值类型变量直接包含值不同, 引用类型变量存储的是数据的存储位置(托管堆内存地址).
对象引用是在托管堆上分配的对象的开始位置指针. 访问数据时, 运行时要先从变量中读取内存位置(隐式间接寻址), 再跳转到包含数据的内存位置, 这一切都是隐藏在 CLR 背后发生的事情, 我们在使用引用类型的时候, 并不需要关心其背后的实现.
二, 值传递和引用传递
很多朋友, 包括我, 在初期学习的时候, 可能都会有这么一个认知误区:"对象在 C# 中是按引用传递的".
对于引用传递, 借鉴《深入理解 C#》中话, 我们需要记住这一点:
假如以引用传递的方式来传送一个变量, 那么调用的方法可以通过更改其参数值, 来改变调用者的变量值.
例如下面这么一个例子:
- static void Main(string[] args)
- {
- Foo foo = new Foo
- {
- Name = "A"
- };
- Test(foo);
- Console.WriteLine(foo.Name); // 输出 B
- }
- static void Test(Foo obj)
- {
- obj.Name = "B";
- obj = new Foo
- {
- Name = "C"
- };
- }
按照引用传递的定义, 上述代码的结果应该是 C, 但实际输出的是 B.
因为 C# 默认是按值传递的, 在将 Main 函数中的 foo 变量传入 Test 函数时, 会将它所包含的值 (对象引用) 复制给变量 obj. 所以可以通过 obj 变量修改原来的实例成员, 这仅仅是由于引用类型的特性导致的, 并不是所谓的引用传递. 因为如果将 obj 变量指向一个新的实例, 并不会影响到 foo 变量, 它们两者是完全独立的.
只要对上述代码做一个小修改, 就能顺利地打印出 C, 也就是通过大家习惯的 ref 关键词.
- static void Main(string[] args)
- {
- Foo foo = new Foo
- {
- Name = "A"
- };
- Test(ref foo);
- Console.WriteLine(foo.Name); // 输出 C
- }
- static void Test(ref Foo obj)
- {
- obj.Name = "B";
- obj = new Foo
- {
- Name = "C"
- };
- }
三, 初识托管指针和非托管指针
在 C# 中, 如果我们想要定义一个引用传递的方法, 我们需要通过给方法参数加上 ref 或者 out 关键词.
同时 C# 也允许我们通过 unsafe 关键词编写不安全的代码. 那么这两者到底有什么区别呢.
以以下 C# 代码为例:
- static unsafe void Main(string[] args)
- {
- int a, b;
- Method1(&a); // 使用非托管指针
- Method2(out b); // 使用 out 关键词
- Console.WriteLine($"a:{a},b:{b}"); // a:1,b:2
- }
- static unsafe void Method1(int* num)
- {
- *num = 1;
- }
- static void Method2(out int b)
- {
- b = 2;
- }
接下来, 我们通过查看生成的 IL 的代码来分析一下这两者之间的区别.
- .assembly extern mscorlib {}
- .assembly 'App' {}
- .class private auto ansi beforefieldinit
- PointerDemo.Program
- extends [mscorlib]System.Object
- {
- .method private hidebysig static void
- Main(
- string[] args
- ) cil managed
- {
- .entrypoint
- .maxstack 3
- .locals init (
- [0] int32 a,
- [1] int32 b
- )
- // [8 9 - 8 10]
- IL_0000: nop
- // [10 13 - 10 25]
- IL_0001: ldloca.s a
- IL_0003: conv.u
- IL_0004: call void PointerDemo.Program::Method1(int32*)
- IL_0009: nop
- // [11 13 - 11 28]
- IL_000a: ldloca.s b
- IL_000c: call void PointerDemo.Program::Method2(int32&)
- IL_0011: nop
- // [13 13 - 13 47]
- IL_0012: ldstr "a:{0},b:{1}"
- IL_0017: ldloc.0 // a
- IL_0018: box [mscorlib]System.Int32
- IL_001d: ldloc.1 // b
- IL_001e: box [mscorlib]System.Int32
- IL_0023: call string [mscorlib]System.String::Format(string, object, object)
- IL_0028: call void [mscorlib]System.Console::WriteLine(string)
- IL_002d: nop
- // [14 9 - 14 10]
- IL_002e: ret
- } // end of method Program::Main
- .method private hidebysig static void
- Method1(
- int32* num
- ) cil managed
- {
- .maxstack 8
- // [17 9 - 17 10]
- IL_0000: nop
- // [18 13 - 18 22]
- IL_0001: ldarg.0 // num
- IL_0002: ldc.i4.1
- IL_0003: stind.i4
- // [19 9 - 19 10]
- IL_0004: ret
- } // end of method Program::Method1
- .method private hidebysig static void
- Method2(
- [out] int32& b
- ) cil managed
- {
- .maxstack 8
- // [22 9 - 22 10]
- IL_0000: nop
- // [23 13 - 23 19]
- IL_0001: ldarg.0 // b
- IL_0002: ldc.i4.2
- IL_0003: stind.i4
- // [24 9 - 24 10]
- IL_0004: ret
- } // end of method Program::Method2
- .method public hidebysig specialname rtspecialname instance void
- .ctor() cil managed
- {
- .maxstack 8
- IL_0000: ldarg.0 // this
- IL_0001: call instance void [mscorlib]System.Object::.ctor()
- IL_0006: nop
- IL_0007: ret
- } // end of method Program::.ctor
- } // end of class PointerDemo.Program
可以看到
静态方法 Method1 中的参数对应的 IL 代码 int32* num.
静态方法 Method2 中的参数对应的 IL 代码是 [out] int32& b, 其中 [out] 即使去除也不影响代码的运行, 上述代码是可通过 ilasm 编译的完整代码, 有兴趣的朋友可以自己做尝试.
通过学习《.NET 探秘: MSIL 权威指南》这本书, 我们可以了解到很多相关的知识.
在 CLR 中可以定义两种类型的指针:
ILAsm 符号 | 说明 |
---|---|
type* | 指向 type 的 < strong ztid="105" ow="70" oh="17"> 非托管指针 |
type& | 指向 type 的 < strong ztid="109" ow="56" oh="17"> 托管指针 |
也就是说用 out/ref 定义的指针类型其实对应的就是 CLR 中的托管指针.
四, 非托管指针
非托管指针的使用主要包括
寻址运算符 &
间接寻址运算符 *
用于结构指针的成员访问运算符 ->
非托管指针的用法和 C/C++ 基本一致, 这边不一一列出, 下面主要列出几个. net 中非托管指针的注意点.
1, 非托管指针不能指向对象引用
我们知道一个引用类型的变量, 它所存储的是托管堆上的实例的内存地址. 这个内存地址记录本身也是保存在内存的某个位置. 类似于我们用记事本记下了某人的联系方式, 同时这条联系方式记录本身也占据了我们记事本上一定的空间, 被我们写在了记事本的某个位置.
我们可以创建指向值类型变量的非托管指针, 也可以创建多级非托管指针, 但是不能创建指向引用类型变量 (对象引用) 的非托管指针.
- static unsafe void Main(string[] args)
- {
- int num = 2;
- object obj = new object();
- int* pNum = # // 指向值类型变量的非托管指针, 编译通过
- int** ppNum = &pNum; // 二级指针, 编译通过
- object* pObj = &obj; // 指向引用类型变量的非托管指针, 编译不通过
- }
2, 类成员指针
如果我们想要创建一个对象的值类型成员变量的指针, 按下方的代码是无法编译通过的.
- class Foo
- {
- public int Bar;
- }
- static unsafe void Main(string[] args)
- {
- Foo foo = new Foo();
- int* p = &foo.Bar; // 编译不通过
- }
因为对于生存在托管堆上的引用类型的实例而言, 在一次 GC 之后, 其内存位置可能会发生变动(GC 的 compact 阶段), 包含在实例内的成员变量也就随之发生了位置的移动. 对于标识内存位置的指针而言, 显然这样的情况是不能够被允许的.
但是我们可以通过 fixed 关键词避免 GC 时实例内存位置的移动来实现这种类型的指针的创建, 如下面代码所示.
- static unsafe void Main(string[] args)
- {
- Foo foo = new Foo();
- fixed (int* p = &foo.Bar) // 编译通过
- {
- Console.WriteLine((int)p); // 打印内存地址
- Console.WriteLine(*p); // 打印值
- }
- }
同理, 我们也可以利用 fixed 关键词创建指向值类型数组的指针(数组是引用类型, 这里指数组的元素是值类型).
- static unsafe void Main(string[] args)
- {
- int[] arr = { 1, 2 };
- // 除去 fixed 关键词外, 指向数组的非托管指针声明方式与 C/C++ 类似
- fixed (int* p = arr)
- {
- // 指针保存的是第一个元素的内存地址
- Console.WriteLine(*p); // 输出 1
- // 通过 +1 可以获取到第二个元素的内存地址
- Console.WriteLine(*(p + 1)); // 输出 2
- }
- }
五, 托管指针
在上文我们已经提到, 我们在使用引用传递的时候使用的 ref/out 关键词其实就是创建了托管指针.
在 C#7 之前, 我们只能在方法参数上见到托管指针的身影, C#7 进一步开放了托管指针的功能, 使得我们能够在更多的场景下使用它们. 例如和非托管指针一样, 用于方法的返回值,
托管指针完全受 CLR 管理, 与非托管指针相比, 在 C# 中 (IL 对于托管指针的限制会更少) 托管指针存在以下几个特点:
只能引用已经存在的项, 例如字段, 局部变量或者方法参数, 并不支持和非托管指针一样的单独声明.
不支持多级托管指针, 但是托管指针能够指向对象引用.
不能够打印内存地址的值.
不能够执行指针算法.
不需要显示的间接寻址(生成的 IL 代码中执行了间接寻址 通过 ldind.i4,ldind.ref 等指令 ).
- static void Main(string[] args)
- {
- var foo = new Foo{Bar = 1};
- // 创建指向引用类型变量 (对象引用) 的托管指针
- ref Foo p = ref foo;
- // IL 代码中通过 ldind.ref 指令间接寻址找到对象引用
- Console.WriteLine(p.Bar); // 输出 1
- }
来源: https://www.cnblogs.com/blurhkh/p/10357576.html