.Net填坑 C#中的按引用传递与按值传递

C#中的按引用传递与按值传递

一、测试代码

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        TestRef(ref a);
        TestA(a);
    }

    static void TestA(int a)
    {
        a = 5;
    }

    static void TestRef(ref int a)
    {
        a = 5;
    }
}

二、反汇编分析

1.调用处反汇编

代码如下(示例):

        int a = 10;
00007FF9676B41D3  mov         dword ptr [rbp+28h],0Ah  
        TestRef(ref a);
00007FF9676B41DA  lea         rcx,[rbp+28h]  
00007FF9676B41DE  call        CLRStub[MethodDescPrestub]@7ff96750e3f0 (07FF96750E3F0h)  
00007FF9676B41E3  nop  
        TestA(a);
00007FF9676B41E4  mov         ecx,dword ptr [rbp+28h]  
00007FF9676B41E7  call        CLRStub[MethodDescPrestub]@7ff96750e3e8 (07FF96750E3E8h)  

可以看到与C++中按引用与按值传递一样,一个传递的是变量的地址,一个传递的是变量的值

2.被调用处的反汇编

按引用处理函数代码如下(示例):

static void TestRef(ref int a)
    {
00007FF9676B4DD0  push        rbp  
00007FF9676B4DD1  push        rdi  
00007FF9676B4DD2  push        rsi  
00007FF9676B4DD3  sub         rsp,20h  
00007FF9676B4DD7  mov         rbp,rsp  
00007FF9676B4DDA  mov         qword ptr [rbp+40h],rcx  
00007FF9676B4DDE  cmp         dword ptr [7FF9675BAD00h],0  
00007FF9676B4DE5  je          Program.TestRef(Int32 ByRef)+01Ch (07FF9676B4DECh)  
00007FF9676B4DE7  call        00007FF9C71BD1C0  
00007FF9676B4DEC  nop  
        a = 5;
00007FF9676B4DED  mov         rax,qword ptr [rbp+40h]  
00007FF9676B4DF1  mov         dword ptr [rax],5

按引用处理,与C++中一样,赋值都是以两条指令,用地址的方式直接对main函数中的本地变量地址处的值进行修改。

按值传递处理如下:

static void TestA(int a)
    {
00007FF9676C4E50  push        rbp  
00007FF9676C4E51  push        rdi  
00007FF9676C4E52  push        rsi  
00007FF9676C4E53  sub         rsp,20h  
00007FF9676C4E57  mov         rbp,rsp  
00007FF9676C4E5A  mov         dword ptr [rbp+40h],ecx  
00007FF9676C4E5D  cmp         dword ptr [7FF9675CAD00h],0  
00007FF9676C4E64  je          Program.TestA(Int32)+01Bh (07FF9676C4E6Bh)  
00007FF9676C4E66  call        00007FF9C71BD1C0  
00007FF9676C4E6B  nop  
        a = 5;
00007FF9676C4E6C  mov         dword ptr [rbp+40h],5 

可以看到也与C++中处理一样,用mov指令直接操作本地变量赋值

二、引用类型的反汇编分析

测试代码:

class Program
{
    class A
    {
       public int intTest = 10;
    }


    static void Main(string[] args)
    {
        A a = new A();
        TestRef(ref a);
        TestA(a);
    }

    static void TestA(A a)
    {
        a.intTest = 5;
    }

    static void TestRef(ref A a)
    {
        a.intTest = 5;
    }

反汇编查看:

        TestRef(ref a);
00007FF963E847CB  lea         rcx,[rbp+28h]  
00007FF963E847CF  call        CLRStub[MethodDescPrestub]@7ff963cde3f0 (07FF963CDE3F0h)  
00007FF963E847D4  nop  
        TestA(a);
00007FF963E847D5  mov         rcx,qword ptr [rbp+28h]  
00007FF963E847D9  call        CLRStub[MethodDescPrestub]@7ff963cde3e8 (07FF963CDE3E8h)

函数处理:
按引用:

    static void TestRef(ref A a)
    {
00007FF963E85420  push        rbp  
00007FF963E85421  push        rdi  
00007FF963E85422  push        rsi  
00007FF963E85423  sub         rsp,20h  
00007FF963E85427  mov         rbp,rsp  
00007FF963E8542A  mov         qword ptr [rbp+40h],rcx  
00007FF963E8542E  cmp         dword ptr [7FF963D8AD00h],0  
00007FF963E85435  je          Program.TestRef(A ByRef)+01Ch (07FF963E8543Ch)  
00007FF963E85437  call        00007FF9C397D1C0  
00007FF963E8543C  nop  
        a.intTest = 5;
00007FF963E8543D  mov         rax,qword ptr [rbp+40h]  
00007FF963E85441  mov         rax,qword ptr [rax]  
00007FF963E85444  mov         dword ptr [rax+8],5

按值:

static void TestA(A a)
    {
00007FF963E85470  push        rbp  
00007FF963E85471  push        rdi  
00007FF963E85472  push        rsi  
00007FF963E85473  sub         rsp,20h  
00007FF963E85477  mov         rbp,rsp  
00007FF963E8547A  mov         qword ptr [rbp+40h],rcx  
00007FF963E8547E  cmp         dword ptr [7FF963D8AD00h],0  
00007FF963E85485  je          Program.TestA(A)+01Ch (07FF963E8548Ch)  
00007FF963E85487  call        00007FF9C397D1C0  
00007FF963E8548C  nop  
        a.intTest = 5;
00007FF963E8548D  mov         rax,qword ptr [rbp+40h]  
00007FF963E85491  mov         dword ptr [rax+8],5

结论:

按引用传递,将引用类型变量的地址传给被调用的函数,被调用函数中对引用类型的操作,都转换为对引用类型变量地址值的操作,如上所示,调用a.intTest = 5时

00007FF963E8543D  mov         rax,qword ptr [rbp+40h]  
00007FF963E85441  mov         rax,qword ptr [rax]  
00007FF963E85444  mov         dword ptr [rax+8],5

先将本地变量中的值存到rax(指向mian函数中A a这个变量的地址 ),获取这个地址的内容(其指向托管堆中真正A实例的地址),再次放到rax中(此时rax中变成了托管堆中的实例地址),然后进行字段位置复制命令。

按值传递:

00007FF963E8548D  mov         rax,qword ptr [rbp+40h]  
00007FF963E85491  mov         dword ptr [rax+8],5

先将本地变量中的值存到rax(由mian传递进来的托管堆中的实例地址值的副本),直接对这个地址值进行操作就可以。
两者的区别就在于:按引用传递的是,引用类型变量本身的地址(用的时候,需要进行一次地址取值操作,得到引用变量内存着的托管推实例对象地址),按值传递,直接得到托管堆实例对象地址。
按引用传递,可以对托管对象变量上例中Main函数中的A的内容进行操作。比如实现swap(ref A a,ref A b)这种交换操作,直接操作a与b中存储的指向来实现交换。

从IL指令中看区别

  .method private hidebysig static void
    Main(
      string[] args
    ) cil managed
  {
    .entrypoint
    .maxstack 1
    .locals init (
      [0] class Program/A a
    )

    IL_0000: nop

    // [16 5 - 16 34]
    IL_0001: newobj       instance void Program/A::.ctor()
    IL_0006: stloc.0      // a

    // [17 5 - 17 27]
    IL_0007: ldloca.s     a
    IL_0009: call         void Program::TestRef(class Program/A&)
    IL_000e: nop

    // [18 5 - 18 21]
    IL_000f: ldloc.0      // a
    IL_0010: call         void Program::TestA(class Program/A)
    IL_0015: nop
    IL_0016: ret

  } 

可以看到IL指令也是有区别分别对应 ldloca.s,ldloc.0,分别就是加载本地变量的地址,与本地变量值,与汇编看到的结论相同。

再看函数内部的区别:
.method private hidebysig static void
TestA(
class Program/A a
) cil managed
{
.maxstack 8

IL_0000: nop

// [23 5 - 23 18]
IL_0001: ldarg.0      // a
IL_0002: ldc.i4.5
IL_0003: stfld        int32 Program/A::intTest
IL_0008: ret

} // end of method Program::TestA

.method private hidebysig static void
TestRef(
class Program/A& a
) cil managed
{
.maxstack 8

IL_0000: nop

// [28 5 - 28 18]
IL_0001: ldarg.0      // a
IL_0002: ldind.ref
IL_0003: ldc.i4.5
IL_0004: stfld        int32 Program/A::intTest
IL_0009: ret

} // end of method Program::TestRef

可以看到两者生成的IL代码也是有区别的,在于ldind.ref这个IL指令,是取地址对应的对象地址的指令,也与汇编中看到的一样。

总结

C#中的按引用与按值传递,与C++中底层原理一致,都是对变量本身传递其地址,还是传递其副本进行操作,在对这两种变量进行操作时,看起来一样的代码,a = 10,a.Test =10等,按引用会多生成一次取地址对应值的操作。备注:C#中应用类型变量本身就是指针的包装,所以按引用对托管堆对象操作时,会有两次取地址操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值