呃。。。今天甫一上线,就听到丁冬声响,打开消息提示,居然是当选了版主。还好我的心理承受能力不错,要象范进那样,我怕真的有点受不了了。值此国庆之际,真是双喜临门。在这里多谢大家的抬爱,本来呢,我想发表点什么豪言壮语,亦或就职宣言什么的,但是呢, 想了想, 还是贴上一个原创以飨支持我的群友。不足之处,还望大家不吝指教。
大凡刚刚接触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++中,“&”有一种缺陷