指针津逮--------浅谈从指针到“ref”
呃。。。今天甫一上线,就听到丁冬声响,打开消息提示,居然是当选了版主。还好我的心理承受能力不错,要象范进那样,我怕真的有点受不了了。值此国庆之际,真是双喜临门。在这里多谢大家的抬爱,本来呢,我想发表点什么豪言壮语,抑或就职宣言什么的,但是呢, 想了想, 还是贴上一个原创以飨支持我的群友。不足之处,还望大家不吝指教。大凡刚刚接触C语言的人,最头疼的就是指针和链表了,别的变量里存放的都是“正而八经”的值,这指针呢,偏偏存的就是一地址,用起来还有声明和定义之别,声明是有“*”号的,赋其地址值,定义时是无“*”号方可赋地址值。由于可以直接给其赋内存地址,初学者稍有不慎,这指针便如群魔乱舞,使编译者错误迭出。
这时初学者不禁扼腕兴叹,要是没有指针多好!指针有什么用?然而指针被喻为C语言的精华,自有其必然之处,例如:
void fun(int a)
{
a=20;
}
void main()
{
int a = 10;
fun(a)
}
想让a变成20,若把a作为实参直接传进去经过fun(a)之后出来a依旧是10。改变的只不过是形参的值,欲以此达到效果,无异刻舟求剑。但是如果把a的地址传进去,即以指针作为实参,则可以达到这个效果:
fun(int *p)
{
*p=20;
}
void main()
{
int a = 10;
int *p = &a;
fun(p);
}
此时改变的,是存储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类:
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;}
}
}
在这个类里有公共字段:Name和Pwd。再写一个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;
}
}
其中有一个集合字段,现在在button1按钮的点击事件中,建立2个User用户的实例往集合中添加,代码如下:
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;
}
}
这时大家可以发现,运行程序,点击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”这个关键字来修饰了。如在窗体类中定义一个方法:
private void fun(ref User user)
{
user = new User();
user.Name = “aaa”;
user.Pwd = “111”;
}
我们把上面的鼠标点击事件里写的代码去掉,重新写入:
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”的地址。我们来看下面一组代码:
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();
}
在这里,我先举一张表来说明二级指针和一级指针的区别:
表的最上端的意思是:任何方法中,实参的值是永远无法被形参所改变,打个比方说,一个二级指针的方法,那么它的实参是指针的地址,我们运行这个方法时,都是在不改变指针地址的前提下进行,一旦我们在“fun()”中运行这么一条语句:“m=n”那么我们对“m”进行的任何操作,也就对外面的“p”没有影响了,因为它所作用的对象已经不是存放“p”地址里面的东西了。
执行上述代码时,为了讲解方便,我特拟了一幅草图:
当运行到函数fun()中时执行第一行代码编译器会先找到“m”里是传进来的指针“p”的地址3,继而找“*m”,发现3里面是指针“p”指向变量的地址5,再转到5的里面最后找到“**m”,到了5里面发现是指针“p”所指向地址里的变量值内容10,并且将其内容改为“50”,接下来就是把 “*m”赋值给“*n”意思是让“t”也指向5。
这里强调一下,上面的方法不可以写成:
void fun(int **m,int **n)
{
**m = 50;
int **k;
*k = *m;
*n = *k;
}
这样调用的话,系统在编译时可能没问题,但是在执行时会报错, 原因是声明了一个没有指向的危险的指针k。这也是为什么我的表要强调第5列是已经声明过了的指针意义所在了。
利用这种方法,我们也能达到修改指针指向之目的。
以上说的是修改指针的指向,要是修改指针指向的堆空间中的数据,则可以直接传对象名进去,因为对象名本身就是指针,把指针传进去,虽然新“new”出来的实例对象是新的,不在同一个栈空间。但是通过传递指向的是同一个堆,经函数修改过后。函数外面指向的堆中的值自然也就改了。如:
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;
}
效果无异。在这里,我们要弄清楚堆和栈变量的区别,堆是由指针指向的空间,而栈变量本身并无指针指向。所以堆有指向它的指针指向发生改变和堆自己发生改变之说。
从这些例子中,我们可以看到C#的语法实际上是源自于C的,就好似天下武功出少林一样,掌握了基本的C语法,就如同练功要先练马步一样,下盘根底扎实了,才能追求更高的造诣。学好指针,就是锻炼我们的基本功。再今后遇到问题时,定能剑锋所指,挡者披靡。
后跋
终于写完了!修改了6,7个小时。。虽不似“两句三年得,一吟双泪流”。不过看着自己的学习心得完工,真是舒畅。“津逮”,原意是指从渡口乘船至目的地,引申为学习的门径。自古文是“以载道”的,本人才疏学浅,肚子里存货太少,写的时候又要考虑举例的抽象性和归类相似避免举出重复例子,又要考虑行文的连贯和逻辑性,把相似的归类撰述;语言还要尽量表述准确。写的真是比古人所说“吟得一句诗,捻断数根须”还难受。这篇小文章,若能对读者朋友有一点抛砖引玉的引导作用,愚愿足以。掌握好C是学好面向对象语言的基础。C#的学习要知其然更要知其所以然,虽然了解原理并不意味着编程能有多高的技术体现出来。但是可以帮助我们快速的查找出错误所在。学习原理这块,封装的思想虽然是要运用,亦不可过分依赖封装而不了解其原理,不然学习起来就犹如墙上芦苇,头重脚轻根底浅,所搭建的代码,也是空中楼阁,华而不实了。
[[it] 本帖最后由 小仙 于 2008-10-3 21:30 编辑 [/it]]