变量语义,通常包括值语义和引用语义,表明编程语言中变量的实际值在不同变量之间传递方式。可能很多初学者都没有深入地去了解所使用的语言的变量语义问题,毕竟程序照样写,也不见得就会出错。不过真等到出现一些莫名其妙的错误时就要贻笑大方了。尤其需要了解下不同语言下的变量语义,千万不要想当然的以为大家都是一样的。这样既能避免一些很奇葩的错误,也能在一些情况下更加高效地使用内存。
接下来,先通过c/c++来简单地介绍下什么是变量语义。
c/c++变量语义
变量语义通常可分为值语义和引用语义。
Tips:你知道c/c++的函数参数的传递默认是什么语义么?如果知道的话,请跳过这一段,下面仅针对初学者。
值语义即值传递,当变量被赋值时,变量的值的内存空间会被复制,或者等待复制。对一个变量的操作不会影响另一个变量的值。直接看代码:
#include<stdio.h> int main(){ int a = 1; int b = a; printf("a=%d\tb=%d\n", a, b); // a=1 b=1 a = 2; printf("a=%d\tb=%d\n", a, b); // a=2 b=1 }
可以看出,虽然b=a,但对a的修改并没有影响到b的值。事实上,如果我们将a和b的地址打印出来的话,会发现a和b的地址是不同的,即它们指向了不同的内存空间。这也就解释了为什么对a的修改不会影响b。
引用语义即引用传递,不多说,直接看代码:
#include<stdio.h> int main(){ int a = 1; int *b = &a; printf("a=%d\tb=%d\n", a, *b); // a=1 b=1 a = 2; printf("a=%d\tb=%d\n", a, *b); // a=2 b=2 }
在这里,对a的改变同时也影响到了b的值。当然,在c里面这样写是直接用了指针,并不是概念上的引用,但所表现出的特征是类似的。
ok回到刚刚的小问题:在c/c++里面,函数参数的传递是什么语义?
#include<stdio.h> void fun(int a){ a = 2; printf("in fun: a=%d\n", a); // in fun: a=2 } int main(){ int a = 1; fun(a); printf("out fun: a=%d\n", a); // out fun: a=1 }
结论很明显,是值语义(这里且不讨论变量作用域等问题)。
要变成引用也很简单,函数外面取地址里面变指针就可以了。
事实上,由于c/c++并不像java那样区分简单类型和复杂类型,因此函数参数的传递永远都是值语义,所以我们经常会看到传指针,甚至指针的指针来得到对象的地址,以对相应的对象直接进行操作,来达到引用语义。
其实,很多正儿八经学过c/c++的同学对上文所述都非常了解。c/c++是一种很自由的需要,可以指针乱跳,所以想值语义还是引用语义,就看指针用的是否飘逸。然而,在一些没有指针的语言中,你可知道它们默认是用哪种语义?
与c的默认值语义的方式不同,我们先来了解下另一种常见的语义方式:
Java/Python的变量语义
之所以将Java和Python放在一起,是因为这两种语言的语义实现是及其相似的。这也是除c/c++以外,很多人所了解的语义方式。
Tips:你知道java的函数参数的传递默认是什么语义么?如果知道答案,请继续跳过
在Java/Python中的变量,分为基本数据类型的复杂数据类型两种。基本数据类型是诸如int,float这种简单的数据类型,复杂数据类型是诸如String、类的实例这种对象式的数据类型。两种数据类型的变量语义是不同的。
简单数据类型
在Java/Python中,对于简单数据类型使用的是值语义。看代码(以python为例,java相似):
a = 1
b = a
print a, b #1, 1
a = 2
print a, b #2, 1
可以看出,对a的修改并不会影响到b。然后,Python的实现并不像c/c++那样,直接将内存进行拷贝,两个变量指向不同的内存空间。可以验证如下:
a = 1
b = a
print id(a), id(b) #38254520 38254520每次运行都会不一样
a = 2
print id(a), id(b) #38254496 38254520
在对a修改之前,a和b的id值是一样的,说明其实a和b指向同一块内存区域。但当对a进行修改之后,a指向了另一块内存区域,而b并没有改变。这也就解释了为什么对a的修改并不会影响到b。
这就是传说中的Copy on Write(写时复制)机制。如果对一个对象只需要读取,那完全可以指向同一个内存区域而不拷贝,这样可以节省内存。只有当需要对这个对象进行修改时,才将对象拷贝一份,并对拷贝的部分进行修改。这个机制能够在一定程度上提高内存的使用率。
在下面对php的介绍中,你会看到对Copy on Write的更加飘逸的用法。
事实上,Java和Python虽然都会重用对象的内存空间,但它们的实现是有些不大一样的。java的这种简单数据类型是存放在栈里,在运行时由jvm去动态优化(写栈的时候查看值是否已存在),python的实现是通过一种特定的类型,与php的实现有些类似,但要简单许多。
同样的问题,在函数的参数传递中,也是这样的:
def fun(b):
b = 2
print b # 2
a = 1
fun(a)
print a # 1
这里也是值语义
复杂数据类型
对于复杂数据类型则刚好相反,默认都是引用语义。看代码:
class CC:
a = 0
def __init__(self, a):
self.a = a
def fun(cc):
cc.a = 3
a = CC(1)
b = a
print a.a, b.a
a.a = 2
print a.a, b.a
fun(b)
print a.a, b.a
在这个例子中,不论是赋值操作,还是函数的参数传递,对赋值变量的修改都会影响所有指向这个变量的实际值。
事实上,如果将它们的id()值打印出来的话,会发现它们都指向同一个位置。
在Java中也是如此。
Java/Python的这种变量语义虽然没c/c++那么灵活,但足够轻巧方便。简单对象复制的代价很低,所以问题不大。复杂对象默认使用引用语义,可以在很大程度上减少内存占用。但程序员在使用的时候就需要尤其注意这一点。这也就是在java甚至c++的函数参数中const用的很多的原因。初级程序员可能觉得无所谓,但它的意义却是非常重要的。这方面的规范本文不做多述。
对于简单数据类型使用引用语义,直接使用其对象的封装即可,比如对应int的Integer。
对于复杂数据类型使用值语义,则相对麻烦一些。java中需要实现cloneable接口,然后深拷贝还是浅拷贝看自己实现。python则可以直接通过copy.copy()和copy.deepcopy()分别进行浅拷贝和深拷贝。具体的做法可参考其他文章,不是本文的重点。
PHP的变量语义
php其实是与Java和Python比较类似的,可认为分为简单类型和复杂类型两种。只不过简单类型和复杂类型所包含的范围有所不同。
php中所有php内置支持的对象,都可认为是“简单类型”,包括long、String等等(php是动态弱类型语言,表面上没有long、float之分,这里只是这么说说而已),当然也包括array。array是我认为php中最飘逸的东西,类似Java中的数组、List、Map的结合体,但用起来要飘逸的多。要知道,在Java中,String、数组、List等等都是引用语义的,但在php中却是值语义。
php中的复杂类型只有用户自定义的class对象。
看代码:
<?php
$a = 1;
$b = $a;
echo $a . "\t" . $b . "\n";
$a = 2;
echo $a . "\t" . $b . "\n";
$c = "abc";
$d = $c;
echo $c . "\t" . $d . "\n";
$c = "def";
echo $c . "\t" . $d . "\n";
$e = array(array(1, 2), array(3, 4));
$f = $e[1];
print_r($e[1]);
print_r($f);
$f[1] = 5;
print_r($e[1]);
print_r($f);
可见,上面的所有变量都是值语义。
当然,这里需要对String做个特别说明。在Java中,String也是类似这样的效果,但Java中String是不可变类型,类似于const char *,对它的赋值操作都是重新指向新的地址。php中的处理虽然也是类似的,但php中使用了标准的Copy on Write的机制,这与Java的实现方式略有不同。当然这里还涉及对象的垃圾回收机制的问题,这里不再多述~
在php中想用引用也非常简单,与c类型只需一个&符号即可,比如:
<?php
$a = 1;
$b = &$a;
echo $a . "\t" . $b . "\n";
$a = 2;
echo $a . "\t" . $b . "\n";
这段代码中,对$a的改变会同时影响$b的值。这便简单地实现了引用语义。在函数参数等地方都可以这么用。
前面有提到,php中的变量使用了一种非常飘逸的Copy on Write机制(写时拷贝),并且是针对对象树(主要是array)的浅拷贝(因为hash的关系,只分离变动的部分,其它部分仍重用)。
事实上,在php的内部实现中(php是用c写的),变量是用zval的格式表示的,其结构体如下:
struct _zval_struct { /* Variable information */ zvalue_value value; /* value */ zend_uint refcount; zend_uchar type; /* active type */ zend_uchar is_ref; };
其中,refcount便是引用计数,is_ref用以标明是否为引用。
这下就好理解了,当$b = $a时。引用计数加1;当$a = 2时,引用计数减1然后分离;当$b = &$a时,引用计数不变,is_ref变为true,然后对$a修改时直接修改即可。
我们可以验证一下(xdebug_debug_zval()函数需要配置好xdebug才可使用):
<?php
$a = "abc";
$b = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
$a = "def";
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输入结果如下:
b: (refcount=2, is_ref=0)='abc'
a: (refcount=1, is_ref=0)='def'
b: (refcount=1, is_ref=0)='abc'
可以清楚的看到,在对$a进行改变时,$a和$b才会分离,引用计数减1。
但是,如果引用计数不为1时用&进行引用时,会直接分离:
<?php
$a = "abc";
$b = $a;
$c = &$a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
$a = "def";
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
输出结果为:
b: (refcount=1, is_ref=0)='abc'
c: (refcount=2, is_ref=1)='abc'
a: (refcount=2, is_ref=1)='def'
b: (refcount=1, is_ref=0)='abc'
c: (refcount=2, is_ref=1)='def'
可以看出,当有is_ref出现时,引用计数直接减1并进行分离。这样,便不会影响非引用变量。
php中的array之所以也可以这样,是因为php中的array都是以hash的方式实现,array的成员本身也是zval变量并存于符号表中。比如:
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
输出结果如下:
'meaning' => (refcount=1, is_ref=0)='life',
'number' => (refcount=1, is_ref=0)=42
)
用图片表示的话就像下边这样(引用自php.net):
从这张图中可以清晰地看到array的结构和之间的关系。这样,就不难理解为何array也可以支持Copy on Write的值语义。
留一个小问题,下面这段代码会出现什么情况?
<?php
$a = array( 'one' );
$a[] =&$a;
//xdebug_debug_zval( 'a' );
unset($a);
不过需要注意的是,在Java的GC机制中,这种循环引用是可以被JVM发现并回收的。但php的GC机制是基于引用计数的,因此并不会回收。所以这样本质上也属于内存泄露。小数据量下倒还好,大数据下就要小心了~
关于php中变量引用计数更详细的介绍,可参见鸟哥(Laruence)的两篇文章:
《深入理解PHP原理之变量(Variables inside PHP)》:http://www.laruence.com/2008/08/22/412.html
《深入理解PHP原理之变量分离/引用(Variables Separation) 》:http://www.laruence.com/2008/09/19/520.html
另外,需要注意的是,php中的class的对象不适用于Copy on Write机制。比如:
<?php
class CC{
var $v;
function __construct($v){
$this->v = $v;
}
}
function fun($c){
$c->v = 3;
}
$a = new CC(1);
$b = $a;
echo $a->v . "\t" . $b->v . "\n"; // 1 1
$a->v = 2;
echo $a->v . "\t" . $b->v . "\n"; // 2 2
fun($a);
echo $a->v . "\t" . $b->v . "\n"; // 3 3
可见,包括函数参数传递的方式,这种复杂数据类型都是引用语义。
小结
貌似很多人(包括我)在写一些代码的时候并不是很注意变量的语义问题,一般情况下倒也不会出什么问题。但是在做一些数据处理时,尤其是流式或者链式数据处理时,搞不清楚变量语义的话就容易经常出一些奇葩的问题。Java和c++的函数参数中经常会加上const便是出于这方面的考虑。
作为一只程序员,总得知道你修改的一个变量的影响范围,是吧?
本文url:http://libitum.tk/blog/433.html