大凡剛剛接觸C語言的人,最頭疼的就是指標和鏈表了,別的變數裡存放的都是“正而八經”的值,這指標呢,偏偏存的就是一地址,用起來還有聲明和定義之別, 聲明是有“*”號的,賦其地址值,定義時是無“*”號方可賦地址值。由於可以直接給其賦記憶體位址,初學者稍有不慎,這指標便如群魔亂舞,使編譯者錯誤迭 出。
這時初學者不禁扼腕興歎,要是沒有指標多好!指標有什麼用?然而指標被喻為C語言的精華,自有其必然之處,例如:
1 void fun(int a)
2 {
3 a=20;
4 }
5 void main()
6 {
7 int a = 10;
8 fun(a)
9 }
想讓a變成20,若把a作為實參直接傳進去經過fun(a)之後出來a依舊是10。改變的只不過是形參的值,欲以此達到效果,無異刻舟求劍。但是如果把a的地址傳進去,即以指標作為實參,則可以達到這個效果:
1 fun(int *p)
2 {
3 *p=20;
4 }
5 void main()
6 {
7 int a = 10;
8 int *p = &a;
9 fun(p);
10 }
此 時改變的,是儲存10這個的空間裡的值。可能有人會問,為什麼不直接讓a=20呢?在這裡的確是可以,打個比方,為了開啟一個A抽屜,有兩種辦法,一種是 將A鑰匙帶在身上,需要是時直接找出該鑰匙開啟抽屜,取出所需的東西,另一種辦法是:為了安全起見,將該A鑰匙放到另一個抽屜B中鎖起來。如果需要開啟A 抽屜,就得先找出B鑰匙(這裡說的鑰匙就是指的地址,抽屜裡的東西,就是*p的值),開啟B抽屜,取出A鑰匙,再開啟A抽屜,取出A抽屜中之物。(譚浩強 C程式設計 第三版 220頁)。我們有時需要用到函數,來達到我們特定的目的,有很多重複的交換,我們可以寫成一個方法。那樣可以削去大量的代碼冗餘,使我們的代碼更洗鍊, 更清晰。指標更大的好處在於一個方法,只能有一個傳回值。若想得到兩個或多個傳回值。這個時候,指標的作用就顯現出來了。我們把想得到的結果以指標變數做 為參數的形式傳遞進去如:
void fun(int* a,int*b)就OK了。
由於指標的這種操作起來的不方便,和管理起來的不安全 性。後來的物件導向語言C#或者是JAVA都有意的屏蔽了指標。但程式員的工作,就是在記憶體上跳舞,不接觸記憶體,能寫出程式嗎?故此.NET提供了一種安 全的方式。不允許把一個地址直接賦給一個變數(但可以通過safe(){…}在特定地區內運用指標,看這樣子就知道,這種方法不被推薦),因此不會出現指 針可以肆意亂指到記憶體的危險地區或保密地區,即便和記憶體打交道,也是通過“CLR”的託管,“CLR”可以自動回收存放記憶體位址資訊的引用變數,也可以檢 測某塊堆空間當前是否有指向它的關聯對象(即“引用”),若此堆空間當前並未被指向,則自動回收。
溯本求源,在C#裡,我們依稀能看到指標的影 子,它,只是變換了一種出場的方式而已,我們熟知的對象名。即“引用”說的就是指標了。它也是在記憶體的棧空間中,開闢出一塊4個位元組大小的空間,裡頭存放 了堆空間中某一地區的首地址。意思亦是同一個“指標”指向了堆空間的特定地區。故此,他山之石,可以攻玉,我們學好了C語言裡的指標,對我們的C#編程也 是大有裨益的。
下面就幾個實踐中遇到的問題,闡述下我對指標的理解。為了方便講解,建立一個windows表單應用程式項目,在表單上拖進一個textBox1文字框和button1按鈕。
寫一個User類:
1 class User
2 {
3 private string m_Name;
4 public string Name
5 {
6 get{return m_Name;}
7 set{m_Name = value;}
8 }
9 private string m_Pwd;
10 public string Pwd
11 {
12 get{return m_Pwd;}
13 set{m_Pwd = value;}
14 }
15 }
在這個類裡有公用欄位:Name和Pwd。再寫一個Users類,
1 class Users
2 {
3 private List<User> userList = new List<User>();
4 public void Add(User user)
5 {
6 userList.Add(user);
7 }
8 public User this[int index]
9 {
10 get{return userList[index];}
11 set{userList[index ] = value;}
12 }
13 public int Count()
14 {
15 return userList.Count;
16 }
17 }
其中有一個集合欄位,現在在button1按鈕的點擊事件中,建立2個User使用者的執行個體往集合中添加,代碼如下:
1 private void button1_Click(object sender, EventArgs e)
2 {
3 User user = new User();
4 Users users = new Users();
5 user.Name =”aaa”;
6 user.Pwd = “111”;
7 users.Add(user);
8 //user = new User();
9 user.Name = “bbb”;
10 user.Pwd = “222”;
11 users.Add(user);
12 textBox1.Text = users.Count().ToString();
13 for(int i =0;i<users.Count();i++)
14 {
15 textBox1.Text += Environment.NewLine + users[i].Name;
16 textBox1.Text += Environment.NewLine + users[i].Pwd;
17 }
18 }
這 時大家可以發現,運行程式,點擊button1按鈕,結果是文字框上顯示是2,也就是說集合裡頭有兩個使用者且其帳號皆為bbb,密碼是222。緣何如此? 我們只執行個體化了一個對象。第一次將其定義為帳號為”aaa”,密碼為”222”的user使用者,並將其添加進了集合users中。我們知道集合中的資訊實 際上並非儲存在集合的堆裡,而是儲存在另外一個記憶體的非託管地區裡,集合的堆中只存放集合所添加元素的地址資訊,也就是產生一個指向非託管地區的指標。故 至此的操作流程是在記憶體的棧中開闢兩塊空間分別存放引用變數“user”和“users”,且在完成“users.Add(user)”之後就在記憶體中新 開闢了一塊地區,即“非託管地區”,用來儲存“user”中的資訊,而集合的堆中只產生一個指標,指向那塊存有“user”資訊的堆。當第2次又添加帳號 為“bbb”,密碼為“222”的使用者時,由於並沒有開闢新的“user”執行個體,所以添加的資訊依舊是上一個執行個體在記憶體中的堆空間,那麼添加到集合的非托 管地區的,也還是那個對應的堆,只是把堆空間裡面的值修改了而已。但是這時在“users”中卻有另一個新的指標指向了那個非託管地區,也就是說,此時 “users”裡有兩個指標同時指向了那個存有“user”資訊的非託管地區。若是把代碼修改下,在添加完第一個使用者之後增加一條代碼“user = new User();”(即上面注釋那條語句取消掉注釋)那麼此指標“user”有了新的堆空間指向,那麼再次添加到“users”中,集合“users”裡就 有兩個指標分別接收不同的堆空間的首地址了,因此“users”裡就有兩條不同的使用者資訊了。這裡我們要注意的是,往集合中添加一次資料,集合中就會有一 個指標指向到添加資料的堆。添加多次,就會有多個指標同時指向到添加資料那個堆。而不是同一個“user”只能往集合中加一次。
上面舉的例子,是直接修改指標指向,若是要通過一個方法修改指標所指向的堆,則是需要“ref”這個關鍵字來修飾了。如在表單類中定義一個方法:
1 private void fun(ref User user)
2 {
3 user = new User();
4 user.Name = “aaa”;
5 user.Pwd = “111”;
6 }
我們把上面的滑鼠點擊事件裡寫的代碼去掉,重新寫入:
private void button1_Click(object sender, EventArgs e)
{
User user = null;
fun(ref user);
textBox1.Text = user.Name;
textBox1.Text += Environment.NewLine + user.Pwd;
}
我 們把“user”這個對象名,以fun(ref user)的方式傳遞進去。由於用”ref”修飾實際上是把”user”這個對象名在棧空間中的地址傳遞進去,那麼修改“fun()”中的“user“實 際上就是等價於修改外面的“user”,也就是相當於以函數修改指標“user”的指向,這種以“ref”的方式傳遞值的,相當於本文開頭所說的直接進行 值傳遞,而區別於指標因為“ref”傳遞時,並未開闢新的空間。只是給user起了一個別名而已,“ref user”就是“user”這個引用的地址。在“fun(ref User user)”中的“user”前“User”只不過是表明“user”的資料類型,而不是聲明!如果沒有“ref”那麼“User user”就是聲明語句,是在棧空間中新開闢一個存指標的地方。所以直接把“user”以實參傳進去,可想而知也是不能達到目的的。這種方式,在C++裡 面也有,不過符號是“&”,這兩種符號都可以稱之為取別名,而別於指標。但是在C++中,“&”有一種缺陷。那就是當聲明一個函數 void fun(int a)和他的重載void fun(int &a)時,調用fun(a)就會報錯,原因是編譯器不知道調用哪個重載(錢能 C++程式設計教程 第191頁)。好在C#裡比較完善,調用時如果是“ref”形式傳實參時必須帶上fun(ref user);這也算是一種革新吧。
上述的原理,我從C語言的角度來解釋下。在C裡,有種變數叫做指向指標的指標,其符號為“**p”;裡頭存放的是指標“*p”的地址。我們來看下面一組代碼:
1 void fun(int **m,int **n)
2 {
3 **m = 50;
4 *n = *m;
5 }
6
7 void main()
8 {
9 int a = 10;
10 int b = 20;
11 int *p = &a;
12 int *t = &b;
13 fun(&p,&t);
14 printf(“%d,\n%d”,a,b);
15 getchar();
16 }
在這裡,我先舉一張表來說明二級指標和一級指標的區別:
表的最上端的意思是:任何方法中,實參的值是永遠無法被形參所改變,打個比方說,一個二級指標的方法,那麼它的實參是指標的地址,我們運行這個方法 時,都是在不改變指標地址的前提下進行,一旦我們在“fun()”中運行這麼一條語句:“m=n”那麼我們對“m”進行的任何操作,也就對外面的“p”沒 有影響了,因為它所作用的對象已經不是存放“p”地址裡面的東西了。
執行上述代碼時,為了講解方便,我特擬了一幅草圖:
當運行到函數fun()中時執行第一行代碼編譯器會先找到“m”裡是傳進來的指標“p”的地址3,繼而找“*m”,發現3裡面是指標“p”指向變數 的地址5,再轉到5的裡面最後找到“**m”,到了5裡面發現是指標“p”所指向地址裡的變數值內容10,並且將其內容改為“50”,接下來就是把 “*m”賦值給“*n”意思是讓“t”也指向5。
這裡強調一下,上面的方法不可以寫成:
1 void fun(int **m,int **n)
2 {
3 **m = 50;
4 int **k;
5 *k = *m;
6 *n = *k;
7 }
這樣調用的話,系統在編譯時間可能沒問題,但是在執行時會報錯, 原因是聲明了一個沒有指向的危險的指標k。這也是為什麼我的表要強調第5列是已經聲明過了的指標意義所在了。
利用這種方法,我們也能達到修改指標指向之目的。
以 上說的是修改指標的指向,要是修改指標指向的堆空間中的資料,則可以直接傳對象名進去,因為對象名本身就是指標,把指標傳進去,雖然新“new”出來的實 例對象是新的,不在同一個棧空間。但是通過傳遞指向的是同一個堆,經函數修改過後。函數外面指向的堆中的值自然也就改了。如:
1 private void fun(User u)
2 {
3 u.Name = “aaa”;
4 u.Pwd = “123”;
5 }
6
7
8 private void button1_Click(object sender, EventArgs e)
9 {
10 User user = new User();
11 fun(user);
12 textBox1.Text = user.Name;
13 textBox1.Text += Environment.NewLine + user.Pwd;
14 }
和
1 private void button1_Click(object sender, EventArgs e)
2 {
3 User user = new User();
4 user.Name = “aaa”;
5 user.Pwd = “123”;
6 textBox1.Text = user.Name;
7 textBox1.Text += Environment.NewLine + user.Pwd;
8 }
效果無異。在這裡,我們要弄清楚堆和棧變數的區別,堆是由指標指向的空間,而棧變數本身並無指標指向。所以堆有指向它的指標指向發生改變和堆自己發生改變之說。
從這些例子中,我們可以看到C#的文法實際上是源自於C的,就好似天下武功出少林一樣,掌握了基本的C文法,就如同練功要先練馬步一樣,下盤根底紮實了,才能追求更高的造詣。學好指標,就是鍛煉我們的基本功。再今後遇到問題時,定能劍鋒所指,擋者披靡。
後跋
終 於寫完了!修改了6,7個小時。。雖不似“兩句三年得,一吟雙淚流”。不過看著自己的學習心得完工,真是舒暢。“津逮”,原意是指從渡口乘船至目的地,引 申為學習的門徑。自古文是“以載道”的,本人才疏學淺,肚子裡存貨太少,寫的時候又要考慮舉例的抽象性和歸類相似避免舉出重複例子,又要考慮行文的連貫和 邏輯性,把相似的歸類撰述;語言還要盡量表述準確。寫的真是比古人所說“吟得一句詩,撚斷數根須”還難受。這篇小文章,若能對讀者朋友有一點拋磚引玉的引 導作用,愚願足以。掌握好C是學好物件導向語言的基礎。C#的學習要知其然更要知其所以然,雖然瞭解原理並不意味著編程能有多高的技術體現出來。但是可以 協助我們快速的尋找出錯誤所在。學習原理這塊,封裝的思想雖然是要運用,亦不可過分依賴封裝而不瞭解其原理,不然學習起來就猶如牆上蘆葦,頭重腳輕根底 淺,所搭建的代碼,也是空中樓閣,華而不實了。