不同程序语言中引用、参数的引用传递及赋值运算符的语义

deltamaster posted @ Mar 19, 2014 10:51:07 AM in 编程语言 , 2534 阅读

问题缘起一位同学在技术聚会上提出的关于Java参数传递方式的讨论。

public class HelloWorld{
    
    public void f1(A a)
    {
        a.x = 5;
    }
    
    public void f2(A a)
    {
        a = new A();
        a.x = 10;
    }

    public static void main(String []args)
    {
        HelloWorld o = new HelloWorld();
        A a = new A();
        o.f1(a);
        System.out.println(a.x);
        o.f2(a);
        System.out.println(a.x);
    }
}

上面给出了一段简单的Java程序,问程序的输出结果是什么。

我作为一名C++程序员,理所当然地觉得既然Java对于对象的参数传递方式是引用传递,那么就应该像C++一样,函数内的局部参数a不管被赋予了什么值,都始终应该等同于调用处的a,那么结果就应该是5和10了。不过事实证明我还是太年轻了,正确的结果应该是5和5。

要解决这个问题,我们得从引用、参数的引用传递以及赋值运算符的语义说起。于是回家之后我用了许多不同的语言进行试验,并尝试总结一些语言设计的观点。

在C中,赋值语句的含义是用rhs的值代替lhs的值,求值以后rhs与lhs除了在值上相同,没有任何关系。参数传递的时候也是类似,将实参的求值结果来构造函数的形参,形参值的改变也无法影响到实参。使用指针的方式传递参数(本质还是值传递)的话,对于指针所指向的资源的修改则是双方都可见的,不过如果函数改变了指针本身的值,也是不会影响到实参的。

在C++中新增加了引用的概念(相比C),从语义上讲,C++的引用就是别名,如果b是a的引用,那么b在它的生命周期内就永远代表a,任何对于b的操作完全等同于对于a的操作。不能改变引用目标,引用初始化时也必须指向一个有效的目标。所以函数参数如果使用引用传递,则对于形参的一切修改都等同于对实参的操作。

#include <iostream>

class A
{
public:
    int x = 0;
};

void f1(A& a)
{
    a.x = 5;
}

void f2(A& a)
{
    a = *(new A); // memory leak
    a.x = 10;
}

void f3(A* a)
{
    a->x = 5;
}

void f4(A* a)
{
    a = new A; // memory leak
    a->x = 10;
}

int main()
{
    A a;
    f1(a);
    std::cout << a.x << std::endl; // 5
    f2(a);
    std::cout << a.x << std::endl; // 10

    A b;
    f3(&b);
    std::cout << b.x << std::endl; // 5
    f4(&b);
    std::cout << b.x << std::endl; // 5

    return 0;
}

上面这段简单的C++程序有一些内存泄露,不过为了说明问题方便,请暂时不要在意这些细节。

程序中f1和f2为引用传递,f3和f4为指针传递,所做的事情与上面Java程序想要做的类似。所有函数的执行结果都写在了调用行后面的注释,根据上面对C++指针传递和引用传递的语义说明,结果都是意料之中的。

在这里指针传递的方式结果与Java是相同的。由此,在Java的参数传递的语义,类似于C的指针传递。因此Java即不支持对于对象的值传递,也不支持类似C++的引用传递。要模拟值传递,必须使用clone方法。

所以,Java的赋值运算符和参数传递语义都与C/C++不同。在Java中,赋值运算符的语义是,令lhs引用rhs所引用的资源;相应的,参数传递则是令形参引用实参所引用的资源。在顶上的Java程序的f2中,我将新建的对象赋值给形参a,则实际上令形参a指向了新建的对象,此时形参a和实参a的指向就不同了,而形参a原先指向的资源,引用计数减少了1,此时还有实参a指向它,等实参a的生命周期结束,这个资源没有其他人指向的时候,就可以自动被GC回收。

许多其他语言在这个问题上的处理与Java相同。

Javascript:

function f1(o)
{
  o.x = 5;
}

function f2(o)
{
  o = {x : 0};
  o.x = 10;
}

var a = {x : 0};
f1(a);
document.write(a.x + "\n"); // 5
f2(a);
document.write(a.x + "\n"); // 5

PHP:

class A
{
  public $x = 0;
}

function f1($a)
{
  $a->x = 5;
}

function f2($a)
{
  $a = new A();
  $a->x = 10;
}

$a = new A();
f1($a);
echo "{$a->x}\n"; // 5
f2($a);
echo "{$a->x}\n"; // 5

Java、Javascript、PHP为什么不实现类似C++的引用?Java中的赋值运算符和参数传递,只有一种语法,如果选择C++引用的语义,那么就要丢失指针语义所带来的灵活性(指针的表达能力要强于引用);而如果引入其他语法规则,那么又需要重新引进指针概念。

而PHP恰恰提供了令一种参数传递的语法和赋值语法(=&),实现与C++功能相同的引用。

class A
{
  public $x = 0;
}

function f1(&$a)
{
  $a->x = 5;
}

function f2(&$a)
{
  $a = new A();
  $a->x = 10;
}

$a = new A();
f1($a);
echo "{$a->x}\n"; // 5
f2($a);
echo "{$a->x}\n"; // 10

所以,无论语言设计的细节是怎样,在赋值运算符和参数传递的语义上,始终可以参照C++的值传递、指针传递和引用传递来进行比较。

* 本文在CC BY-SA(署名-相同方式共享)协议下发布。
  • 无匹配
  • 无匹配
Hokuang 说:
Mar 19, 2014 04:53:56 PM

引用这个语法貌似只有C++里面才有的,别的语言都是用指针传参的。同样对于“reference”的定义,C++跟别的语言也是不同的,别的语言程序员所谓的reference,就等同于指针了。可以到 http://stackoverflow.com/questions/40480/is-java-pass-by-reference 这里的讨论。

Hokuang 说:
Mar 19, 2014 04:55:17 PM

问题的第一个评论我觉得很有道理,“I believe that much of the confusion on this issue has to do with the fact that different people have different definitions of the term "reference". People coming from a C++ background assume that "reference" must mean what it meant in C++, people from a C background assume "reference" must be the same as "pointer" in their language, and so on. Whether it's correct to say that Java passes by reference really depends on what's meant by "reference"”

Avatar_small
deltamaster 说:
Mar 20, 2014 10:29:24 PM

@Hokuang: 说得很对,我这篇也就相当于是阐述这几种几个定义之间的关系


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter