传值调用和传引用调用是几乎所有主流语言都会涉及到的问题,下面我谈谈我对C#中传值调用和传引用调用的理解。

http://www.cnblogs.com/wang_yb/archive/2011/05/18/2050574.html
--

自己的理解:对应引用类型var a = new var(); a只是个分配在堆栈上的临时变量,真正的类实例分配在堆上。而对于临时变量,它有自己的value(堆上的真正类实例的地址),还有一个自己的地址。在不适用ref时是传值方式(但和值类型稍微有点区别的,它的值value是堆上的真正类实例的地址),相当于创建了一个新的临时对象,并且它的值是是堆上的真正类实例的地址.

传址方式是直接传递同一个临时堆栈变量,所有的操作是基于这个临时变量的.传址即地址相同,代表同一个变量。

1.
一般对C#中传值调用和传引用调用的理解



  • 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
  • 如果传递的参数是类(class)那么就是传引用调用。
  • 如果传递的参数前有ref或者out关键字,那么就是传引用调用。

验证示例的代码如下:










1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73


using
System;


public
class ArgsByRefOrValue

{

    public
static void Main(string[] args)

    {

        // 实验1.
传值调用--基元类型

        int
i = 10;

        Console.WriteLine("before call ChangeByInt: i = " + i.ToString());

        ChangeByInt(i);

        Console.WriteLine("after
call ChangeByInt: i = "
+ i.ToString());


        Console.WriteLine("==============================================");

        // 实验2.
传值调用--结构体

        Person_val p_val
=
new Person_val();

        p_val.name =
"old val name";

        Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);

        ChangeByStruct(p_val);

        Console.WriteLine("after
call ChangeByStruct: p_val.name = "
+
p_val.name);


        Console.WriteLine("==============================================");

        // 实验3.
传引用调用--类

        Person_ref p_ref
=
new Person_ref();

        p_ref.name =
"old ref name";

        Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);

        ChangeByClass(p_ref);

        Console.WriteLine("after
call ChangeByClass: p_ref.name = "
+
p_ref.name);


        Console.WriteLine("==============================================");

        // 实验4.
传引用调用--利用ref

        Person_ref p =
new Person_ref();

        p.name =
"old ref name";

        Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);

        ChangeByClassRef(ref p);

        Console.WriteLine("after
call ChangeByClassRef: p.name = "
+ p.name);


        Console.ReadKey(true);

    }


    static
void ChangeByInt(int
i)

    {

        i = i + 10;

        Console.WriteLine("when
calling ChangeByInt: i = "
+ i.ToString());

    }


    static
void ChangeByStruct(Person_val p_val)

    {

        p_val.name =
"new val name";

        Console.WriteLine("when
calling ChangeByStruct: p_val.name = "
+
p_val.name);

    }


    static
void ChangeByClass(Person_ref p_ref)

    {

        p_ref.name =
"new ref name";

        Console.WriteLine("when
calling ChangeByClass: p_ref.name = "
+
p_ref.name);

    }


    static
void ChangeByClassRef(ref Person_ref p)

    {

        p.name =
"new ref name";

        Console.WriteLine("when
calling ChangeByClassRef: p.name = "
+
p.name);

    }

}


public struct
Person_val

{

    public
string name;

}


public class
Person_ref

{

    public
string name;

}

运行结果如下:



看起来似乎上面代码中实验3实验4是一样的,即对于类(class)来说,不管加不加ref或out,都是传引用调用。


其实,这只是表面的现象,只要稍微改一下代码,结果就不一样了。


修改上面代码,再增加两个实验。










1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103


using
System;


public
class ArgsByRefOrValue

{

    public
static void Main(string[] args)

    {

        // 实验1.
传值调用--基元类型

        int
i = 10;

        Console.WriteLine("before call ChangeByInt: i = " + i.ToString());

        ChangeByInt(i);

        Console.WriteLine("after
call ChangeByInt: i = "
+ i.ToString());


        Console.WriteLine("==============================================");

        // 实验2.
传值调用--结构体

        Person_val p_val
=
new Person_val();

        p_val.name =
"old val name";

        Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);

        ChangeByStruct(p_val);

        Console.WriteLine("after
call ChangeByStruct: p_val.name = "
+
p_val.name);


        Console.WriteLine("==============================================");

        // 实验3.
传引用调用--类

        Person_ref p_ref
=
new Person_ref();

        p_ref.name =
"old ref name";

        Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);

        ChangeByClass(p_ref);

        Console.WriteLine("after
call ChangeByClass: p_ref.name = "
+
p_ref.name);


        Console.WriteLine("==============================================");

        // 实验4.
传引用调用--利用ref

        Person_ref p =
new Person_ref();

        p.name =
"old ref name";

        Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);

        ChangeByClassRef(ref p);

        Console.WriteLine("after
call ChangeByClassRef: p.name = "
+ p.name);


        Console.WriteLine("==============================================");

        // 实验5.
传引用调用--类 在调用的函数重新new一个对象

        Person_ref
p_ref_new =
new Person_ref();

        p_ref_new.name =
"old new ref name";

        Console.WriteLine("before call ChangeByClassNew: p_ref_new.name = "
+ p_ref_new.name);

        ChangeByClassNew(p_ref_new);

        Console.WriteLine("after
call ChangeByClassNew: p_ref_new.name = "
+
p_ref_new.name);


        Console.WriteLine("==============================================");

        // 实验6.
