图源:
有意思的是,是否支持引用还是指针已经变成了区分编程语言的特征之一。比如:
-
C只支持指针。
-
-
Python不需要明确指定使用引用还是指针,因为Python中所有的变量都是对象,都是引用。
-
Java支持引用,不支持指针。
-
Go lang支持指针,不支持引用。
-
PHP支持引用,不支持指针。
关于Python变量的内容,可以阅读。
所以在谈论php的引用前,我们必须先搞懂什么是指针,什么是引用,它们之间的区别。
指针和引用
有一个比喻比较恰当,指针就像是Linux中文件系统的软链接(符号链接),而引用就像是硬连接。
关于Linux文件系统的软链接和硬链接,可以阅读。
具体来说,一般情况情况下,我们在声明一个变量的时候,编译器会根据变量类型在内存空间中申请一块区域,并使用一个变量名标记该变量,如果存在初始化值的话,同时会对该变量进行初始化。
具体的做法不同的编程语言可能会以不同方式实现,C++是在程序的调用栈中创建局部变量和变量名称。
假设我们在C++中创建一个变量int a = 1;
,可以粗略地表示为:
变量名称a和实际变量的关系就和Linux中的文件路径和真实文件一样,如果是一般情况,即一个变量只有一个变量名,我们将两者完全等同是没有问题的。但如果使用了引用变量,就像是给一个文件添加了一个额外的“硬链接”,事情就会变得有所不同,此时不应当将两者混为一谈。
看一个实际的C++程序输出:
using namespace std;
int main()
{
int a = 1;
int &b = a;
cout << "a var address is:" << &a << endl;
cout << "b var address is:" << &b << endl;
}
// a var address is:0x61fe14
// b var address is:0x61fe14
这里变量b
是a
的一个引用,准确的说a
是一个int
变量的名称,而b
是该变量的另一个名称,也就是所谓的“别名”。这个关系可以用下图表示:
这里的”变量a“和“变量b”实际上都只是同一个真实变量的两个不同名称而已。换句话说,引用变量b
和“原始变量”a
在地位和作用上是完全相同的,它们就像是指向同一个文件的两个硬链接。
现在再来看指针,指针的本质是一个指向真正变量的地址:
using namespace std;
int main()
{
int a = 1;
int *b = &a;
cout << "a var address is:" << &a << endl;
cout << "pointer's value::" << b << endl;
cout << "pointer's point value:" << *b << endl;
cout << "pointer's address:" << &b << endl;
}
// a var address is:0x61fe0c
// pointer's value::0x61fe0c
// pointer's point value:1
// pointer's address:0x61fe00
指针和变量的关系可以表示为:
变量b
是一个int
类型的指针,指向变量a
,实际上b
中保存的值就是变量a
的地址。
在C++中,对于一个指针,可以进行多种操作:
-
*b
:获取指针b
对应的变量,也就是b
中保存的地址值对应的变量。在这个示例中就是变量a
,输出后就会显示a
的值,即1
。 -
&b
:获取指针b
的地址,即b
变量自身的地址,在这个示例中是0x61fe00
,可以看出和a
的地址是不同的,这也侧面说明了指针和其指向的变量是不同的东西。 -
b
:获取指针b
的值,也就是其保存的地址值,即a
的地址。在这个示例中是0x61fe0c
。
引用和指针的区别在于:
-
引用在创建的时候必须被初始化。这很好理解,因为引用本质上只是一个变量别名,创建一个不关联到任何变量的“空别名”是没有任何意义的,Linux的文件系统同样不会允许你创建一个不指向任何文件的硬链接。
-
引用被初始化后就不能再改变引用关系,即无法让其变成另一个变量的引用。
我们来看这个例子:
using namespace std;
int main()
{
int a = 1;
int &b = a;
int c = 9;
b = c;
cout << "a var address is:" << &a << endl;
cout << "b var address is:" << &b << endl;
cout << "c var address is:" << &c << endl;
cout << "a value:" << a << endl;
cout << "b value:" << b << endl;
}
// a var address is:0x61fe04
// b var address is:0x61fe04
// c var address is:0x61fe00
// a value:9
// b value:9
b
是a
的一个引用,而b = c
只会改变a
和b
代表的变量的值,不会改变b
的引用关系。
因为引用和原始变量名的作用等同,所以我们可以“使用引用来创建引用”:
using namespace std;
void print_var_address(int &var, string var_anme);
int main()
{
int a = 1;
int &b = a;
int &c = b;
int &d = c;
print_var_address(a, "a");
print_var_address(b, "b");
print_var_address(c, "c");
print_var_address(d, "d");
// a address is :0x61fdac
// b address is :0x61fdac
// c address is :0x61fdac
// d address is :0x61fdac
}
void print_var_address(int &var, string var_name)
{
cout << var_name << " address is :" << &var << endl;
}
这点更说明了引用和指针的差异。如果你想用指针去创建一个指向原始变量的指针,要这么做:
using namespace std;
int main()
{
int a = 1;
int *b = &a;
int *c = &(*b);
cout << &a << endl;
cout << b << endl;
cout << c << endl;
// 0x61fe0c
// 0x61fe0c
// 0x61fe0c
}
现在应当已经理解了指针和引用的原理和差异,我们来看php的引用。
php的引用
引用赋值
php中创建一个引用的语法和C++略有差异:
<?php
$a = 1;
$b = &$a;
$b = 9;
echo $a . PHP_EOL;
echo $b . PHP_EOL;
// 9
// 9
语法上和C++中用变量地址给指针赋值更相似,但是要清楚,$b=&$a
并非是用$a
的地址创建了一个指针,而是创建了一个对$a
的引用。
此外,php中是可能对一个未定义的变量创建引用的,此时会自动创建一个值为null
的变量:
var_dump($d);
var_dump($c);
// NULL
// NULL
如果是引用传参,也是相同的效果:
function get_null_ref(&$param)
{
var_dump($param);
// NULL
}
get_null_ref($e);
var_dump($e);
// NULL
在C++中,引用一旦创建是无法改变引用关系的,但php不同:
<?php
$a = 1;
$b = &$a;
$c = 2;
$b = &$c;
echo "$a $b $c" . PHP_EOL;
// 1 2 2
$b = 9;
echo "$a $b $c" . PHP_EOL;
// 1 9 9
上边的示例中,$b
先作为$a
的引用,后又作为$c
的引用。
再看一个涉及全局变量的例子:
<?php
$msg1 = 'hello';
$msg2 = 'world!';
function change_msg()
{
global $msg1, $msg2;
$msg2 = &$msg1;
}
change_msg();
echo "$msg1 $msg2" . PHP_EOL;
// hello world!
虽然再函数change_msg
中声明并使用了全局变量$msg1
和$msg2
,但是$msg2=&$msg1
语句并不能让外部的全局变量$msg2
变成$msg1
的引用。
实际上global $msg1,$msg2
的实际效果是创建两个全局变量的引用,而这两个引用的作用域是函数作用域。也就是说它们等效于:
$msg1 = &$GLOBALS['msg1'];
$msg2 = &$GLOBALS['msg2'];
所以$msg2 = &$msg1;
的作用只不过是将函数内的$msg2
引用指向了全局变量$msg1
,但并不会影响到全局变量$msg2
。如果要改变全局变量$msg2
,让其变为$msg1
的引用,要这样:
<?php
$msg1 = 'hello';
$msg2 = 'world!';
function change_msg()
{
$GLOBALS['msg2'] = &$GLOBALS['msg1'];
}
change_msg();
echo "$msg1 $msg2" . PHP_EOL;
// hello hello
当然,也可以:
<?php
$msg1 = 'hello';
$msg2 = 'world!';
function change_msg()
{
global $msg1;
$GLOBALS['msg2'] = &$msg1;
}
change_msg();
echo "$msg1 $msg2" . PHP_EOL;
// hello hello
遍历数组时使用引用
这是一个经常使用,但容易被忽略的问题:在遍历数组时使用的引用应当及时注销。
如果你忘了这么做,可能会遇到一些bug,这样的bug不仅低级,还难以排查:
<?php
$arr1 = range(1, 10);
$arr2 = range(1, 20, 2);
require_once '../util/array.php';
foreach ($arr1 as &$val) {
$val += 1;
}
foreach ($arr2 as $key => $val) {
$arr2[$key] = $val * 2;
}
print_arr($arr1);
print_arr($arr2);
// [0:2, 1:3, 2:4, 3:5, 4:6, 5:7, 6:8, 7:9, 8:10, 9:19]
// [0:2, 1:6, 2:10, 3:14, 4:18, 5:22, 6:26, 7:30, 8:34, 9:38]
在这个示例中,遍历$arr1
并修改元素的时候使用的是元素引用,遍历$arr2
并修改元素的时候使用的是数组下标。乍一看并没有什么问题,但观察输出的结果就能发现,本来$arr1
的结果最后一位应当是11
,但确是19
。这是因为遍历完$arr1
的时候没有注销$val
,也就是说foreach ($arr2 as $key => $val) {
这条语句中,$val
并非一个不存在的变量,而是一个有效的引用,其引用的变量正是$arr1[9]
所代表的变量。虽然这对遍历$arr2
本身并不会有什么影响,但有一个副作用,即每次遍历时都会修改$arr1[9]
的值:
...
foreach ($arr2 as $key => $val) {
echo $arr1[9]." ";
$arr2[$key] = $val * 2;
}
// 1 3 5 7 9 11 13 15 17 19
要避免这种问题也很简单——每次使用引用来遍历数组时,遍历完毕后及时注销引用:
<?php
$arr1 = range(1, 10);
$arr2 = range(1, 20, 2);
require_once '../util/array.php';
foreach ($arr1 as &$val) {
$val += 1;
}
unset($val);
foreach ($arr2 as $key => $val) {
$arr2[$key] = $val * 2;
}
print_arr($arr1);
print_arr($arr2);
// [0:2, 1:3, 2:4, 3:5, 4:6, 5:7, 6:8, 7:9, 8:10, 9:11]
// [0:2, 1:6, 2:10, 3:14, 4:18, 5:22, 6:26, 7:30, 8:34, 9:38]
事实上,应当对所有的引用遍历都在使用完毕后及时注销。只不过这个一般来说这种“无意中使用了一个有效引用”的问题在遍历中最为常见。
数组中的引用
可以利用引用来创建数组:
<?php
[$a, $b, $c] = [1, 2, 3];
$arr = [&$a, &$b, &$c];
foreach ($arr as &$val) {
$val = 9;
}
unset($val);
require_once '../util/array.php';
print_arr($arr);
print_arr([$a, $b, $c]);
// [0:9, 1:9, 2:9]
// [0:9, 1:9, 2:9]
如果数组中包含引用,那么复制数组的时候就会比较微妙:
<?php
$a = 1;
$arr = [&$a, 2, 3];
$arr2 = $arr;
$arr2[0] = 9;
$arr2[1] = 9;
require_once '../util/array.php';
print_arr($arr);
print_arr($arr2);
因为$arr[0]
是一个引用,所以数组赋值后的$arr2
中$arr2[0]
也是一个引用。这点严格来说并不符合“值拷贝”的规则,因为$arr[0]
的值是$a
的值,应当是常量1
才对。但实际上这里并没有采用为$arr2[0]
创建一个新变量,并传入1
的做法,而是依然将其作为$a
的引用。
这样做的好处在于让数组在赋值后,其中的元素类型与原数组保持一致,即普通类型依然是普通类型,引用依然是引用,对象依然是对象。
引用传递
如果需要函数修改外部数据,一种方式是将结果返回后外部程序自行修改,一种是以引用的方式接收参数,并在函数内直接修改:
<?php
function pass_ref(&$param)
{
$param++;
}
$a = 1;
pass_ref($a);
echo $a;
// 2
需要注意的是接收的参数只能是基础类型和数组,对象是不应该使用引用参数接收的:
<?php
class MyClass{
public $num = 0;
}
function pass_ref(MyClass &$mc)
{
$mc->num++;
}
$a = new MyClass;
pass_ref($a);
echo $a->num;
// 1
虽然这么做依然有效,但在某些php版本中会输出E_NOTICE
,应当尽量避免这么做。
引用返回
一般来说函数并不需要返回一个引用,但如果需要,可以这样做:
<?php
function &increase(int &$a)
{
$a++;
return $a;
}
$a = 1;
increase(increase($a));
echo $a;
// 3
这里的increase
函数接收一个引用,让其自增后再将其返回。需要注意的是这里返回的并不是一个值,而是一个引用,这是函数名前有一个&
符号决定的。这相当于C++中的int& increase(int& a)
这样的函数声明,意思是返回值是一个引用。
正因为返回的是一个引用,所以我们才可以在示例中进行使用同一个函数“级连调用”:increase(increase($a))
后,返回的结果依然是$a
这个变量。而echo $a
的结果也说明了这一点。
需要注意的是,使用普通的赋值语句获取的引用返回函数的返回值,依然是一个普通变量:
<?php
function &increase(int &$a)
{
$a++;
return $a;
}
$a = 1;
$b = increase($a);
$b = 9;
echo $a . PHP_EOL;
echo $b . PHP_EOL;
// 2
// 9
如果要让“承接”返回值的变量也是原变量的引用,需要使用类似于$b = &$a
的写法:
...
$a = 1;
$b = &increase($a);
$b = 9;
echo $a . PHP_EOL;
echo $b . PHP_EOL;
// 9
// 9
最后吐槽一下,我个人觉得php的引用语法很糟糕,应当使用
var& $b = $a
这样的写法,或者&$b = $a
,而不是$b = &$a
,现在这种写法很容易让C++或者Go lang转过来的开发者误以为是取地址操作,$b
是$a
的指针,然而根本就不是那么一回事。更糟糕的是如果函数返回的是引用,还需要用引用承接,就变成了$b = &increase($a)
,这种写法简直灾难,这是要对返回值取地址?
取消引用
要取消一个引用很简单:
<?php
$a = 1;
$b = &$a;
unset($b);
echo $a . PHP_EOL;
// 1
echo $b . PHP_EOL;
// Warning: Undefined variable $b in ...
C++中并不能手动取消引用,只能等引用在声明周期结束后自己销毁。但因为C++有完善的包作用域,也不能重复定义一个同名引用,所以并不会存在php中的很多潜在bug。这也是为什么在php中,最好在用完引用后手动取消的原因。
取消引用的原理就像是Linux的硬链接,只要还存在一个有效的硬链接,那么原始文件就不会丢失。
以上就是php引用的全部内容了。
谢谢阅读。
文章评论