PHP 零基础教程

PHP 函数参数传递

在 PHP 中,函数参数就是在调用函数时传递给它的变量。这些参数允许函数接收输入、处理输入并潜在地返回一个结果。在函数内部处理这些参数的方式——具体来说,也就是在函数内部对参数的修改是否会影响函数外部的原始变量——完全取决于它们是“按值传递”还是“按引用传递”。

1. 按值传递 (Pass by Value)

当一个参数被按值传递时,发送给函数的是该参数值的一个副本(拷贝)。在函数内部对该参数所做的任何修改,都绝对不会影响在函数外部声明的原始变量。

这是 PHP 中参数传递的默认行为。

想象这样一个场景:你想对一个数字进行计算,但不想改变原始数字本身。

1.1 基础数值操作

<?php
function incrementNumber($num) {
    // 在函数内部,$num 只是原始变量值的一个副本
    $num++; // 递增这个局部副本
    echo "函数内部的值:" . $num . "\n";
}

$originalNumber = 5;

echo "函数调用前:" . $originalNumber . "\n";
incrementNumber($originalNumber); // 默认按值传递 $originalNumber
echo "函数调用后:" . $originalNumber . "\n";

// 输出:
// 函数调用前:5
// 函数内部的值:6
// 函数调用后:5 (原始变量未受影响)
?>

在这个例子中,即使调用了 incrementNumber()$originalNumber 的值依然保持为 5,因为函数接收到的只是 5 这个值的拷贝,而不是原始变量本身。

1.2 字符串修改

<?php
function modifyString($str) {
    // $str 是原始字符串的一个副本
    $str .= " World!"; // 将新内容拼接到局部副本上
    echo "函数内部的值:" . $str . "\n";
}

$greeting = "Hello";

echo "函数调用前:" . $greeting . "\n";
modifyString($greeting); // 按值传递
echo "函数调用后:" . $greeting . "\n";

// 输出:
// 函数调用前:Hello
// 函数内部的值:Hello World!
// 函数调用后:Hello
?>

与整数的例子类似,$greeting 保留了它最初的 "Hello",因为 modifyString() 是在一个独立的副本上进行操作的。

1.3 数组操作(按值传递)

即使是数组,PHP 的默认行为也是按值传递。理解 PHP 如何处理数组副本很重要,特别是对于大数组,因为复制可能会带来性能开销。

<?php
function addToArray($arr) {
    // $arr 是原始数组的一个副本
    $arr[] = "葡萄"; // 向局部副本添加一个元素
    echo "函数内部的数组:" . implode(", ", $arr) . "\n";
}

$fruits = ["苹果", "香蕉"];

echo "函数调用前:" . implode(", ", $fruits) . "\n";
addToArray($fruits); // 按值传递 $fruits
echo "函数调用后:" . implode(", ", $fruits) . "\n";

// 输出:
// 函数调用前:苹果, 香蕉
// 函数内部的数组:苹果, 香蕉, 葡萄
// 函数调用后:苹果, 香蕉 (原始数组未改变)
?>

2. 按引用传递 (Pass by Reference)

当一个参数被按引用传递时,函数接收到的是指向原始变量的引用(一个别名或内存指针),而不是它的值拷贝。这意味着在函数内部对参数所做的任何修改,都直接影响到函数外部的原始变量。

要按引用传递参数,你需要在函数定义的参数名前面加上一个和号(&

这种方法在你需要一个函数直接修改传给它的变量,而不仅仅是返回一个新值时非常有用。

2.1 按引用修改整数

<?php
function incrementNumberByReference(&$num) {
    // $num 是对原始变量的引用
    $num++; // 直接递增原始变量
    echo "函数内部的值:" . $num . "\n";
}

$originalNumber = 5;

echo "函数调用前:" . $originalNumber . "\n";
incrementNumberByReference($originalNumber); // 按引用传递
echo "函数调用后:" . $originalNumber . "\n";

// 输出:
// 函数调用前:5
// 函数内部的值:6
// 函数调用后:6 (原始变量被改变了!)
?>

在这里,$originalNumber 变成了 6,因为 incrementNumberByReference() 直接访问并修改了它所引用的那个变量。

2.2 按引用修改数组

按引用传递数组非常常见。例如,当一个函数需要添加、删除或更改函数外部数组的元素,且不想将整个修改后的数组作为返回值返回时。

<?php
function addToArrayByReference(&$arr) {
    // $arr 是对原始数组的引用
    $arr[] = "葡萄"; // 向原始数组添加元素
    echo "函数内部的数组:" . implode(", ", $arr) . "\n";
}

$fruits = ["苹果", "香蕉"];

echo "函数调用前:" . implode(", ", $fruits) . "\n";
addToArrayByReference($fruits); // 按引用传递 $fruits
echo "函数调用后:" . implode(", ", $fruits) . "\n";

// 输出:
// 函数调用前:苹果, 香蕉
// 函数内部的数组:苹果, 香蕉, 葡萄
// 函数调用后:苹果, 香蕉, 葡萄 (原始数组被修改了!)
?>

3. 该选按值传递还是按引用传递?

决定使用哪种传递方式,取决于你期望的行为和性能考量。

按值传递 (默认行为):

  • 何时使用: 当你想保护原始数据不被函数意外修改时。这保证了函数在一个独立的副本上运行,提升了数据的完整性,使得函数的行为更可预测、更独立。它适用于大多数执行计算或转换并返回结果且没有副作用的通用函数。
  • 性能说明: PHP 底层对许多标量类型甚至数组使用了一种称为“写时复制 (copy-on-write)”的内存优化机制。这意味着,只有当函数内部真正去修改这个值时,PHP 才会真正在内存里去复制一份拷贝。所以,对于大数组,只要你不修改它,按值传递也不会带来巨大的内存负担。

按引用传递 (&):

  • 何时使用: 当函数必须直接修改它的参数时。常见场景包括:在函数内填充一个空数组、实现交换函数(交换两个变量的值)、或者函数需要返回多个“结果”时(尽管返回一个数组通常是更清晰的做法)。
  • 可预测性警告: 需要谨慎使用。因为按引用传递引入了“副作用 (side effects)”——函数修改了它作用域之外的变量。如果不加节制地使用,这会让代码变得极难调试和理解。

3.1 关于对象 (Objects) 的特殊说明

在 PHP 中,对象总是按值传递的,但是传递的这个“值”实际上是对象的一个标识符句柄 (handle)。这意味着,如果你在函数内部修改了对象的属性,这些更改影响到函数外部的原始对象(这看起来很像按引用传递)。

但是,如果你在函数内部对这个对象变量进行重新赋值(例如 $obj = new OtherClass();),它只会改变局部的变量指向,函数外部的原始对象依然保持不变。

<?php
class MyObject {
    public $value = '初始值';
}

// 修改对象属性
function modifyObjectProperties($obj) {
    $obj->value = '属性被修改了'; // 这会改变原始对象
}

// 重新赋值对象变量
function reassignObjectVariable($obj) {
    $obj = new MyObject(); // 这在局部创建了一个全新的对象
    $obj->value = '局部重新赋值'; // 只影响局部的新对象
}

$myInstance = new MyObject();

echo "初始对象值:" . $myInstance->value . "\n"; // 初始值

modifyObjectProperties($myInstance);
echo "执行 modify 后:" . $myInstance->value . "\n"; // 属性被修改了

reassignObjectVariable($myInstance);
echo "执行 reassign 后:" . $myInstance->value . "\n"; // 依然是:属性被修改了 (原始对象没有被替换)
?>