Skip to content

字符串访问与修改

概述

PHP 将字符串视为由字符组成的数组,支持通过方括号偏移量来访问和修改单个字符。本章详细讲解字符串偏移访问的语法规则、边界条件、多字节字符处理以及相关的注意事项。

基础概念

字符串偏移访问

操作语法说明
读取字符$str[0]获取指定偏移量的字符
修改字符$str[0] = 'x'替换指定偏移量的字符
空字符串访问$str[0]返回空字符串,PHP 8.0+ 抛出警告
越界访问$str[100]返回空字符串,PHP 8.0+ 抛出警告
负偏移$str[-1]PHP 7.1+ 从末尾开始访问

PHP 8.0 变更

PHP 8.0 起,访问空字符串或越界偏移时会抛出 E_WARNING,PHP 8.1 起使用 [] 读取越界偏移会抛出 Warning。应使用 substr() 作为替代。

语法与代码

基本偏移访问

php
<?php

declare(strict_types=1);

// 通过偏移量读取单个字符
$str = 'Hello, World!';

echo $str[0];        // 输出: H(第一个字符)
echo $str[4];        // 输出: o(第五个字符)
echo $str[-1];       // 输出: !(最后一个字符,PHP 7.1+)
echo $str{-2};       // 输出: d(花括号语法,PHP 7.4+ 废弃,PHP 8.0+ 移除)

// 遍历字符串中的每个字符(注意:对多字节字符不安全)
$word = 'PHP';
for ($i = 0, $len = strlen($word); $i < $len; $i++) {
    echo $word[$i];  // P, H, P
}

修改字符串字符

php
<?php

declare(strict_types=1);

// 通过偏移量修改字符
$str = 'Hello';
$str[0] = 'h';       // 修改为 'hello'
echo $str;           // 输出: hello

$str[5] = '!';       // 在末尾追加字符
echo $str;           // 输出: hello!

// 修改中间字符
$phone = '138-0000-0000';
$phone[3] = '-';     // 已经是 '-'
$phone[4] = '1';     // 修改为 138-1000-0000
echo $phone;

// 注意:不能用偏移方式修改不存在的位置(PHP 8.0+ 警告)
// $str[100] = 'x';  // PHP 8.0+: Warning

负偏移访问(PHP 7.1+)

php
<?php

declare(strict_types=1);

// 负偏移 - 从字符串末尾开始计数
$str = 'abcdef';

echo $str[-1];       // 输出: f(最后一个字符)
echo $str[-2];       // 输出: e(倒数第二个)
echo $str[-3];       // 输出: d

// 负偏移也可以用于修改
$str[-1] = 'F';
echo $str;           // 输出: abcdeF

// 负偏移越界
// echo $str[-100];  // PHP 7.1-7.4: 返回空字符串;PHP 8.0+: Warning

字符串转为数组处理

当需要对字符串中的每个字符进行复杂操作时,可以将字符串拆分为数组。

php
<?php

declare(strict_types=1);

// str_split - 将字符串拆分为单字符数组
$str = 'Hello';
$chars = str_split($str);
// ['H', 'e', 'l', 'l', 'o']

// 按指定长度拆分
$chars = str_split('abcdefgh', 3);
// ['abc', 'def', 'gh']

// 反转字符串(多字节安全的方式见 mbstring 章节)
$reversed = implode('', array_reverse($chars));

// 检查字符串是否为回文
function isPalindrome(string $str): bool
{
    $str = strtolower(preg_replace('/[^a-z0-9]/', '', $str));
    return $str === implode('', array_reverse(str_split($str)));
}

var_dump(isPalindrome('racecar'));   // bool(true)
var_dump(isPalindrome('hello'));     // bool(false)

详细说明

偏移访问的边界行为

PHP 对字符串偏移访问有特殊的边界处理规则,不同版本的行为有所不同:

