指针津逮--------浅谈从指针到“ref”

    大凡刚刚接触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 }

在这里,我先举一张表来说明二级指针和一级指针的区别:

指针津逮--------浅谈从指针到“ref” - fanscky - ╄叶子。○珍藏屋╅ 

表的最上端的意思是:任何方法中,实参的值是永远无法被形参所改变,打个比方说,一个二级指针的方法,那么它的实参是指针的地址,我们运行这个方法 时,都是在不改变指针地址的前提下进行,一旦我们在“fun()”中运行这么一条语句:“m=n”那么我们对“m”进行的任何操作,也就对外面的“p”没 有影响了,因为它所作用的对象已经不是存放“p”地址里面的东西了。






执行上述代码时,为了讲解方便,我特拟了一幅草图:

指针津逮--------浅谈从指针到“ref” - fanscky - ╄叶子。○珍藏屋╅ 

当运行到函数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#的学习要知其然更要知其所以然,虽然了解原理并不意味着编程能有多高的技术体现出来。但是可以 帮助我们快速的查找出错误所在。学习原理这块,封装的思想虽然是要运用,亦不可过分依赖封装而不了解其原理,不然学习起来就犹如墙上芦苇,头重脚轻根底 浅,所搭建的代码,也是空中楼阁,华而不实了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值