zoukankan      html  css  js  c++  java
  • c# 内存的具体表现- 通用类型系统 深拷贝 浅拷贝 函数传参

    c# 通用类型系统 及变量在 深拷贝 浅拷贝 函数传参 中的深层次的表现

    在编程中遇到了一些想不到的异常,跟踪发现,自己对于c#变量在内存上的表现理解有偏差,系统的学习并通过代码实验梳理了各种情况下,变量在内存级的表现情况,对以后的coding应该有些帮助。在此记录以免忘记了。。。

    1. 通用类型系统

    先来一张图:

     

     

     

     

     

     

     

     

    通用数据类型分为了值类型和引用类型。

    我们定义一个int型实际上是一个system.int32的实例,在语法上我们像使用其他的类对象一样,但是,存储的的仍然是基本类型。这种把基本类型和引用类型在语法上统一起来,所以可以称之为

    通用类型系统。其实每一个值类型都有一个.net相对应的system.***。

    引用类型,string是引用类型。在很多人看来string的表现与值类型一样,但是他是引用类型,只是string类的很多成员函数返回的重新生成的string对象。我们应该把引用类型当做对象指针。

    在学习c++时,我们知道变量分配在堆或者栈上。在c#讨论是我们不在区分堆栈,现在统一称为堆栈。c#引入了一个新的内存空间,称之为托管堆。

    引用一张别人的图,图右侧的堆其实应该是托管堆。

    值类型和引用类型的对象指针分配在堆栈上,而引用类型的具体对象分配在托管堆上。

     

    2. 变量赋值操作

    值类型赋值:值类型的赋值很简单,即在堆栈申请空间,将值写入申请的空间。int a =10;即堆栈上给a分配4字节空间,4字节空间保存了10的二进制;int b =a,即堆栈上给b分配4字节空间,4字节空间保存了a的值即10的二进制.a与b是完全独立的,b只是使用了a的值对自己的空间进行了赋值。

    引用类型赋值:引用类型声明时即在堆栈分配了4字节的空间(一个指针的大小),初始值为0x00000000。如object boxed,此时boxed分配了4字节的空间,空间内为0x00000000;boxed=  new object(),此时在托管堆分配了空间存放了一个object的对象,boxed在堆栈的4个字节的空间存放了对象在托管堆栈的首地址。当时使用object a =b时,堆栈上分配了a的4字节的空间,其值是b的4字节的空间的值。即a,b保存了同样的地址值,指向了同样的托管堆中的一个对象。a,b只是同一个对象的不同“指针”。

    3. 引用类型的浅拷贝和深拷贝

    浅拷贝和深拷贝问题出现在对象包含一个引用类型的成员这种情况下。说白了,浅拷贝和深拷贝就是有没有对对象的引用类型成员重新分配空间。引用类型对象存储在托管堆中,若对象本身包含一个引用类型的成员时,此成员在对象中存储的是引用类型的成员对象的地址。理解为“我”包含一个指针,指向了一个“他”,你要浅拷贝我时,我把这个指针给你,你也指向了“他”;你要深拷贝我时,创建一个“他”的弟弟即使用new,你指向“他”的弟弟即new的返回值(地址)。

    示例说明下:

     class Program
        {
            static void Main(string[] args)
            {
    
                Test a = new Test("aaa",10);
                Test b = a;
                Test c = a.Clone();
                c.myInt = 100;
                c.myStr = "bbb";
                c.myIntClass.myInt = 1000;
                Console.WriteLine(a.myInt +"    "+ a.myStr+"    "+a.myIntClass.myInt);
                Console.WriteLine(b.myInt + "    " + b.myStr + "    " + b.myIntClass.myInt);
                Console.WriteLine(c.myInt + "    " + c.myStr + "    " + c.myIntClass.myInt);
    
                Console.ReadLine();
    
            }
        }
        public class Test:Object
        {
            public string myStr { get; set; }
            public int myInt { get; set; }
    
            public IntClass myIntClass { get; set; }
            public Test Clone()
            {
                return (Test)this.MemberwiseClone();
            }
            public Test(string _str,int _int)
            {
                myStr = _str;
                myInt = _int;
                myIntClass = new IntClass(_int);
            }
        }
        public class IntClass
        {
            public int myInt { get; set; }
            public IntClass(int _int)
            {
                myInt = _int;
            }
        }

    a和b是指向同一个Test对象的引用,c是a的一个浅拷贝;即c指向的对象内有一个int,一个指向string的指针(与a指向string的指针的值相同),一个指向IntClass的指针(与a指向IntClass的指针的值相同)。当改变c的int时,a不受影响;当改变c的intClass时,其实是改变了a,c指向的同一个对象,故a,b,c的IntClass 同时改变了。这里有个有趣的现象,即string也是引用类型,而c改变了其myStr,a竟然没收受到影响。这要特殊说明string是一个特殊的引用类型,

     c.myStr = "bbb";其实是重新创建了一个string对象,c的mystr指向了新的对象,故a不受影响。string对象是不会被改变值的,对string的任何操作都是在一个新的对象上进行,然后返回新的对象引用。所以很多时候string表现的更像一个值类型,虽然它的确是一个引用类型。

    4. 函数传参

    无论是值类型还是引用类型,函数默认传递方式都是值传递,形参都“copy”了实参的值。  对于值类型很好理解,即堆栈上给形参分配了新的空间,把实参的值copy进新分配的空间。对于引用类型,堆栈上给形参分配了新的空间,把实参的值--实参在堆栈保持的4字节的对象地址 copy进了新分配的空间,注意,并没有在托管堆创建对象。所有,函数传递的引用类型,实参与形参在堆栈上是不同的空间,但其所指向的对象是同一个。

    示例:

    Test a = new Test("123", 10);
    Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
    int b = 100;
    Change(a, b);
    Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
    Console.WriteLine(b);

    public static void Change(Test _test,int _int)
    {
    _test.myInt = _int;
    _int *= 10;
    }

    经过函数change之后对象a的myInt变为了100,而int类型的b的值并没有变化还是100。

     可以简单的认为函数的值传递仅限于堆栈上的分配空间和对于新空间的赋值。
    out 和 ref
    c#函数有out和ref2个关键字修饰。先讲讲ref,ref相当于c++中的&传参。ref在堆栈上也没有分配空间,形参和实参在堆栈上是一个空间。所有对形参的任何改变都会体现在实参上,甚至,对于形参指向对象的改变也会体现在实参上。示例如下:
                Test a = new Test("123", 10);
                Console.WriteLine(a.myInt + "    " + a.myStr + "    " + a.myIntClass.myInt);
                int b = 100;
                Change(ref a, ref b);
                Console.WriteLine(a.myInt + "    " + a.myStr + "    " + a.myIntClass.myInt);
                Console.WriteLine(b);
            
            public static void Change(ref Test _test,ref int _int)
            {
                _test = new Test("new", _int);
                _int *= 10;
            }


    change传递的是ref关键字修饰的参数,在change内部对于形参的任何操作相当于对实参的操作。我们与不带ref关键字的对比下:
            Test a = new Test("123", 10);
                Console.WriteLine(a.myInt + "    " + a.myStr + "    " + a.myIntClass.myInt);
                int b = 100;
                Change( a,  b);
                Console.WriteLine(a.myInt + "    " + a.myStr + "    " + a.myIntClass.myInt);
                Console.WriteLine(b);
            
            public static void Change( Test _test, int _int)
            {
                _test = new Test("new", _int);
                _int *= 10;
            }
    
    
    
     
    不带ref关键字时,对于引用类型的赋值操作不会体现在实参上,这是由于我们把形参的堆栈上的地址值赋值为了在托管堆新分配的对象。而实参堆栈上的地址值依然指向旧的对象。不带ref实参,形参在堆栈是不同的2块空间。

    out关键字与ref一样,形参与实参是同一块堆栈地址,不同的时,函数退出前必须对out参数赋值(即在托管堆分配新的对象)。

    总结函数传参:

    默认是指传递,在堆栈上分配空间并把实参在堆栈上相应值复制到新分配的空间。对于形参堆栈的操作(值类型的赋值,引用类型的赋值)不会体现到实参上。对于托管堆的操作(改变引用类型的成员)会体现到实参上。
    ref传递,在堆栈上没有分配新的空间,形参和实参是同一块堆栈地址。
    对于形参堆栈的操作(值类型的赋值,引用类型的赋值)和 对于托管堆的操作(改变引用类型的成员)都会体现到实参上。
    out传递,在堆栈上没有分配新的空间,形参和实参是同一块堆栈地址。函数返回前必须对形参赋值。(若转载请注明博客园源地址)
     
    
    

     

    享受生活+创造
  • 相关阅读:
    WCF 、Web API 、 WCF REST 和 Web Service 的区别
    BusyIndicator using MVVM 忙碌状态指示器的的实现
    复制文件夹的方法 .net
    SQL/LINQ/Lamda
    CSLA验证规则总结
    C++中GB2312字符串和UTF-8之间的转换
    如何用VC编写供PB调用的DLL
    【转】lucene4.3.0 配置与调试
    cygwin主要命令
    【转】eclipse中window->preference选项中没有tomcat的解决方法
  • 原文地址:https://www.cnblogs.com/theLife/p/5060885.html
Copyright © 2011-2022 走看看