场景PHP < 7.1PHP 7.1+PHP 8.0+
越界读取返回空字符串,E_NOTICE返回空字符串返回空字符串 + Warning
空字符串读取返回空字符串,E_NOTICE返回空字符串返回空字符串 + Warning
越界写入静默追加静默追加Warning(字符串中间部分不变)
负偏移报错支持负偏移支持负偏移
花括号语法支持废弃移除

推荐做法

使用 substr() 函数替代偏移访问,它能更好地处理边界情况,并且对多字节字符串更友好。

方括号与花括号语法

PHP 历史上支持两种字符串偏移语法:$str[0]$str{0}。PHP 7.4 将花括号语法标记为废弃,PHP 8.0 完全移除。

php
<?php

declare(strict_types=1);

$str = 'Hello';

// 推荐写法(始终兼容)
echo $str[0];     // H

// 废弃/移除写法
// echo $str{0};  // PHP 7.4: Deprecated,PHP 8.0: Fatal Error

多字节字符串偏移

使用方括号偏移访问多字节字符串(如 UTF-8 中文)时,会按字节而非字符工作,导致错误结果。

php
<?php

declare(strict_types=1);

// 错误示范:直接用偏移访问多字节字符
$chinese = '你好世界';
echo $chinese[0];     // 输出: (不可见字节,不是'你')
echo $chinese[1];     // 输出: (不可见字节)

// 正确做法:使用 mb_substr
echo mb_substr($chinese, 0, 1, 'UTF-8');    // 输出: 你
echo mb_substr($chinese, 1, 1, 'UTF-8');    // 输出: 好

// 将多字节字符串拆分为字符数组
function mbStrSplit(string $str, string $encoding = 'UTF-8'): array
{
    $result = [];
    $len = mb_strlen($str, $encoding);

    for ($i = 0; $i < $len; $i++) {
        $result[] = mb_substr($str, $i, 1, $encoding);
    }

    return $result;
}

$chars = mbStrSplit('你好世界');
// ['你', '好', '世', '界']

实战示例

单词首字母大小写转换(逐字符处理)

php
<?php

declare(strict_types=1);

function toggleCase(string $str): string
{
    $chars = str_split($str);

    return implode('', array_map(function (string $char): string {
        if (ctype_upper($char)) {
            return strtolower($character);
        }
        if (ctype_lower($char)) {
            return strtoupper($character);
        }
        return $character;
    }, $chars));
}

echo toggleCase('Hello World');  // 输出: hELLO wORLD

字符串遮罩(敏感信息处理)

php
<?php

declare(strict_types=1);

// 手机号遮罩
function maskPhone(string $phone): string
{
    $len = strlen($phone);
    $maskLen = (int)floor($len * 0.4);
    $start = (int)floor(($len - $maskLen) / 2);

    for ($i = $start; $i < $start + $maskLen; $i++) {
        if ($phone[$i] !== '-') {
            $phone[$i] = '*';
        }
    }

    return $phone;
}

echo maskPhone('138-0000-1234');  // 输出: 138-****-1234
echo maskPhone('13987654321');    // 输出: 139***54321

Caesar 密码实现

php
<?php

declare(strict_types=1);

function caesarCipher(string $text, int $shift): string
{
    $chars = str_split($text);
    $shift = $shift % 26;

    return implode('', array_map(function (string $char) use ($shift): string {
        if (!ctype_alpha($char)) {
            return $char;
        }

        $base = ctype_lower($char) ? ord('a') : ord('A');
        $ord = ord($char) - $base;
        $ord = ($ord + $shift + 26) % 26;

        return chr($base + $ord);
    }, $chars));
}

echo caesarCipher('Hello, World!', 3);   // 输出: Khoor, Zruog!
echo caesarCipher('Khoor, Zruog!', -3);  // 输出: Hello, World!

字符串偏移与 substr 性能对比

php
<?php

declare(strict_types=1);

// 性能对比:$str[0] vs substr($str, 0, 1)
$str = str_repeat('Hello World! ', 100);
$n = 100000;

// 方式 1: 直接偏移
$start = microtime(true);
for ($i = 0; $i < $n; $i++) {
    $char = $str[0];
}
$t1 = microtime(true) - $start;

