2006年3月25日

关于值/引用传递的一点深入理解

关于值传递和引用传递,第一次在考卷之外认识到它的重要性是在去年八月做类分析器的演习项目中,在那之后仔细翻阅了钱能的那本C++教材,觉得它讲得不够彻底,于是又去翻阅了谭浩强的那本著名的C语言教材,基本上能够从本质上认清了值/引用传递,前两天在翻阅Deitel父子的<C# for Experienced Programmers>时再一次巩固了对值/引用传递的认识,遂作此文。

函数(方法)传值,从本质上来说其实是只有一种机制——将实参在栈中生成一个备份,称作"形参"。然后在栈中展开函数,对形参进行操作。函数中所有对形参的更改都不会保留到实参中。比如下面这个常见的例子:

void swap(int, int);

void main()
{
int a=3;
int b=5;
swap(a, b);

cout << "a=" << a << "; b=" << b <<endl;
}

void swap(int var1, int var2)

{
int temp = var1;
var1 = var2;
var2 = temp;
}

在上面的例子中,本意是想通过调用swap函数来更改a,b的值,结果因为函数中只对形参进行操作,当函数调用结束,栈被清空,存储在内存数据段的a,b值没有受到任何影响。

这时,如果对上 面的swap函数进行如下调整:

void swap(int *p1, int *p2)
{
int temp = (*p1);
(*p1) = (*p2);
(*p2) = temp;
}

对应的函数调用也应该改为 swap(&a, &b);再次输出结果就会看出变量a,b的值的确被对调过了。

BUT WHY?

原因很简单,关键就在于更改后的函数签名中的参数已经不再是单纯的int型,而是int型指针——说白了,就是两个内存地址。于是,当swap函数被调用时。变量a和b的地址会各被复制一份用作函数的形参。而在上例的函数体中,进行交换的则是两个地址中所存储的数值。而这一点是相当重要的,因为保存有两个地址的形参仍然会在函数调用结束后从栈中清空,但由于两个地址是数据段的地址,而在形参被清空之前,两个数据段地址实际保存的值已经被交换了(就是变量a,b的地址所保存的值被交换),所以在函数调用结束后可以看出a和b的值被交换过了

由于很多资料对引用传递的机制 表述很含糊,就会读者造成这么一种错觉——如果传给参数的是指针,那么在函数体内操纵的就是实际的指针。因此,下面一种错误就出来了。

void swap(int *p1, int *p2)
{
int *temp = p1;
p1 = p2;
p2 = temp;
}

调用函数的方法依旧为swap(&a, &b);作者本以为函数体内操纵的就是a,b的实际地址,所以预料输出结果应该是正确的。但实际上,它在swap函数内操作的仍旧为a, b地址在栈中的备份。

现在来看看小wing在去年八月所犯下的一个错误:我想要写一个函数来生成一个单链表的头节点,所以写了如下方法(之前已经定义了节点 Node——是一个结构体):

int GetFirstNode(Node *pHead)
{
if (pHead)
{
return 0;
}
else
{
pHead=(Node*)malloc(sizeof (Node));
// Assign the value to the members of Node
... ...
return 1;
}
}

当我调用该函数时,我首先让头节点指针pHead为NULL,之后通过int flag = GetFirstNode(pHead)的方式来调用函数。满以为调用结束后,pHead会指向新生成的节点,结果一跟踪却 发现pHead仍旧为 NULL。

当然,现在来看这段代码很容易就能发现问题的所在(这是一段比较危险的代码):由于pHead一开始为空,调用函数时,pHead的形参自然也是为空,之后的代码中却改变了 pHead的值(是个地址)。但是调用结束后,形参被销毁,pHead没有变化。事实上,这段代码中,pHead 没有正确指向预期的地址倒是小时,关键问题是在函数体中调用了malloc在堆上申请了空间后却没有任何指针可以控制它,更不用说去释放这段内存空间。这便造成了烦人的内存泄漏。

实际上,这段代 码可以进行如下修正就可以解决这个问题:

int GetFirstNode(Node **pHeadPoint)
{
if ((*pHead))
{
return 0;
}
else
{
(*pHead)=(Node*)malloc(sizeof (Node));
// Assign the value to the members of Node
... ...
return 1;
}
}

