指针津逮--------浅谈从指针到“ref”
这时初学者不禁扼腕兴叹,要是没有指针多好!指针有什么用?然而指针被喻为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#的学习要知其然更要知其所以然,虽然了解原理并不意味着编程能有多高的技术体现出来。但是可以帮助我们快速的查找出错误所在。学习原理这块,封装的思想虽然是要运用,亦不可过分依赖封装而不了解其原理,不然学习起来就犹如墙上芦苇,头重脚轻根底浅,所搭建的代码,也是空中楼阁,华而不实了。
转载于:https://www.cnblogs.com/bihailantian/archive/2010/09/29/1838290.html
指针津逮--------浅谈从指针到“ref”相关推荐
- c语言指针很危险,浅谈C语言中指针使用不当的危险性.doc
浅谈C语言中指针使用不当的危险性.doc 第 19 卷 Vol . 19 第 2 期 No . 2 洛阳师专学报 Journal of Luoyang Teachers College 2000 年 ...
- 浅谈react hook ( ref)
import React ,{useRef,useState,PureComponent} from "react"; import ReactDOM from "rea ...
- 浅谈Object Pascal的指针[引用 Nicrosoft]
浅谈Object Pascal的指针 Nicrosoft(nicrosoft@sunistudio.com) -- 2001.8.26 http://www.sunistudio.com/ni ...
- 浅谈C++的智能指针
一转:http://www.cppblog.com/yearner/archive/2008/11/09/66447.html 浅谈C++的智能指针 内存泄露是C++程序员都头疼的大问题.C++缺乏像 ...
- 浅谈Object Pascal的指针
浅谈Object Pascal的指针 作者:Nicrosoft 时间:2001-8-26 来源:Nicrosoft个人网站 大家都认为,C语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上 ...
- c语言弱符号与函数指针,浅谈C语言中的强符号、弱符号、强引用和弱引用【转】...
首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引用.在看到3.5.5节弱符号和强符号时,我感觉有些困惑,所以写下此篇,希望能和同样 ...
- 浅谈C中的指针和数组(一)
本文转载地址:http://www.cnblogs.com/dolphin0520/archive/2011/11/09/2242138.html 在原文的基础上加入自己的想法作为修改. 指针是C/C ...
- 浅谈C/C++中的指针和数组(一)
指针是C/C++的精华,而指针和数组又是一对欢喜冤家,很多时候我们并不能很好的区分指针和数组,对于刚毕业的计算机系的本科生很少有人能够熟练掌握指针以及数组的用法和区别.造成这种原因可能跟现在大学教学以 ...
- 浅谈Go语言(7) - 接口与指针
文章目录 1. 写在前面 2. 接口类型的使用 (1) 定义 (2) 实现 (3) 使用 基本示例 iface (4) 扩展知识 接口变量值nil 接口间的组合 3. 指针 (1) 基本概念 (2) ...
最新文章
- Tomcat V6 Examples移植到Apusic V5.1
- docker教程之从一头雾水到不一头雾水(1)
- c语言大学程序设计题库,黑龙江大学C语言程序设计试题库程序单选
- 《面向模式的软件体系结构2-用于并发和网络化对象模式》读书笔记(12)--- 策略化加锁...
- boost::hana::zip_shortest_with用法的测试程序
- 2015-11-16 入职坑
- 转载:说一下AI的前景吧
- django model中的DateField()转为时间戳
- 易语言下载别人的源码编译出来用不了_我是如何阅读源码的
- utilities(matlab)—— 合成数据(synthesis data)
- 推荐系统入门实践:世纪佳缘会员推荐
- js Object.is 相等判断
- 最好用的进销存软件测评排名
- 大一想去参加培训的学生
- 微信小程序appid的修改方法
- 北大核心期刊2020_职称期刊论文发表 | 会计如何发表北大核心论文
- 计算—六合彩的中奖概率
- 容性耦合等离子体(CCP)和电感耦合等离子体(ICP)
- Oracle官网账号
- confluence是什么
热门文章
- Java代码示例: 使用reflections工具类获取某接口下所有的实现类
- css设置input框长度_干货极致分享浅谈CSS属性,有趣的盒模型。网友:哎呦不错哦!...
- linux hadoop测试,快速搭建Hadoop环境并测试mapreduce
- mysql 测试快生产慢_生产上MySQL慢查询优化实战,SQL优化实战
- INVALID_HANDLE_VALUE 、 NULL、nullptr 和 nullptr_t 的联系
- Tiniux 3.0 / Memory.c / OSMemInit
- 启明云端1.54寸串口屏使用经验分享
- mysql dump gtid_mysqldump导出数据备份 --set-gtid-purged=OFF
- 手机号正则_这20个正则表达式,对你有大帮助,快来Make
- c# html 后台拼_c#编写html后台