常用编程语言的变量语义

变量语义,通常包括值语义和引用语义,表明编程语言中变量的实际值在不同变量之间传递方式。可能很多初学者都没有深入地去了解所使用的语言的变量语义问题,毕竟程序照样写,也不见得就会出错。不过真等到出现一些莫名其妙的错误时就要贻笑大方了。尤其需要了解下不同语言下的变量语义,千万不要想当然的以为大家都是一样的。这样既能避免一些很奇葩的错误,也能在一些情况下更加高效地使用内存。

 

接下来,先通过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');

 输入结果如下:

 

a: (refcount=2, is_ref=0)='abc'
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');

 输出结果为:

 

a: (refcount=2, is_ref=1)='abc'
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' );

 输出结果如下:

a: (refcount=1, is_ref=0)=array (
'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

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值