实际调用时,假设一开始声明仍旧是Node * pHead = NULL;那么就可以利用 GetFirstNode(&pHead)来让pHead指向新生成的节点。因为指向pHead的指针(即双重指针)在函数体中没有发生变化。

SO...

到现在,我们就可以得出这样一个结论——
值传递 和引用传递在本质上没有区别,都是利用函数的形参。关键在于,如果在函数体中想要改变一个外部变量(可能是个基本数据类型变量,也有可能是一个指针变量)的值,并且希望这种改变能够在函数调用完成之后仍旧被保持。那么,就请将该外部变量的地址作为函数的参数传进去。

另外,由于C#的托管代码中没有指针和取址符&amp;amp;所以C#提供了两个特有的关键字ref和out,前者更像C/C++中的取址符 &,而后者则更能应付在调用函数前没有经过初始化的变量。而C#中需要记住的另一个原则就是,任何class类型的变量其本质都是一个指针——即变量所保存的值其实是一个指向该class实例的地址。

2006年3月,"滚"进中国~~~



来了,全球流行文化的先锋——《滚石》来了!

大学时代每个月埋头“苦读”《通俗歌曲》时,经常会看到上面的一些文章或者乐评都会提到大洋彼岸的一本名为《滚石》的杂志,查了一些资料,知道了这本杂志原来是诞生于嬉皮士运动的鼎盛时代,是一本介绍摇滚乐以及西方文化的杂志。于是,当时的我便不由地羡慕起西方的年轻人来:他们真是幸福,可以有这样的一本杂志看。

今天,路过路边的一家书店,偶然瞥见一张海报——“Rolling Stone 3月震撼上市”。宛如打了一针强心针,我浑身的血液立即沸腾了起来——传说中的《滚石》中文版终于来了……

后面的事情可以预料,我毫不犹疑地掏出钱包,付了20元钱,买了店里最后的一本(据老板说一共只进了5本……).

回家翻开了杂志,一种一见如故的感觉油然而生。又看到了列侬、又看到了娄·里德、又看到了米克·贾格尔这帮家伙,呵呵,犹如老朋友一般的面孔。不过,小 wing对于周杰伦和木子美能登上《滚石》实在有点不满,感觉他们和摇滚乐以及西方精神没什么关系。如果这就是结合中国本地化,为什么不去介绍朴树、木马、叶蓓、艾敬、许巍或者汪峰呢?另外,对于杂志里的广告小wing也颇有不满,因为除了Levi's这样的品牌外,其他什么帕玛强尼腕表、Land Rover四驱车貌似都不是我能买得起的,真不知道它到底如何定位读者群的 -_-b

不过也许是因为主编郝舫、执行主编李宏杰(《摇滚圣经》啊~~~)的关系,在第一期中文版的《滚石》里有些文章颇有意思——比如最喜欢的一篇文章《丝绒革命——捷克摇滚音乐史》,它介绍的就是在强权政治下的摇滚乐以及自由运动,它对那个从一个异议作家最终当上总统的哈维尔的赞誉之情洋溢在字里行间,如果有某个编中学生历史教科书的保守学者看到这篇文章,肯定会气得鼻子冒烟。同样的道理,在这一期《滚石》里,它也大书特书“2005美国异见领袖”——反主流的态度不言而喻,而这种态度能够堂而皇之地跃于纸面,也是颇为玩味的。

It's just the beginning.尽管《滚石》已经是一本纵横江湖30余年的老杂志了,但它在中国市场生根这还是刚刚开始的事,希望有一天,能够看到《滚石》在这片黄土地上开出奇葩——就像郝舫在发刊词里说的那样:“让我们一起在东方也缔造一段无愧于这个时代的传奇。”

2006年3月16日

初识Solaris 10的新特性——Zone技术

这两天因为项目需要,开始接触Solaris 10的新特性Zone技术。这个技术被称作Solaris的革命性技术,不过愚笨的我一开始并没有领悟到这一点。

