PHP学习笔记19:引用

PHP学习笔记19:引用

image-20211129162010327

图源:php.net

有意思的是,是否支持引用还是指针已经变成了区分编程语言的特征之一。比如:

  • C只支持指针。
  • C++是同时支持引用和指针的。
  • Python不需要明确指定使用引用还是指针,因为Python中所有的变量都是对象,都是引用。
  • Java支持引用,不支持指针。
  • Go lang支持指针,不支持引用。
  • PHP支持引用,不支持指针。

关于Python变量的内容,可以阅读Python学习笔记25:再谈变量

所以在谈论php的引用前,我们必须先搞懂什么是指针,什么是引用,它们之间的区别。

指针和引用

有一个比喻比较恰当,指针就像是Linux中文件系统的软链接(符号链接),而引用就像是硬连接。

关于Linux文件系统的软链接和硬链接,可以阅读Linux 之旅 5:磁盘与文件系统管理

具体来说,一般情况情况下,我们在声明一个变量的时候,编译器会根据变量类型在内存空间中申请一块区域,并使用一个变量名标记该变量,如果存在初始化值的话,同时会对该变量进行初始化。

具体的做法不同的编程语言可能会以不同方式实现,C++是在程序的调用栈中创建局部变量和变量名称。

假设我们在C++中创建一个变量int a = 1;,可以粗略地表示为:

image-20211212144249901

变量名称a和实际变量的关系就和Linux中的文件路径和真实文件一样,如果是一般情况,即一个变量只有一个变量名,我们将两者完全等同是没有问题的。但如果使用了引用变量,就像是给一个文件添加了一个额外的“硬链接”,事情就会变得有所不同,此时不应当将两者混为一谈。

看一个实际的C++程序输出:

#include <iostream>
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

这里变量ba的一个引用,准确的说a是一个int变量的名称,而b是该变量的另一个名称,也就是所谓的“别名”。这个关系可以用下图表示:

image-20211212160446202

这里的”变量a“和“变量b”实际上都只是同一个真实变量的两个不同名称而已。换句话说,引用变量b和“原始变量”a在地位和作用上是完全相同的,它们就像是指向同一个文件的两个硬链接。

现在再来看指针,指针的本质是一个指向真正变量的地址:

#include <iostream>
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

指针和变量的关系可以表示为:

image-20211212162213758

变量b是一个int类型的指针,指向变量a,实际上b中保存的值就是变量a的地址。

在C++中,对于一个指针,可以进行多种操作:

  • *b:获取指针b对应的变量,也就是b中保存的地址值对应的变量。在这个示例中就是变量a,输出后就会显示a的值,即1
  • &b:获取指针b的地址,即b变量自身的地址,在这个示例中是0x61fe00,可以看出和a的地址是不同的,这也侧面说明了指针和其指向的变量是不同的东西。
  • b:获取指针b的值,也就是其保存的地址值,即a的地址。在这个示例中是0x61fe0c

引用和指针的区别在于:

  • 引用在创建的时候必须被初始化。这很好理解,因为引用本质上只是一个变量别名,创建一个不关联到任何变量的“空别名”是没有任何意义的,Linux的文件系统同样不会允许你创建一个不指向任何文件的硬链接。
  • 引用被初始化后就不能再改变引用关系,即无法让其变成另一个变量的引用。

我们来看这个例子:

#include <iostream>
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

ba的一个引用,而b = c只会改变ab代表的变量的值,不会改变b的引用关系。

因为引用和原始变量名的作用等同,所以我们可以“使用引用来创建引用”:

#include <iostream>
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;
}

这点更说明了引用和指针的差异。如果你想用指针去创建一个指向原始变量的指针,要这么做:

#include <iostream>
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引用的全部内容了。

谢谢阅读。

往期内容

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值