// 方式 2: substr
$start = microtime(true);
for ($i = 0; $i < $n; $i++) {
    $char = substr($str, 0, 1);
}
$t2 = microtime(true) - $start;

echo "偏移方式: {$t1}s\n";
echo "substr方式: {$t2}s\n";
// 直接偏移比 substr 略快,但差异极小

安全的字符访问封装

php
<?php

declare(strict_types=1);

/**
 * 安全获取字符串指定位置的字符
 */
function charAt(string $str, int $index): string
{
    $len = strlen($str);
    if ($index < 0) {
        $index = $len + $index;
    }
    if ($index < 0 || $index >= $len) {
        return '';
    }
    return $str[$index];
}

/**
 * 多字节安全的字符获取
 */
function mbCharAt(string $str, int $index, string $encoding = 'UTF-8'): string
{
    $len = mb_strlen($str, $encoding);
    if ($index < 0) {
        $index = $len + $index;
    }
    if ($index < 0 || $index >= $len) {
        return '';
    }
    return mb_substr($str, $index, 1, $encoding);
}

echo charAt('Hello', 0);     // H
echo charAt('Hello', -1);    // o
echo charAt('Hello', 100);   // ''(越界安全)
echo mbCharAt('你好', 0);    // 你
echo mbCharAt('你好', -1);   // 好

字符串逐字符处理模式

php
<?php

declare(strict_types=1);

// 将字符串按字节逐字符处理
function processBytes(string $str, callable $callback): string
{
    $result = '';
    $len = strlen($str);
    for ($i = 0; $i < $len; $i++) {
        $result .= $callback($str[$i], $i);
    }
    return $result;
}

// 跳过第一个字符
echo processBytes('Hello', fn(string $c, int $i): string =>
    $i === 0 ? '' : $c
);
// ello

// 每个字符加前缀
echo processBytes('ABC', fn(string $c, int $i): string =>
    "[{$i}]{$c}"
);
// [0]A[1]B[2]C

字符串编码检测与安全修改

php
<?php

declare(strict_types=1);

// 检查字符串是否为有效的 UTF-8
function isValidUtf8(string $str): bool
{
    return $str === '' || mb_check_encoding($str, 'UTF-8');
}

// 安全修改(确保不截断多字节字符)
function safeCharReplace(string $str, int $index, string $replacement): string
{
    if (!isValidUtf8($replacement) || mb_strlen($replacement, 'UTF-8') !== 1) {
        throw new \InvalidArgumentException('Replacement must be a single UTF-8 character');
    }
    return mb_substr($str, 0, $index, 'UTF-8') . $replacement . mb_substr($str, $index + 1, null, 'UTF-8');
}

echo safeCharReplace('你好世界', 1, '中');
// 你中世界

注意事项

多字节字符陷阱

方括号偏移 $str[0] 按字节操作,一个 UTF-8 中文字符占用 3 个字节。直接使用偏移会导致截断乱码。必须使用 mb_substr()mb_str_split() 处理多字节字符串。

PHP 版本差异

PHP 8.0 以后,对空字符串或越界偏移的读写会产生 Warning。在生产环境中,应使用 substr() 等函数替代直接偏移访问,或先用 isset() 检查偏移是否存在。

字符串不可变性

虽然 PHP 允许通过偏移修改字符串字符,但字符串在内存中是按值传递的。每次修改都会创建一个新的字符串副本,大量逐字符修改会导致性能问题。

最佳实践

  1. 优先使用 substr():用 substr($str, 0, 1) 替代 $str[0],更安全、更可读
  2. 多字节字符串使用 mbstring:处理中文等 UTF-8 字符串时,始终使用 mb_* 系列函数
  3. 大段修改使用 str_replace():需要替换多个字符时,使用 str_replace() 而非逐字符修改
  4. 避免花括号语法:PHP 8.0+ 已移除 $str{0} 语法,统一使用 $str[0]
  5. 敏感数据处理:使用偏移修改实现数据遮罩时,注意多字节字符和边界检查

参考链接