C# 堆内存 vs 栈内存 (2)

C# 堆内存与栈内存 - 参数传递

第一部分了解了堆内存和栈内存的基本概念,以及值类型和引用了类型的内存分配,同时也了解了指针的基本情况。本节主要是介绍在函数调用时参数的传递情况。

参数概述

在第一部分了解了函数调用时内存分配的基本情况,这一部分将对做进一步详细介绍。当调用一个函数时,会发生如下操作:
1. 在栈内存上为调用的方法分配空间,这里主要包括返回地址指针和参数传递所需空间。
2. 复制方法参数,这是将要详细研究的部分。
3. 指令指针指向被调函数的JIT代码,然后开始执行代码,因此在堆栈上又分配了一个栈帧。

代码:

public int AddFive(int pValue)
{
    int result;
    result = pValue + 5;
    return result;
}

调用该函数时,堆栈类似下面:
调用栈内存

在第一部分中讨论过,参数内存的分配与参数是值类型还是引用类型有关,值类型直接进行拷贝,而引用类型参数拷贝参数的引用(指针)。

传递值类型参数

首先,当传递值类型参数时,会在栈内存上分配空间,并将值复制到分配的空间,通过下面例子看下具体情况。

public class Class1
{
    public void Go()
    {
        int x = 5;
        AddFive(x);

        Console.WriteLine(x.ToString());      
    }

    public int AddFive(int pValue)
    {
        pValue += 5;
        return pValue;
    }
}

当调用 “Go” 方法时,在栈内存上为局部变量 x 分配空间,并将值 5 拷贝到分配的空间。栈内存变化如下图:
栈内存空间

接下来,当调用 AddFive 时,记录函数的返回地址,并为AddFive所需内存(局部变量、参数)并将值复制到分配的地址。

记录返回地址

当 AddFive 调用结束之后,根据上一步记录的返回地址,返回到Go函数中;并且将 AddFive调用时分配的栈内存空间进行释放。
返回调用函数

所以最终 x 的值为5,主要原因在于,所有值类型参数在传递时都是传递的副本,而非直接修改原始值。基于这一点,我们需要知道,当传递一个非常大的值类型时(比如一个struct),每次复制都非常的消耗空间和时间,栈内存空间是有限的,所以在使用struct时,需要慎重考虑。

通过一个例子来看一下结构体做参数时的情况:

public struct MyStruct
{
    public long a,b,c,d,e,f,g,h,i,j,k,l,m;
}

public class Program
{
    public void Go()
    {
        MyStruct x = new MyStruct();
        DoSomething(x);
    }

    public void DoSomething(MyStruct pValue)
    {
        //Do something here...
    }
}

当执行 Go() 方法时,栈内存会发生如下变化:
结构体封送

这种方式是非常低效的,所以一种更好的替代方法是,通过传引用的方式传递结构体参数,下面对上面代码进行下修改:

public class Program
{
    public void Go()
    {
        MyStruct x = new MyStruct();
        DoSomething(ref x);
    }

    public void DoSomething(ref MyStruct pValue)
    {
        //Do something here...
    }
}

此种方式减少了不必要内存的消耗,调用 Go 时栈内存变化如下:
结构体传引用

当选择传值或者按引用传递时,需要注意对象值得变化,区分清楚再函数内修改对象时副本还是对象本身。

传递引用类型参数

传递引用类型参数,和按引用传递值类型参数时相似的。

public class MyInt
{
    public int myValue;
}

public class Program
{
    public void Go()
    {
        MyInt x = new MyInt();
    }
}

当执行 Go() 函数时,堆栈内存变化如下:

heawpvsstack2-7

下面修改一下 Go 函数的内容

public void Go()
{
    MyInt x = new MyInt();
    x.myValue = 2;

    DoSomething(x);
    Console.WriteLine(x.myValue.ToString());
}

public void DoSomething(MyInt pValue)
{
    pValue.myValue = 12345;
}

此时在调用 Go 函数时,堆栈变化如下:
heapvsstack2-8

具体过程如下:
1. 在Go 方法开始,在堆内存上为 x 分配内存,并将内存地址复制到栈内存。
2. 在开始调用 DoSomething 时,参数pValue 的将在栈内存进行分配。
3. x 对象的指针值复制到 pValue内存。

所以当在 DoSomething 函数中修改MyInt属性myValue时,修改的其实是堆内存上的内容,所以在调用Console.WriteLine() 时输出内容是:12345

按引用传递引用类型参数

下面研究一下按引用传递引用类型时会发生什么,看下下面的代码。

public class Thing{}

public class Animal:Thing{}

public class Vegetable:Thing{}

public void Go()
{
    Thing x = new Animal();
    Switcharoo(ref x);

    Console.WriteLine("x is Vegetable : "+(x is Animal).ToString());
    Console.WriteLine("x is Vegetable : "+(x is Vegetable).ToString());
}

public void Switcharoo(ref Thing pValue)
{
    pValue = new Vegetable();
}

输出结果:

x is Animal : False
x is Vegetable : True

看下堆栈内存发生了什么变化:
heapvsstack2-9

具体步骤:
1. 开始调用 Go 函数时,在栈内存在 x 指针分配空间。
2. 然后将 Animal 对象的地址,复制到 x 指针地址。
3. 开始调用 Switchroo 函数,将 x 的地址复制到 pValue 内存中。
4. 在 堆内存 上为 Vegetable 分配内存。
5. 利用 pValue,使 x 指向刚分配的 Vegetable 内存地址。

heapvsstack2-10

如果不是按引用传递 x,x的值就不会改变。如果对上面的例子还有上面疑惑,可以参考上一篇 增加一下对按引用传值的理解。

总结

大致总结一下这一部分内容:
1. 传递值类型参数时,将实参的副本传递给函数。
2. 传递引用类型参数时,将引用对象地址传递给函数。
3. 当按引用传递引用类型参数时,将栈内存地址(保存着对象在堆内存的地址),传递给函数。

Created with Raphaël 2.1.2 栈内存上 pValue 内存 栈内存上 x 的内存地址 堆内存上 Animal 内存地址

通过本节介绍,对于内存如何处理参数传递,应该有了一个比较直观的理解,直到如何处理一些参数传递相关问题。

原文地址:C# Heap(ing) Vs Stack(ing) in .NET: Part II

猜你喜欢

转载自blog.csdn.net/salvare/article/details/80640827
今日推荐