今天偶然在csdn論壇看到這麼一篇帖子,就是說有這麼一道面試題,題目如下:
using System;
public class Test1
{
public static void Main()
{
int num = 0;
Person p = new Person("Li");
A1(p, num);
Console.WriteLine("{0},{1}", p.name, num);
}
static void A1(Person p, int num)
{
p = new Person("Wang");
num = 1;
}
}
public class Person
{
public string name;
public Person(string name)
{
this.name = name;
}
}
using System;
public class Test1
{
public static void Main()
{
int num = 0;
Person p = new Person("Li");
A1(p, num);
Console.WriteLine("{0},{1}", p.name, num);
}
static void A1(Person p, int num)
{
p = new Person("Wang");
num = 1;
}
}
public class Person
{
public string name;
public Person(string name)
{
this.name = name;
}
}
說說上面的程序產生的結果,以及產生這個結果的原因是什麼?
帖子上面說面試了十個人,居然有十個人答錯了,貌似非常不正常啊,但是說白了,這裡面主要就是想了解一下面試者對引用傳遞和值傳遞的理解;
我在以前過過一篇關於引用傳遞和值傳遞的博客,地址如下:http://www.BkJia.com/kf/201201/116158.html
今天再就上面的面試題說一說,一方面鞏固自己,另一方面方便心裡沒底的面試者徹底了解;
首先我們得清楚,在C#中,數據類型分為引用類型和值類型,值類型保存在堆棧中,引用類型稍微復雜點,引用類型分為兩部分保存,引用類型的值保存在托管堆中,對該值的引用保存在堆棧中,值和值引用構成了一個完整的引用類型變量;我們經常使用下面的語法聲明變量:
int i=0;
string str = "new string";
int i=0;
string str = "new string";
在i的聲明過程中,系統做了兩件事情,一件事情是在內存的堆棧中找到一個4字節的位置(int類型的長度為4字節),轉換成代碼應該這麼表示:int i= new int();第二件事情是將0賦予i,轉換成代碼應該這麼表示:i=0;綜合下來實際代碼應該如下所示:
int i=new int();
i=0;
int i=new int();
i=0;
在str的聲明過程中,系統也是做了兩件事情,只不過兩件事情的手段不一致,第一件事情是在內存中找了兩個地方,一個地方在托管堆中,用於存儲str的值,另外一個地方在堆棧中,用於指向托管堆的存儲位置;轉換成代碼也是一句話:string str=new string();第二件事也是將new sting這個字符串賦予變量值,但是new string是記錄在托管堆中的,這是與值類型的區別,轉換成代碼應該這麼表示:str=“new string”;綜合下來代碼是一樣的,但是在分配內存時存在的差異就比較大了:
string str=new striing();
str="new string";
string str=new striing();
str="new string";
弄清楚了值類型和引用在內存中的保存方式,我們再來看看方法的參數傳遞,在C#中,所有的參數都是通過值來傳遞的,被調用的方法得到的都是該值的副本;這裡一定要注意:被調用的方法得到的都是該值的副本,也就是說,我們看下面這個方法:
public void MethDouble(int i)
{
return i*2;
}
public void MethDouble(int i)
{
return i*2;
}
下面我們在程序中調用該方法,代碼如下:
int i=3;
MethDouble(i);
在以上的代碼中,系統做了如下事情:首先將變量在內存堆棧中copy一個副本,這個副本的變量名我們假設稱為copy_i,得到副本後,再將副本copy_i傳遞給方法MethDouble執行;所以在以上的過程中,MethDouble方法內部所做的任何事情都不會對變量i產生任何影響;我們再來看看下面的方法:
<span style="white-space:pre"> </span>public void StrDouble(string str)
{
return str+str;
}
<span style="white-space:pre"> </span>public void StrDouble(string str)
{
return str+str;
}
下面我們在程序中調用該方法,代碼如下:
string str = "myString";
StrDouble(str);
string str = "myString";
StrDouble(str);
同理,在以上的代碼中,系統做了如下事情:首先將str在堆棧中的值引用變量copy一個副本,這個副本變量名我們假設稱為copy_str,這個副本是一個引用,該引用指向的地址就是str引用指向的托管堆的地址,也就是托管堆中“myString”值,得到副本後,再將副本copy_str傳遞給方法執行,所以在操作過程中,方法對副本所做的修改都會直接修改托管堆中的值,從而影響方法外的str變量;
明白了以上的原理,我們再來看看開篇的面試題:
int num = 0;
Person p = new Person("Li");
int num = 0;
Person p = new Person("Li");聲明了值類型變量num,引用類型變量p;
A1(p, num);
A1(p, num); 將num的副本和p的引用副本傳遞給方法;
這裡我們可能有疑問了,p的引用傳遞進去了,那麼對p所做的修改應該會影響托管堆中的值啊,我們這裡看看方法A1方法的實現
static void A1(Person p, int num)
{
p = new Person("Wang");
num = 1;
}
static void A1(Person p, int num)
{
p = new Person("Wang");
num = 1;
} 注意了,上面的方法其實是用了一個障眼法,我把方法改改,大家再看看:
static void A1(Person person, int num)
{
person = new Person("Wang");
num = 1;
}
static void A1(Person person, int num)
{
person = new Person("Wang");
num = 1;
} 沒改之前的方法裡面一句p=new Person("wang");大家就以為p在堆棧中的值改變了,其實不然,p僅僅是一個堆棧中的副本,在方法A1中,是用了p=new Person("Wang"),此時系統做了兩件事情:一件是在托管堆中找一個地方,用於存儲p(實際上是下面方法的person)的值,然後將A1方法中的p引用指向該堆棧的位置,此時對A1方法的p所做的任何修改都是在修改第一件事情中找到的托管堆,因為A1方法中的引用指向更改了,所以主函數內的變量p此時指向的托管堆與A1方法內p指向的托管堆分別為不同的位置;所以此時A1方法所做的任何事情對主函數內的變量p都不會造成影響;
所以面試題的結果應該為:Li,0
int num = 0;
Person p = new Person("Li");
int num = 0;
Person p = new Person("Li");
摘自 魯信的專欄