使用条件:
(1)使用引用参数时,必须在方法的声明和调用中都使用ref修饰符 例如:void foo(ref int value) 调用foo(ref y)
(2)实参必须是变量,在用做实参前必须被赋值,如果是引用类型变量,可以赋值为一个引用或null
引用参数与值参数的区别:
对于值参数,系统在栈上为形参分配内存,相反,引用参数具有以下特征
(1)不会为形参在栈上分配内存
(2)实际情况是,形参的参数名将作为实参变量的别名,指向相同的内存位置(由于形参名和实参名的行为就好像指向相同的内存位置,所以在方法的执行过程中对形参作的任何改变在方法完成后依然有效
如图所示,f1,f2分别为a1,a2的别名。
引用类型ref与函数中的值参数之间的使用
值参数只是传递单纯的值,它没有引用,只存储在栈上,与C++相同,如果函数的参数是一个值参数,那么在调用的时候会在栈上新开辟一个空间来存储这个形参,你可以对它进行操作,但是这个函数生命周期结束后,这个栈空间也会随之销毁,所以它不会对实参产生影响,如果要产生影响则需要将这个参数申明为ref引用类型(C++则需声明为type&)
将值参数声明为值类型:
class Program
{
public static void foo(int i)
{
i += 10;
}
static void Main(string[] args)
{
int i = 1;
foo(i);
Console.WriteLine(i); //1
}
}
将值参数声明为引用(ref)类型:
class Program
{
public static void foo(ref int i)
{
i += 10;
}
static void Main(string[] args)
{
int i = 1;
foo(ref i);
Console.WriteLine(i); //11
}
}
引用类型ref与函数中的引用参数之间的使用
C#中所谓引用参数,就是指参数是一个引用类型,例如C#中的类(class)就是一个引用类型,所以在函数中我们可以不声明为ref来直接更改类中字段值。
修改栈上引用所指向的实际数据的值:
public class A
{
public int i = 10; //字段初始值为10
}
class Program
{
public static void foo(A aa) //此时与声明成 foo(ref A aa)效果相同
{
aa.i = 100; //对该字段进行重新赋值为100
}
static void Main(string[] args)
{
A a = new A();
A a1 = a;
Console.WriteLine(a.i); //10
Console.WriteLine(a1.i); //10
foo(a); //与foo(ref a)写法效果相同
Console.WriteLine(a.i); //100
Console.WriteLine(a1.i); //100
a1.i = 99;
Console.WriteLine(a.i); //99
Console.WriteLine(a1.i); //99
}
}
在看上述代码前首先要理清楚,C#在new出对象之后,以及相同类型的对象赋值后在内存上的存储关系。
如之前文章所写,C#在new出对象之后,是在栈上存放一个引用,它所指向的实际数据是在堆中存放,如断点显示,&a &a1为各自所在栈上的地址,它们的地址并不相同,因为这是俩个空间来存放引用,但是*&a和*&a1的值却是相同,这代表它们存放的地址是堆上的同一个地址,所以它们指向内存中的同一块区域,而当调用到foo函数的时候,因为参数是非引用类型的引用参数,所以与A a1=a;类似,相当于在栈上另外开辟了一块空间存放引用a的拷贝,它们之间的地址互不相同,但确都指向堆中同一块内存,然后对aa进行i的赋值,则会影响到实际堆中数据,所以打印出的i的值为100,这和a1,i=99;是相同的道理,在C++中类似的非引用传递会在函数结束后对函数参数所在的栈空间进行销毁,但C#中个人不确定在foo函数结束后aa是否会被销毁,待以后再确认。(以上为个人见解,若有错误请指正)
修改栈上引用的指向(重新定义栈上引用所指向的地址):
public class A
{
public int i = 10; //字段初始值为10
}
class Program
{
public static void foo(A aa)
{
aa = new A();
}
static void Main(string[] args)
{
A a = new A();
A a1 = a;
Console.WriteLine(a.i); //10
Console.WriteLine(a1.i); //10
a.i = 100;
Console.WriteLine(a.i); //100
Console.WriteLine(a1.i); //100
foo(a);
Console.WriteLine(a.i); //100
Console.WriteLine(a1.i); //100
a1.i = 99;
Console.WriteLine(a.i); //99
Console.WriteLine(a1.i); //99
}
}
此处代码不同的是在foo函数中是对aa的重新赋值,也就是更改了aa的指向,当还没有走到aa=new A();时,和上个例子一样,aa为a的一个拷贝,还是会指向实际数据,但经过此赋值之后,aa就不再指向实际数据,而是指向了实际数据2,一个新的地址,它对应的i为初始值10,而原有的a和a1还是保持i为100不变,所以打印结果为注释所示。
public class A
{
public int i = 10; //字段初始值为10
}
class Program
{
public static void foo(ref A aa)
{
aa = new A();
}
static void Main(string[] args)
{
A a = new A();
A a1 = a;
Console.WriteLine(a.i); //10
Console.WriteLine(a1.i); //10
a.i = 100;
Console.WriteLine(a.i); //100
Console.WriteLine(a1.i); //100
foo(ref a);
Console.WriteLine(a.i); //10
Console.WriteLine(a1.i); //100
a1.i = 99;
Console.WriteLine(a.i); //10
Console.WriteLine(a1.i); //99
}
}
注意,此时已经将foo函数的参数声明为引用ref类型,此时再次调用foo的时候,传递的就是a的引用了,而不再是拷贝一份a的副本,这里会感觉有些别扭,传递的参数为"引用类型的引用",这时aa的地址就是a的地址,对aa进行的操作就是对a的操作,所以,当进行aa=new A();时,会将a重新定向,让它指向一个新区域也就是实际数据2,它的i值为初始值10,所以自此之后a和a1毫无瓜葛,各走各路,输出如注释所示。
C#中ref的逻辑类似C++中一级指针、二级指针,在C++中可以通过一级指针修改0级指针的内容,可以通过二级指针修改一级指针的指向,可以通过N级指针修改N-1级指针的指向,同理如果修改引用的指向,则需要声明为该引用的引用,如果只修改引用对应的值,则直接利用该引用即可修改。
另外,在C#中,有引用计数的概念,例如a和a1都指向实际数据,则实际数据的引用计数为2,直到它的引用计数为0的时候,系统才会释放调这块内存,(疑问:有没有哪种方式可以释放掉a所对应的堆中内存,那释放掉之后是不是a1对应的内存也被释放了,还是仅仅是实际数据的引用计数-1而已,a1的内存还会存在?)