浅谈 .NET 中的对象引用、非托管指针和托管指针
前言
本文主要是以 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 中的对象引用、非托管指针和托管指针的更多相关文章
- 浅谈 .NET 中的对象引用、非托管指针和托管指针 理解C#中的闭包
浅谈 .NET 中的对象引用.非托管指针和托管指针 目录 前言 一.对象引用 二.值传递和引用传递 三.初识托管指针和非托管指针 四.非托管指针 1.非托管指针不能指向对象引用 2.类成员指针 五 ...
- 浅谈Java中的对象和对象引用
浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...
- 浅谈Java中的equals和==(转)
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...
- 浅谈Linux中的信号处理机制(二)
首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...
- 浅谈Java中的对象和引用
浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...
- 浅谈Java中的equals和==
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...
- 转【】浅谈sql中的in与not in,exists与not exists的区别_
浅谈sql中的in与not in,exists与not exists的区别 1.in和exists in是把外表和内表作hash连接,而exists是对外表作loop循环,每次loop循环再对内表 ...
- 浅谈iOS中的userAgent
浅谈iOS中的userAgent User-Agent(用户代理)字符串是Web浏览器用于声明自身型号版本并随HTTP请求发送给Web服务器的字符串,在Web服务器上可以获取到该字符串. 在公司产 ...
- 浅谈JavaScript中的闭包
浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...
随机推荐
- 初识EntityFramework6
初识EntityFramework6 什么是EF? EF是一种ORM(Object-relational mapping)框架,它能把我们在编程时使用对象映射到底层的数据库结构.比如,你可以在数据库中 ...
- MongoDB学习笔记(三)--权限 && 导出导入备份恢复 && fsync和锁
权限 绑定内网I ...
- Java8新特性之重复注解(repeating annotations)浅析
Java8新特性之重复注解(repeating annotations)浅析 学习了:https://www.jb51.net/article/50827.htm
- 解决Windows Git Bash中文乱码问题
在git 安装目录 etc 下面 添加以下配置信息 1,/etc/gitconfig: [gui] encoding = utf-8 #代码库统一用urf-8,在git gui中可以正常显示中文 [i ...
- 帝吧fb出征是什么原因?帝吧fb出征事情始末 帝吧出征FB打“台独” 台湾网民崩溃:巨人之墙爆了
帝吧出征FB打"台独" 台湾网民崩溃:巨人之墙爆了 发表时间:2016-01-20 21:08:10 字号:A-AA+ 关键字: 帝吧帝吧出征FB帝吧出征FB打台独台独脸书巨人之墙 ...
- Linux操作系统启动界面(字符or图形界面)的设置及切换方法
安装完Linux操作系统后,默认是从图形界面启动的.那么如何使得系统从字符界面启动呢? 打开一个命令终端: #vim /etc/inittab 修改 id::initdefault: 为 id::in ...
- ssh2的application.xml配置文件配置详解
ssh2的application.xml配置文件配置详解 1.导入其他的配置文件.在ssh项目中可以导入其他的配置文件,导入的格式为: <import resource="clas ...
- Ubuntu 源码方式安装Subversion
使用到的安装包: apr-1.5.1.tar.gz apr-util-1.5.3.tar.gz pcre-8.35.tar.gz httpd-2.4.9.tar.bz2 subversion-1.8. ...
- webpack entry和output配置属性
1.entry entry的三种配置方式: (1)传递字符串: 单个入口语法:传递一个字符串 entry: './src/js/main.js', (2)传递数组 将创建“多个主入口(multi-ma ...
- 克隆server2008R2造成SID冲突
在云上搞的虚拟机,安装5台winserver2008r2,搭域环境,域环境搭好之后,改域用户为管理员,死活更改不成功,之前在测试环境搞域环境时碰到克隆镜像系统全部还原后搭建域环境不成功的情况,后来全部 ...