前言

本文主要是以 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的非托管指针
type& 指向type的托管指针

也就是说用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
}

浅谈 .NET 中的对象引用、非托管指针和托管指针的更多相关文章

  1. 浅谈 .NET 中的对象引用、非托管指针和托管指针 理解C#中的闭包

    浅谈 .NET 中的对象引用.非托管指针和托管指针   目录 前言 一.对象引用 二.值传递和引用传递 三.初识托管指针和非托管指针 四.非托管指针 1.非托管指针不能指向对象引用 2.类成员指针 五 ...

  2. 浅谈Java中的对象和对象引用

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  3. 浅谈Java中的equals和==(转)

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...

  4. 浅谈Linux中的信号处理机制(二)

    首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...

  5. 浅谈Java中的对象和引用

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  6. 浅谈Java中的equals和==

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...

  7. 转【】浅谈sql中的in与not in,exists与not exists的区别_

    浅谈sql中的in与not in,exists与not exists的区别   1.in和exists in是把外表和内表作hash连接,而exists是对外表作loop循环,每次loop循环再对内表 ...

  8. 浅谈iOS中的userAgent

    浅谈iOS中的userAgent   User-Agent(用户代理)字符串是Web浏览器用于声明自身型号版本并随HTTP请求发送给Web服务器的字符串,在Web服务器上可以获取到该字符串. 在公司产 ...

  9. 浅谈JavaScript中的闭包

    浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...

随机推荐

  1. 深入C++的new

    new”是C++的一个关键字,同时也是操作符.关于new的话题非常多,因为它确实比较复杂,也非常神秘,下面我将把我了解到的与new有关的内容做一个总结. new的过程 当我们使用关键字new在堆上动态 ...

  2. wiggle-subsequence

    // 参考了:https://discuss.leetcode.com/topic/51893/two-solutions-one-is-dp-the-other-is-greedy-8-lines ...

  3. UVA 165 Stamps (DFS深搜回溯)

     Stamps  The government of Nova Mareterrania requires that various legal documents have stamps attac ...

  4. Longest Substring Without Repeating Characters leetcode java

    题目: Given a string, find the length of the longest substring without repeating characters. For examp ...

  5. Proxy 代理模式 动态代理 cglib MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  6. TextBox_TextChanged

    private void TextBox_TextChanged(object sender, TextChangedEventArgs e) { TextBox textBox = sender a ...

  7. 如何开机就启动node.js程序

      npm install -g qckwinsvc 定位到安装目录,node_modules/.bin/ 运行如下命令: > qckwinsvc prompt: Service name: [ ...

  8. 在Fedora8上安装MySQL5.0.45的过程

    本来想安装最新的5.6.13-1版本,下载下来后,依赖的包rpmlib无处下载,无法只得作罢.从Foreda8的安装光盘中找到了以下文件: mysql-5.0.45-4.fc8.i386.rpm my ...

  9. 创建一个pre标签展开折叠的UI组件(原创)

    这些天练习UI组件的编写,顺便模仿一个h5版本的pre标签收缩展开的效果组件: 兼容ie8.9,谷歌,火狐: 图片效果如下: demo.html代码: <!DOCTYPE html> &l ...

  10. Excel中R1C1引用样式

    在Excel处理中,经常需要修改某行某列的值.默认情况下Excel中的列号是字母,每次都要去数,因为对字母的位置不熟悉,特别是又有合并单元格的时候,很容易数错.能不能把列也显示成数字,我坚信Offic ...