实际工作中用了才知道,Zone技术的确是有其独创性的。因为它是相对独立统一的,原本我是将Zone当作VMWare之类的虚拟机技术来理解的。但是,逐渐发现了其有截然的不同。首先,虚拟机与实际实体系统(比如Windows)之间是完全独立的,进程之间完全不能相互通信,而且关于资源的共享,虚拟机采取的方式为:硬件资源,通过虚拟机系统进行软件模拟——什么显卡啊,光驱啊之类都是通过模拟来实现的;软件资源——不好意思,要用什么软件,您自个儿装去吧。倘若是文件资料之类的,必须将实体系统和虚拟机当作两台远程计算机来看待,文件资源的共享方式因此也必须通过Samba,ftp这样的网络连接来完成

但是Zone技术是截然不同的。在Solaris 10上,默认存在的是global zone。用户可以根据需要,创建自己的zone(理论上可以创建n个,只要不怕机器负载过重)。用户定义的zone被称作none-global zone,而这个none-global zone就相当于新建一个Solaris 10环境,它会在none-global zone所在的路径(用户指定的)下再创建一个根分区,并将/bin , /lib, /usr/bin, /sbin/ , /etc,这些重要的目录在该根分区下创建一个干净的备份。在none-global zone之间,进程是严格独立的,彼此不能通信。但是global zone可以统一管理所有none-global zone进程,可以通过unix socket与之进行通信。

关于硬件资源的分享,zone奉行的是“拿来主义”。从CPU, 到网卡。直接与global zone进行分享(不清楚其调度方式是怎样的)。而如果是文件资源的管理,在同一个zone内采用的是Unix的标准方式——即通过用户,群组,权限来进行管理。none-global zone之间资源不可共享(因为在none-global zone中最多只能看见当前zone的根目录,以外的东西是不可见的),但是在global zone中,则可以通过权限设置来对none-global zone的资源直接进行管理(本来none-global zone对global zone来说也可以看作只是一个简单的目录罢了)。

事实上,可以从Unix用户的角度将global zone可以看作是一个root,none-global zone可以看作是一个个普通用户。只不过,此时一个zone就是一个操作系统环境,而并不只是像普通user那么简单只是实现了文件级别的权限管理, zone可以看作是一台机器上操作系统级别的权限管理。

至于zone技术在实际中有什么用?深层次的我说不出来,结合实际工作来看。比如只有一台机器,上面有个Solaris系统,如果要做测试,希望同样的测试用例能够在不同的语言环境中都进行一遍测试。有了 zone的出现,所有语言环境下的测试就可以并发进行了。而不用像从前那样测完一个语言环境再进行切换,从而开始下一个环境下的测试。而且有了none- global zone,再也不用担心build的过程中将系统环境弄毁了。实在不行,大不了对这个none-global zone进行uninstall - install,整个过程不超过半小时,就又能重新得到一份崭新的系统环境了。

P.S:关于zone的具体实践,Unix中文宝库中Solaris版的斑竹——南非蜘蛛所写的文章说得很详细,我都是照着它做的:)    Solaris10的革命性功能之Zone技术

2006年3月12日

读书笔记——int到float的强制类型转换

最近在看一本名为<Computer System - A Programmer's Perspective>的书。由于我所看过的计算机理论方面的书较少,加上自己大学期间一直也不用功,所以对于计算机的工作原理以及程序的工作方式我始终只知甚少,印象也十分模糊。

不过,应该说我碰到了一本好书。至少,通过昨晚对浮点数一章的阅读(呃...我的确之前对浮点数从没弄明白过),我终于了解了C语言中为什么32位int型数据强制转换到float型会出现精度不能完全保留的现象:

