首页 > 其他分享 >浅谈 .NET 中的对象引用、非托管指针和托管指针

浅谈 .NET 中的对象引用、非托管指针和托管指针

时间:2023-06-15 10:35:00浏览次数:33  
标签:浅谈 void 托管 static IL 指针 引用

目录

 

前言#

本文主要是以 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

}

 

 

出处:https://www.cnblogs.com/eventhorizon/p/10357576.html

标签:浅谈,void,托管,static,IL,指针,引用
From: https://www.cnblogs.com/mq0036/p/17482200.html

相关文章

  • 【LeetCode双指针】合并两个有序数组,从后向前遍历
    合并两个有序数组https://leetcode.cn/problems/merge-sorted-array/给你两个按非递减顺序排列的整数数组nums1和nums2,另有两个整数m和n,分别表示nums1和nums2中的元素数目。请你合并nums2到nums1中,使合并后的数组同样按非递减顺序排列。注意:最终,合并后数......
  • 浅谈 thinkphp composer 扩展包加载原理
    浅谈thinkphpcomposer扩展包加载原理本文将介绍ThinkPHP中Composer扩展包的加载原理,帮助读者更好地理解和应用该功能。前言如题,今天感觉好久没有更新博客了。最近迷上了物联网开发。一直在研究stm32、51这些东西。想起来前几天群里面有人问到tp扩展包原理。其实这个前......
  • WPF之浅谈数据模板(DataTemplate)
    数据模板有什么用简而言之,数据模板能让你更方便、更灵活的显示你的各类数据。只有你想不到,没有它做不到的(感觉有点夸张,实践之后,你就觉得一点不夸张......
  • StringPtr StringPtrs 字符串指针 字符串指针切片
    funcBoolPtr(vbool)*bool{  return&v}funcStringPtr(vstring)*string{  return&v} funcStringPtrs(vals[]string)[]*string{  ptrs:=make([]*string,len(vals))  fori:=0;i<len(vals);i++{    ptrs[i]=&v......
  • 浅谈MultipartFile中transferTo方法的坑 服务器上面使用相对路径 file.transferTo(fil
    浅谈MultipartFile中transferTo方法的坑服务器上面使用相对路径file.transferTo(filePath.getAbsoluteFile())而不是file.transferTo(filePath.getPath())绝对路径,实际生产配置服务器里面的一个文件夹。比如配置服务器文件夹前缀为/downfile/excelfile原文链接:https://ww......
  • 关于函数指针的一些问题小结
    最近接到一个需求,使用sdk提供的消息回调,一般我们是继承sdk的消息类,然后sdk的消息回调(虚函数)会在有消息的时候调用回调指针,从而触发回调不过因为sdk那边又对该消息类二次封装了并提供了一些接口,所以在研究二次封装的方法时,遇到了一些有意思的问题,故记录下typedefvoid(_......
  • 武汉星起航浅谈亚马逊卖家如何编辑产品关键词,提升产品排名
    作为全球最大的在线零售平台之一,亚马逊为卖家提供了丰富的机会和潜力。在亚马逊平台上,一个关键的因素是如何编辑产品关键词,以帮助产品在搜索结果中脱颖而出,并提升排名。现在,武汉星起航将分享编辑产品关键词的秘诀,助力你的产品排名飙升。首先,关键词的选择至关重要。亚马逊卖家应该仔......
  • SSM框架学习之Spring浅谈(二)
    Spring常用注解@Controller:对应SpringMVC控制层,主要用户接受用户请求并调用Service层返回数据给前端页面。@Service:对应服务层,主要涉及一些复杂的逻辑,需要用到Dao层。@Component:通用的注解,可标注任意类为Spring组件。如果一个Bean不知道属于哪个层,可以使用@......
  • Git(分布式版本控制系统)在Windows下的使用-将代码托管到开源中国(oschina)
    一、Git是什么?    Git---Thestupidcontenttracker,傻瓜内容跟踪器。Git是目前世界上最先进的分布式版本控制系统。二、SVN与Git的最主要的区别?     SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而干活的时候,用的都是自己的电脑,所以首先要从中央服务......
  • 力扣第209题(双指针)
    209.长度最小的子数组-力扣(LeetCode)我的思路:固定起始位置,移动终止位置,将起始位置和终止位置之间的元素进行加和。直到满足条件就停止移动终止位置。这个时候将起始位置向前移动一个距离,然后将终止位置重新移回更新后的起始位置上。这样做的问题是会带来重复的操作。比如一个......