传引用调用--利用ref 在调用的函数重新new一个对象

        Person_ref p_new
=
new Person_ref();

        p_new.name =
"old new ref name";

        Console.WriteLine("before call ChangeByClassRefNew: p_new.name = "
+ p_new.name);

        ChangeByClassRefNew(ref p_new);

        Console.WriteLine("after
call ChangeByClassRefNew: p_new.name = "
+
p_new.name);


        Console.ReadKey(true);

    }


    static
void ChangeByInt(int
i)

    {

        i = i + 10;

        Console.WriteLine("when
calling ChangeByInt: i = "
+ i.ToString());

    }


    static
void ChangeByStruct(Person_val p_val)

    {

        p_val.name =
"new val name";

        Console.WriteLine("when
calling ChangeByStruct: p_val.name = "
+
p_val.name);

    }


    static
void ChangeByClass(Person_ref p_ref)

    {

        p_ref.name =
"new ref name";

        Console.WriteLine("when
calling ChangeByClass: p_ref.name = "
+
p_ref.name);

    }


    static
void ChangeByClassRef(ref Person_ref p)

    {

        p.name =
"new ref name";

        Console.WriteLine("when
calling ChangeByClassRef: p.name = "
+
p.name);

    }


    static
void ChangeByClassNew(Person_ref p_ref_new)

    {

        p_ref_new =
new Person_ref();

        p_ref_new.name =
"new ref name";

        Console.WriteLine("when
calling ChangeByClassNew: p_ref_new.name = "
+
p_ref_new.name);

    }


    static
void ChangeByClassRefNew(ref Person_ref p_new)

    {

        p_new =
new Person_ref();

        p_new.name =
"new ref name";

        Console.WriteLine("when
calling ChangeByClassRefNew: p_new.name = "
+
p_new.name);

    }

}


public struct
Person_val

{

    public
string name;

}


public class
Person_ref

{

    public
string name;

}

则运行结果为:



实验5的运行结果似乎说明即使参数是类(class),只要不加ref,也是传值调用。


下面就引出了我的理解。


2.
没有ref时,即使参数为引用类型(class)时,也可算是一种传值调用


参数为引用类型时,传递的是该引用类型的地址的一份拷贝,“该引用类型的地址的一份拷贝”即为传值调用的“值”。


注意这里说传递的是该引用类型的地址的一份拷贝,而不是引用类型的地址。


下面将用图的形式来说明以上实验3,实验5和实验6中内存的情况。


2.1 首先是实验3


实验3的内存图如下,实参是函数ChangeByClass外的Person_ref对象,形参是函数ChangeByClass内的Person_ref对象。


捕获


从图中我们可以看出实参new出来之后就在托管堆上分配了内存,并且在栈上保存了对象的指针。


调用函数ChangeByClass后,由于没有ref参数,所以将栈上的实参p_val拷贝了一份作为形参,注意这里p_val(实参)p_val(形参)是指向托管堆上的同一地址。


所以说没有ref时,即使参数为引用类型(class)时,也可算是一种传值调用,这里的值就是托管堆中对象的地址(0x1000)。


调用函数ChangeByClass后,通过p_val(形参)修改了name属性的值,由于p_val(实参)p_val(形参)是指向托管堆上的同一地址,所以函数外的p_val(实参)的name属性也被修改了。


捕获


2.2 然后是实验5


上面的实验3从执行结果来看似乎是传引用调用,因为形参的改变导致了实参的改变。


下面的实验5就可以看出,p_val(形参)p_val(实参)并不是同一个变量,而是p_val(实参)的一个拷贝。


捕获


从图中可以看出第一步还是和实验3一样,但是在调用函数ChangeByClassNew后,就不一样了。


函数ChangeByClassNew中,对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100),如下图:


捕获


所以p_val(形参)的name属性改了时候,p_val(实参)的name属性还是没变。


2.3 最后是实验6


我觉得实验6是真正的传引用调用。不废话了,直接上第一个图。


捕获


参数中加了ref关键字之后,其实传递的不是托管堆中对象的地址(0x1000),而是栈上p_val(实参)的地址(0x0001)。


所以这里实参和形参都是栈上的同一个东西,没有什么区别了。我觉得这才是真正的传引用调用。


然后调用了函数ChangeByClassRefNew,函数中对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100)。


捕获


由于p_val(形参)就是p_val(实参),所以p_val(形参)的name属性改变后,函数ChangeByClassRefNew外的p_val(实参)的name属性也被改变了。


而原先分配的对象(地址0x1000)其实已经没有被引用了,随时会被GC回收。


3.
结论


  • 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
  • 如果传递的参数前有ref或者out关键字,那么就是传引用调用。
  • 如果传递的参数是类(class)并且没有ref或out关键字:

  • 如果调用的函数中对参数重新进行了地址分配(new操作),那么执行结果类似传值调用
  • 如果调用的函数中没有对参数重新进行了地址分配,直接就是使用了传递的参数,那么执行结果类似传引用调用
  • posted on 2012-06-07 16:32  higirle  阅读(897)  评论(0编辑  收藏  举报