指针津逮--------浅谈从指针到“ref”
這時(shí)初學(xué)者不禁扼腕興嘆,要是沒有指針多好!指針有什么用?然而指針被喻為C語言的精華,自有其必然之處,例如:
void fun(int a)
{
a=20;
}
void main()
{
????int a = 10;
????fun(a)
}
想讓a變成20,若把a(bǔ)作為實(shí)參直接傳進(jìn)去經(jīng)過fun(a)之后出來a依舊是10。改變的只不過是形參的值,欲以此達(dá)到效果,無異刻舟求劍。但是如果把a(bǔ)的地址傳進(jìn)去,即以指針作為實(shí)參,則可以達(dá)到這個(gè)效果:
fun(int *p)
{
*p=20;
}
void main()
{
int a = 10;
int *p = &a;
fun(p);
}
此時(shí)改變的,是存儲(chǔ)10這個(gè)的空間里的值??赡苡腥藭?huì)問,為什么不直接讓a=20呢?在這里的確是可以,打個(gè)比方,為了打開一個(gè)A抽屜,有兩種辦法,一種是將A鑰匙帶在身上,需要是時(shí)直接找出該鑰匙打開抽屜,取出所需的東西,另一種辦法是:為了安全起見,將該A鑰匙放到另一個(gè)抽屜B中鎖起來。如果需要打開A抽屜,就得先找出B鑰匙(這里說的鑰匙就是指的地址,抽屜里的東西,就是*p的值),打開B抽屜,取出A鑰匙,再打開A抽屜,取出A抽屜中之物。(譚浩強(qiáng) C程序設(shè)計(jì) 第三版 220頁(yè))。我們有時(shí)需要用到函數(shù),來達(dá)到我們特定的目的,有很多重復(fù)的交換,我們可以寫成一個(gè)方法。那樣可以削去大量的代碼冗余,使我們的代碼更洗練,更清晰。指針更大的好處在于一個(gè)方法,只能有一個(gè)返回值。若想得到兩個(gè)或多個(gè)返回值。這個(gè)時(shí)候,指針的作用就顯現(xiàn)出來了。我們把想得到的結(jié)果以指針變量做為參數(shù)的形式傳遞進(jìn)去如:
void fun(int* a,int*b)就OK了。
由于指針的這種操作起來的不方便,和管理起來的不安全性。后來的面向?qū)ο笳Z言C#或者是JAVA都有意的屏蔽了指針。但程序員的工作,就是在內(nèi)存上跳舞,不接觸內(nèi)存,能寫出程序嗎?故此.NET提供了一種安全的方式。不允許把一個(gè)地址直接賦給一個(gè)變量(但可以通過safe(){…}在特定區(qū)域內(nèi)運(yùn)用指針,看這樣子就知道,這種方法不被推薦),因此不會(huì)出現(xiàn)指針可以肆意亂指到內(nèi)存的危險(xiǎn)區(qū)域或保密區(qū)域,即便和內(nèi)存打交道,也是通過“CLR”的托管,“CLR”可以自動(dòng)回收存放內(nèi)存地址信息的引用變量,也可以檢測(cè)某塊堆空間當(dāng)前是否有指向它的關(guān)聯(lián)對(duì)象(即“引用”),若此堆空間當(dāng)前并未被指向,則自動(dòng)回收。
溯本求源,在C#里,我們依稀能看到指針的影子,它,只是變換了一種出場(chǎng)的方式而已,我們熟知的對(duì)象名。即“引用”說的就是指針了。它也是在內(nèi)存的棧空間中,開辟出一塊4個(gè)字節(jié)大小的空間,里頭存放了堆空間中某一區(qū)域的首地址。意思亦是同一個(gè)“指針”指向了堆空間的特定區(qū)域。故此,他山之石,可以攻玉,我們學(xué)好了C語言里的指針,對(duì)我們的C#編程也是大有裨益的。
下面就幾個(gè)實(shí)踐中遇到的問題,闡述下我對(duì)指針的理解。為了方便講解,新建一個(gè)windows窗體應(yīng)用程序項(xiàng)目,在窗體上拖進(jìn)一個(gè)textBox1文本框和button1按鈕。
寫一個(gè)User類:
class User
{
private string m_Name;
public string Name
{
????get{return m_Name;}
????set{m_Name = value;}
}
private string m_Pwd;
public string Pwd
{
????get{return m_Pwd;}
????set{m_Pwd = value;}
}????????????
}
在這個(gè)類里有公共字段:Name和Pwd。再寫一個(gè)Users類,
class Users
{
????private List<User> userList = new List<User>();
????public void Add(User user)
????{
????????userList.Add(user);
}
public User this[int index]
{
????get{return userList[index];}
????set{userList[index ] = value;}
}
public int Count()
{
????return userList.Count;
}
}
其中有一個(gè)集合字段,現(xiàn)在在button1按鈕的點(diǎn)擊事件中,建立2個(gè)User用戶的實(shí)例往集合中添加,代碼如下:
private void button1_Click(object sender, EventArgs e)
{
User user = new User();
Users users = new Users();
user.Name =”aaa”;
user.Pwd = “111”;
users.Add(user);
//user = new User();
user.Name = “bbb”;
user.Pwd = “222”;
users.Add(user);
textBox1.Text = users.Count().ToString();
for(int i =0;i<users.Count();i++)
{
????textBox1.Text += Environment.NewLine + users[i].Name;
????textBox1.Text += Environment.NewLine + users[i].Pwd;
}
}
這時(shí)大家可以發(fā)現(xiàn),運(yùn)行程序,點(diǎn)擊button1按鈕,結(jié)果是文本框上顯示是2,也就是說集合里頭有兩個(gè)用戶且其帳號(hào)皆為bbb,密碼是222。緣何如此?我們只實(shí)例化了一個(gè)對(duì)象。第一次將其定義為帳號(hào)為”aaa”,密碼為”222”的user用戶,并將其添加進(jìn)了集合users中。我們知道集合中的信息實(shí)際上并非存儲(chǔ)在集合的堆里,而是存儲(chǔ)在另外一個(gè)內(nèi)存的非托管區(qū)域里,集合的堆中只存放集合所添加元素的地址信息,也就是生成一個(gè)指向非托管區(qū)域的指針。故至此的操作流程是在內(nèi)存的棧中開辟兩塊空間分別存放引用變量“user”和“users”,且在完成“users.Add(user)”之后就在內(nèi)存中新開辟了一塊區(qū)域,即“非托管區(qū)域”,用來存儲(chǔ)“user”中的信息,而集合的堆中只生成一個(gè)指針,指向那塊存有“user”信息的堆。當(dāng)?shù)?次又添加帳號(hào)為“bbb”,密碼為“222”的用戶時(shí),由于并沒有開辟新的“user”實(shí)例,所以添加的信息依舊是上一個(gè)實(shí)例在內(nèi)存中的堆空間,那么添加到集合的非托管區(qū)域的,也還是那個(gè)對(duì)應(yīng)的堆,只是把堆空間里面的值修改了而已。但是這時(shí)在“users”中卻有另一個(gè)新的指針指向了那個(gè)非托管區(qū)域,也就是說,此時(shí)“users”里有兩個(gè)指針同時(shí)指向了那個(gè)存有“user”信息的非托管區(qū)域。若是把代碼修改下,在添加完第一個(gè)用戶之后增加一條代碼“user = new User();”(即上面注釋那條語句取消掉注釋)那么此指針“user”有了新的堆空間指向,那么再次添加到“users”中,集合“users”里就有兩個(gè)指針分別接收不同的堆空間的首地址了,因此“users”里就有兩條不同的用戶信息了。這里我們要注意的是,往集合中添加一次數(shù)據(jù),集合中就會(huì)有一個(gè)指針指向到添加數(shù)據(jù)的堆。添加多次,就會(huì)有多個(gè)指針同時(shí)指向到添加數(shù)據(jù)那個(gè)堆。而不是同一個(gè)“user”只能往集合中加一次。
上面舉的例子,是直接修改指針指向,若是要通過一個(gè)方法修改指針?biāo)赶虻亩?#xff0c;則是需要“ref”這個(gè)關(guān)鍵字來修飾了。如在窗體類中定義一個(gè)方法:
private void fun(ref User user)
{
user = new User();
user.Name = “aaa”;
user.Pwd = “111”;
}
我們把上面的鼠標(biāo)點(diǎn)擊事件里寫的代碼去掉,重新寫入:
????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”這個(gè)對(duì)象名,以fun(ref user)的方式傳遞進(jìn)去。由于用”ref”修飾實(shí)際上是把”user”這個(gè)對(duì)象名在??臻g中的地址傳遞進(jìn)去,那么修改“fun()”中的“user“實(shí)際上就是等價(jià)于修改外面的“user”,也就是相當(dāng)于以函數(shù)修改指針“user”的指向,這種以“ref”的方式傳遞值的,相當(dāng)于本文開頭所說的直接進(jìn)行值傳遞,而區(qū)別于指針因?yàn)椤皉ef”傳遞時(shí),并未開辟新的空間。只是給user起了一個(gè)別名而已,“ref user”就是“user”這個(gè)引用的地址。在“fun(ref User user)”中的“user”前“User”只不過是表明“user”的數(shù)據(jù)類型,而不是聲明!如果沒有“ref”那么“User user”就是聲明語句,是在??臻g中新開辟一個(gè)存指針的地方。所以直接把“user”以實(shí)參傳進(jìn)去,可想而知也是不能達(dá)到目的的。這種方式,在C++里面也有,不過符號(hào)是“&”,這兩種符號(hào)都可以稱之為取別名,而別于指針。但是在C++中,“&”有一種缺陷。那就是當(dāng)聲明一個(gè)函數(shù)void fun(int a)和他的重載void fun(int &a)時(shí),調(diào)用fun(a)就會(huì)報(bào)錯(cuò),原因是編譯器不知道調(diào)用哪個(gè)重載(錢能 C++程序設(shè)計(jì)教程 第191頁(yè))。好在C#里比較完善,調(diào)用時(shí)如果是“ref”形式傳實(shí)參時(shí)必須帶上fun(ref user);這也算是一種革新吧。
????上述的原理,我從C語言的角度來解釋下。在C里,有種變量叫做指向指針的指針,其符號(hào)為“**p”;里頭存放的是指針“*p”的地址。我們來看下面一組代碼:
void fun(int **m,int **n)
{
????**m = 50;
????*n = *m;
}
void main()
{
????int a = 10;
????int b = 20;
????int *p = &a;
????int *t = &b;
????fun(&p,&t);
????printf(“%d,\n%d”,a,b);
????getchar();
}
在這里,我先舉一張表來說明二級(jí)指針和一級(jí)指針的區(qū)別:
?
表的最上端的意思是:任何方法中,實(shí)參的值是永遠(yuǎn)無法被形參所改變,打個(gè)比方說,一個(gè)二級(jí)指針的方法,那么它的實(shí)參是指針的地址,我們運(yùn)行這個(gè)方法時(shí),都是在不改變指針地址的前提下進(jìn)行,一旦我們?cè)凇癴un()”中運(yùn)行這么一條語句:“m=n”那么我們對(duì)“m”進(jìn)行的任何操作,也就對(duì)外面的“p”沒有影響了,因?yàn)樗饔玫膶?duì)象已經(jīng)不是存放“p”地址里面的東西了。
執(zhí)行上述代碼時(shí),為了講解方便,我特?cái)M了一幅草圖:
?
當(dāng)運(yùn)行到函數(shù)fun()中時(shí)執(zhí)行第一行代碼編譯器會(huì)先找到“m”里是傳進(jìn)來的指針“p”的地址3,繼而找“*m”,發(fā)現(xiàn)3里面是指針“p”指向變量的地址5,再轉(zhuǎn)到5的里面最后找到“**m”,到了5里面發(fā)現(xiàn)是指針“p”所指向地址里的變量值內(nèi)容10,并且將其內(nèi)容改為“50”,接下來就是把 “*m”賦值給“*n”意思是讓“t”也指向5。
這里強(qiáng)調(diào)一下,上面的方法不可以寫成:
void fun(int **m,int **n)
{
????**m = 50;
int **k;
????*k = *m;
????*n = *k;
}
這樣調(diào)用的話,系統(tǒng)在編譯時(shí)可能沒問題,但是在執(zhí)行時(shí)會(huì)報(bào)錯(cuò), 原因是聲明了一個(gè)沒有指向的危險(xiǎn)的指針k。這也是為什么我的表要強(qiáng)調(diào)第5列是已經(jīng)聲明過了的指針意義所在了。
利用這種方法,我們也能達(dá)到修改指針指向之目的。
????以上說的是修改指針的指向,要是修改指針指向的堆空間中的數(shù)據(jù),則可以直接傳對(duì)象名進(jìn)去,因?yàn)閷?duì)象名本身就是指針,把指針傳進(jìn)去,雖然新“new”出來的實(shí)例對(duì)象是新的,不在同一個(gè)??臻g。但是通過傳遞指向的是同一個(gè)堆,經(jīng)函數(shù)修改過后。函數(shù)外面指向的堆中的值自然也就改了。如:
????private void fun(User u)
{
????u.Name = “aaa”;
????u.Pwd = “123”;
}
private void button1_Click(object sender, EventArgs e)
{
????User user = new User();
????fun(user);
????????textBox1.Text = user.Name;
????????textBox1.Text += Environment.NewLine + user.Pwd;
}
和
private void button1_Click(object sender, EventArgs e)
{
????User user = new User();
????user.Name = “aaa”;
????user.Pwd = “123”;
textBox1.Text = user.Name;
????????textBox1.Text += Environment.NewLine + user.Pwd;
}
效果無異。在這里,我們要弄清楚堆和棧變量的區(qū)別,堆是由指針指向的空間,而棧變量本身并無指針指向。所以堆有指向它的指針指向發(fā)生改變和堆自己發(fā)生改變之說。
從這些例子中,我們可以看到C#的語法實(shí)際上是源自于C的,就好似天下武功出少林一樣,掌握了基本的C語法,就如同練功要先練馬步一樣,下盤根底扎實(shí)了,才能追求更高的造詣。學(xué)好指針,就是鍛煉我們的基本功。再今后遇到問題時(shí),定能劍鋒所指,擋者披靡。
???????????????
后跋
終于寫完了!修改了6,7個(gè)小時(shí)。。雖不似“兩句三年得,一吟雙淚流”。不過看著自己的學(xué)習(xí)心得完工,真是舒暢?!敖虼?#xff0c;原意是指從渡口乘船至目的地,引申為學(xué)習(xí)的門徑。自古文是“以載道”的,本人才疏學(xué)淺,肚子里存貨太少,寫的時(shí)候又要考慮舉例的抽象性和歸類相似避免舉出重復(fù)例子,又要考慮行文的連貫和邏輯性,把相似的歸類撰述;語言還要盡量表述準(zhǔn)確。寫的真是比古人所說“吟得一句詩(shī),捻斷數(shù)根須”還難受。這篇小文章,若能對(duì)讀者朋友有一點(diǎn)拋磚引玉的引導(dǎo)作用,愚愿足以。掌握好C是學(xué)好面向?qū)ο笳Z言的基礎(chǔ)。C#的學(xué)習(xí)要知其然更要知其所以然,雖然了解原理并不意味著編程能有多高的技術(shù)體現(xiàn)出來。但是可以幫助我們快速的查找出錯(cuò)誤所在。學(xué)習(xí)原理這塊,封裝的思想雖然是要運(yùn)用,亦不可過分依賴封裝而不了解其原理,不然學(xué)習(xí)起來就猶如墻上蘆葦,頭重腳輕根底淺,所搭建的代碼,也是空中樓閣,華而不實(shí)了。
?
轉(zhuǎn)載于:https://www.cnblogs.com/bihailantian/archive/2010/09/29/1838290.html
總結(jié)
以上是生活随笔為你收集整理的指针津逮--------浅谈从指针到“ref”的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Web.config中连接字符串的加密和
- 下一篇: Invalid URI