值类型与引用类型(转和修改)

在C#中有两种类型的数据,一种是值类型数据,一种是引用类型数据。在编码的时候区分这两种类型数据,可以避免一些细小的编码错误。

  首先说说什么类型是值类型,例如:int、float、bool之类的基础类型,以及用struct定义的类型,如:DateTime。除此外,如 string,数组,以及用class定义的类型等都是引用类型。
1.计算机内存组织:
  内存中放两样东西---代码和数据
  操作系统和运行库(CLR)通常将用于容纳数据的内存划分为两个独立的区域---栈(stack)和堆(heap)。
  二者的设计目的不同:
  a).调用一个方法的时候,它的参数以及它的局部变量需要的内存是从stack中获取的。使用完毕后,该内存自动归还给栈。
  b).在使用new和一次构造函数调用来创建一个对象时,构件所需的内存是从heap中获取的。对对象的最后一个引用消失的时候,该部分内存就可以供重用(当然是在被收回后)。
  所有的值类型都是在stack上创建的,所有的引用类型都是在heap上创建的。
  c).对象本身存储在heap上,而对对象的引用(变量c)是存储在stack上。
     Circle c;
     c = new Circle();
  d).stack 和 heap的区别在于运行库对内存的组织方式不同:
     stack是”一个叠一个的箱子“
     heap是”一个屋子里到处乱七八糟放的一堆箱子“
2.值类型和引用类型的内存分配:
  a).当声明一个int型变量时,编译器会生成代码分配一个内存块,该块足以容纳一个整数。
    向一个int赋值,将导致值被copy到这个内存块中。
    int  i = 42;
    int copyi = i;//copyi中包含的是i的一个副本,i 的变化不会影响到copyi。
  b).声明一个Circle(Class)时,编译器不会分配一个足以容纳Circle对象的内存块,而是仅分配一小片内存,它刚好能够容纳包含Circle的另一个内存块的“地址“(或者对一个内存块的引用)
    Circle c = new Circle(42);
    Circle refc = c;
    其中,以”42“为半径的圆对象是存放在heap中,变量c和refc的内容是存放在stack中的,都是heap中对应内存块的引用(我觉得也可理解为地址)
    值类型:就是直接容纳着值
    引用类型:就是容纳着对内存块的引用。
3.关于指针和引用
  a).c sharp中的引用和指针很类似。但指针提供的功能更多。指针可以引用几乎任何的内存块。
  而在c sharp中,所有引用都限定了严格的类型,即,必须类型匹配才可以引用。你不能声明了一个引用变量引用一种类型,再用同一个变量去访问容纳了一种不同类型的的内存块。
  b).另外,在C#中,CLR对二者的管理和回收方式有区别。
4.使用ref和out参数
  a).向方法传递一个实参的时候,对应的参数(形参)会使用实参的一个副本来初始化。换言之,不可能通过修改方法中对应的参数来影响实参。不管参数是ValueType或者是ReferenceType,都成立。
   static void Main()
  {
 string str = "aaa";
 ChangeStr(str);
        Console.WriteLine(str);
  }
   static void ChangeStr(string str1)
  {
 str1 = "bbb";
        Console.WriteLine(str1);
  }
  如上:str虽为引用类型,但按值传递,传递时会在栈上拷贝一份副本,拷贝完后又进行new操作,重新在栈上分配新的对象。所以不会影响原来的str的值。
 
    static void Main()
  {
 Button b1 = new Button();
 b1.Text = "aaa";
        ChangeButton(b1)
 ChangeButtonText(b1);
  }

   static void ChangeButtonText(Button b3)
  {
 b3.Text = "bbb";
  }

    static void ChangeButton(Button b2)
  {
 b2 = new Button();
        b2.Text = "ccc";
  }
  再如上:ChangeButton 按值传递,传递时会在栈上拷贝一份b1副本(指向堆的引用),名为b2,这时候又调用了new Button 改变了b2指向原b1的引用,令b2指向了一个新的托管堆内存,所以我们再改变 b2的 Text  b1不会跟着变化,而ChangeButtonText值传递,传递时会在栈上拷贝一份b1副本(指向堆的引用),名为b3,这时候我们没有调用new 新的托管堆内存分配,所以b1,b3指向的 对象是一致的,调用属性赋值,b1也会改变的。
  我们需要记住的是不带ref传递的是值,而引用类型的变量,其值就是引用,所以传递了引用。
  带ref传递的是引用,所以对引用类型的变量来说,传递了引用的引用。

  当参数是“引用类型”时,实际上是传了一个“实参的引用”的副本(地址的副本)。
  b).如果为一个形参前附加了ref关键字,那么参数就会成为实参的一个别名(对实参的一个引用,区别与“实参的一个副本”)。传递实参的时候,也要附加ref。在使用ref参数时,也要满足”变量必须使用前赋值“的规则。
  c).如果希望由方法本身来初始化参数,传递一个未初始化参数给方法,这时候,需要在参数前附加out关键字。使用和ref参数类似。
 
  关键点:传参数的两种方式---传”副本“(包括值副本和引用副本(地址副本)),传”别名“(就是说,对形参的任何操作也就是对实参的操作)
5.Object类和装、拆箱
  a)."object"是”System.Object“的别名。c sharp中,所有类都是object的一个特殊化类型。
  b).object类型的变量可以引用任何一个引用类型的的任何对象,同时,它也可以引用一个值类型。
     所谓"装箱操作",实质上就是将一个数据项"从stack上复制到heap上的过程"。
     int i = 42;
     object o = i;
     过程:先从stack中分配一小片内存,用42初始化。(假如,o中的引用直接引用i,那么引用的将是堆栈stack。然而,所有引用应该引用heap上的对象。创建对堆栈中的数据的引用,会严重影响运行库的可靠性,应该坚决禁止),然后,在heap中分配一小块内存,将i中的值的一个副本复制到这片内存中,最后让o中的引用指向这个副本。这一过程,就称为Boxing。
     如果修改了i中的原始值,o中引用的值不会被修改。
  c).拆箱的过程中,一定要注意”类型匹配“的问题。
     当通过一个变量来访问一个已装箱的值的时候:
     object o = 42;
     int i = (int)o;
     int i = (long)o;//编译会失败(隐式类型转换失效)
     另外,如果o没有引用一个已装箱的值int,就会出现类型不匹配。
     Circle c = new Circle();
     object o = c;
     int i = (int)o;//编译会通过,但运行时会抛出异常
    
     注意:不能滥用装拆箱操作,开销太大,影响性能。

 

 

----------------------------------------------------------------------------

 

 

ref关键字传递栈地址
引用类型传递的是堆地址
如果你把ref 使用到引用类型上那么就会把引用类型的地址传递过去
而不是把引用类型应用的堆地址传递过去
引用类型:他的引用是存储在栈地址 他的值存储在堆地址 引用里边的值就是他堆的地址
值类型:值存储在栈地址
如果你先把一个引用类型当作值类型看
那么他的值就是他所引用的地址

所以如果一个引用类型加上Ref 那就可以在被调用函数中修改传递哪个参数的
对象
void main()
{
A a=new A();
a.b="c";
Main1(a);
Console.WriteLine(a.b);
}
void Main1(ref A a)
{
a=new A();//这个时候上边的a已经不是以前的a了
}

 

posted on 2010-03-07 14:18  kasafuma  阅读(141)  评论(0编辑  收藏  举报