一、先分清两个概念:变量引用与字符串“引用”
PHP里的“引用”(Reference)本质是变量符号表别名,不是C语言那种指针,也不是Java的对象引用。用&符号让两个变量名指向同一个内部数据(zval)。改一个,另一个跟着变。
那“字符串引用”是什么?它并不是说字符串本身变成引用类型。字符串在PHP里是标量,标量默认按值传递。所谓字符串引用,通常指下面几种情况:
-
用
&把两个变量绑成别名,其中一个存的是字符串; -
函数参数用
&$str接收,函数内部修改会影响外部; -
数组元素用
&$arr['key']取引用,批量处理字符串; -
对象属性、静态属性上的字符串引用;
-
引用计数与写时复制(Copy-On-Write)对字符串性能的影响。
这几个场景合起来,才是日常说的“PHP字符串引用”。
二、变量引用:字符串也能被“绑”在一起
看一段基础代码:
<?php
$a = '代码号学习编程';
$b = &$a;
$b = '代码号进阶';
echo $a; // 输出:代码号进阶
$b=&$a;之后,$a和$b是同一个变量的两个名字。给$b赋新字符串,$a读出来也变了。这里要注意:引用绑定发生在赋值那一刻,不是先复制再绑定。
很多教程只讲“引用能改原值”,但没说清楚一个坑:如果先$b=$a;再想$b=&$a;,第一次赋值已经产生了一次值拷贝。字符串短的时候无所谓,字符串很大时,这次拷贝就是白开销。
个人经验:在模板引擎或配置合并里,经常需要让多个键共享同一个字符串配置。用引用可以避免重复内存,但一定要在赋值前就绑定,顺序错了就退化成值拷贝。
三、函数传参:&$str的真实用途
函数参数加&,是字符串引用最常见的落地点。
<?php
function appendSuffix(&$str, $suffix) {
$str .= $suffix;
}
$title = '代码号';
appendSuffix($title, '学习编程');
echo $title; // 代码号学习编程
如果不加&,函数内部的$str是副本,外部$title不变。加&后,函数直接改外部变量。
但这里有个设计建议:不要为了“省内存”而滥用引用传参。PHP7以后,标量传参本身有写时复制,只读场景下不复制。只有确实需要“函数修改外部字符串”时才用&。如果只是拼接返回,直接return$str.$suffix;更清晰。
项目反思:早期写过一个日志格式化函数,参数用了&$message,结果调用方传了字面量formatLog(&'abc'),直接报错。引用参数只能接变量,不能接表达式或字面量。这个坑在代码评审时被指出,后来改成返回值方式,调用方更自由。
四、数组元素引用:批量处理字符串的利器
数组里存字符串时,可以用引用遍历,直接改原数组。
<?php
$langs = ['php', 'java', 'python'];
foreach ($langs as &$lang) {
$lang = strtoupper($lang);
}
unset($lang); // 必须断开引用
print_r($langs);
// Array ( [0] => PHP [1] => JAVA [2] => PYTHON )
这段代码的关键在unset($lang)。foreach结束后,$lang仍然是之后一个元素的引用。如果后面再写$lang='go';,数组之后一个元素会被意外改掉。这个坑非常经典,几乎每个PHP开发者都踩过。
另一种写法是用键名:
<?php
$langs = ['php', 'java', 'python'];
foreach ($langs as $key => $value) {
$langs[$key] = strtoupper($value);
}
这种写法没有引用残留问题,但多了一次键名查找。数据量小的时候,两种写法差异可以忽略;数据量大、循环体复杂时,引用遍历少一次哈希查找,但风险也更高。我的建议是:优先用键名写法,除非性能测试确实证明引用遍历有显著收益。
五、引用计数与写时复制:字符串引用的底层视角
PHP的变量存储在zval里,字符串是zend_string。多个变量指向同一个zend_string时,引用计数(refcount)增加。当某个变量要修改内容,且refcount>1,才真正复制一份,这就是写时复制(Copy-On-Write)。
<?php
$a = str_repeat('代码号', 1000);
$b = $a; // refcount +1,没有复制内容
$b .= '学习'; // refcount > 1,触发复制
理解这一点后,就能明白:普通赋值并不总是立即复制大字符串。PHP已经帮你做了优化。真正需要手动引用的场景,是你要让“修改一个变量影响另一个变量”,而不是单纯为了省内存。
六、对象属性与静态属性上的字符串引用
对象的属性也可以被引用:
<?php
class Config {
public static $siteName = '代码号';
}
$alias = &Config::$siteName;
$alias = '代码号学习编程';
echo Config::$siteName; // 代码号学习编程
静态属性引用在全局配置场景里偶尔有用,但可读性差。更推荐用常量或配置数组,而不是让一个外部变量偷偷改类内部状态。
七、哪些场景不建议用字符串引用
-
只读传参:不需要修改外部变量时,不要加
&。 -
返回引用:
function&getStr()这种写法容易让调用方困惑,除非做迭代器或单例池。 -
字面量传参:引用参数不能接
'abc'这种字面量。 -
foreach后忘记unset:引用残留会污染后续逻辑。
-
用引用做“性能优化”:PHP7+的COW已经够用,手动引用反而增加维护成本。
八、一个综合示例:配置覆盖
<?php
$baseConfig = [
'title' => '代码号',
'desc' => '学习编程',
];
$override = &$baseConfig['title'];
$override = '代码号学习编程';
print_r($baseConfig);
// Array ( [title] => 代码号学习编程 [desc] => 学习编程 )
这个例子在项目里可以用于“默认配置+环境覆盖”的场景。但更工程化的做法是用array_replace_recursive,而不是靠引用去改。引用在这里能跑通,但可读性和可测试性都不如数组合并函数。
PHP字符串引用的核心不是“字符串特殊”,而是“变量别名机制作用在字符串上”。它能做四件事:让两个变量共享同一份字符串数据、让函数修改外部字符串、让数组元素被原地批量修改、让对象或静态属性被外部别名修改。用之前先问一句:我是要“改外部变量”,还是只想“读值”?如果是后者,引用就是多余的。把unset和字面量限制记牢,能避开大部分坑。