首先来看看我们可爱的int型变量吧,在一台典型的32位机器上一个有符号的int型的取值范围为- 2147483648 ~ 2147483647(-2^31 ~ (2^31-1))(注1)。也就是说,在一个4字节(32位2进制),除去首位用于符号位表示正负外,其余的31位都是数字的有效位。< br>
下面再来看看“万恶的”float型变量:根据IEEE的浮点标准,一个浮点数应该用下述形式来表示:
V=(-1)^s * M * 2^E (公式1)
在C语言中,32位的float型变量有着这样的规定:首位表示符号位s,接下来的8位(指数域)用于表示2的指数E,剩余的23位(小数域)表示M(取值范围为[1,2)或[0,1))。除了上述规定以外,根据指数域的二进制表示情况不同,被编码的float型数字又可以分成三种情况——
1、规格化值。当指数域的8个二进制数字既非全零又非全1时,float数值就是这种情况。设指数域的八位二进制所表示的十进制数为e, 则公式1中的E就是 E = e - (2^7 - 1) (公式2);
而且此时,将小数域所表示的二进制假设为(f22)(f21)...(f1)(f0) (注2) ,则该小数域所表示的值即为f = 0.(f22)(f21)...(f1)(f0).于是M = 1 + f
2. 非规格化值。当指数域的8个二进制数字为全0时,float数值就为这种情况。这时指数域所表示的十进制数为0,规定指数值为 E = 1 - (2^7 - 1),也就是E为定值-126;此时小数域的值仍表示f = 0.(f22)(f21)...(f1)(f0),但是M的值却变成M = f。
3. 特殊值。当指数域的8个二进制数字为全1时即为这种情况。当小数域为全零时,该float值根据符号位的不同表示正无穷或者负无穷;当小数域为非全零时,该float值为NaN(Not a Number)。

以上,只是在C语言中对int和float的规约。具体在代码中执行强制类型转化究竟会发生什么?从下面两句很简单的语句开始:

int a = 3490593;
float b = (float)a;

那么在内存中a和b究竟存放的是什么值呢?< br>
将a展开为二进制,其值为0000 0000 0011 0101 0100 0011 0010 0001,其十六进制即为0x00354321。 因为要转化为float型,所以首先要对上述二进制的表示形式改变为 M * 2^E 的形式.由于该数明显大于1,所以按照IEEE的标准,其浮点形势必然为规格化值。因此 ,转化后的形式为
a = 1.101010100001100100001 * 2^21

根据 规格化值的定义,M = 1 + f. 所以f = 0.101010100001100100001.因为float型变量的小数域一共23位。所以b的最后23位可以得出,其值为 10101010000110010000100

下面再演绎指数域的值:因为a的指数表示法中,指数E = 21。根据公式2,e = E + (2^7 -1) = 148.所以可以得出b的指数域的二进制表示为:10010100。在加上原数为正,所以符号位s=0。

所以,可以得出b的二进制表示为0 10010100 10101010000110010000100。转化为十六位进制则是0x4A550C84。换句话说,它存储在内存中的值是与a是完全不同的。但是其间还是有关联性的——a的首位为1的数值位后的二进制表示是与b的小数域完全相同的。

很快,问题就出现了。 int型的有效位数是31,而float型小数域的有效位只有23位,也就是说如果上面的a的二进制的有效位超过了24位,那么float型的小数域的精度就不够了。因此必须进行舍入。比如:如果上面的a的二进制为0000 0001 1111 0101 0100 0011 0010 0001。这时b的小数域必须有24位才够,但是,这显然是不现实的,因此必须舍入到23位,舍入的原则是:所得结果的最低有效位为0。因此这个a在转换到float时,其精度就会丢失,因为该float的最后23位变成了11110101010000110010000——这显然是与原值不符的。

实际上,C语言中对于double型在32位机器上的小数域有52位,对于int型的31位有效位是绰绰有余了。这就是为什么大部分C语言教材上鼓励读者在执行强制类型转换时将int型转换成double。同时,这可能也是为什么int型能够直接隐式转换到 double型的缘故。

注1:x ^ y表示 x的y次方
注2:(fn)取0或1

P.S: 文中所用的int型数字3490593是<Computer System>一书第二章的课后习题2.34。写这篇读书笔记的同时顺便把这个练习题给做了。应该还是有点好处的,呵呵

I'll start a new dak~~~

MSN Space has became more and more complicated. Well... it may mean more functions to some people, but in my opinion, my dak should be simple and easy. I dropped in Howjay's new blog months ago and google blogger impressed on me the neatness and simpleness. That's why I finally chose google blooger as the best place to start my new dak.

I won't tell anybody about my new dak and plan spending about half a year moving all my posts from the 小wing的驿站 to here. Then I will discard the blog on MSN Space and publish this blog.

Wish me good luck