PHP 基本语法 03|字符串:单引号、双引号和那些细节

单引号 vs 双引号,差别不只是"能不能插值"

<?php
$a = '单引号';
$b = "双引号";

单引号里只有两个转义有效:\'(表示单引号本身)和 \\(表示反斜杠)。其他像 \n 会原样输出两个字符:

<?php
echo 'Hello\nWorld';    // 输出 Hello\nWorld(字面)
echo "Hello\nWorld";    // 真的换行

单引号的处理更快(不需要扫描变量),所以在"纯字面量、不含变量"的场合,用单引号是更好的习惯。含变量或用转义序列时才用双引号。

双引号支持的转义序列

转义 含义
\n 换行
\r 回车
\t 制表符
\v 垂直制表符
\e ESC
\f 换页
\\ 反斜杠
\$ 美元符(防止变量插值)
\" 双引号
\0 ~ \777 八进制字符
\x41 十六进制字符
\u{4F60} Unicode 码点(PHP 7+)
<?php
echo "Windows 路径:C:\\Users\\Admin\n";
echo "价格:\$99.00\n";          // 用 \$ 让 $ 不被当变量
echo "\u{4F60}\u{597D}";         // 你好

变量插值的两种语法

简单语法

<?php
$name = "张三";
$arr  = ['city' => '北京'];
$obj  = new stdClass();
$obj->age = 30;

echo "你好,$name";              // 你好,张三
echo "城市:$arr[city]";          // 城市:北京  —— 裸键名可以,但不推荐
echo "年龄:$obj->age";           // 年龄:30

简单语法的限制在于:它只能识别一个"变量 + 可选的一个访问符",再复杂就断掉:

<?php
echo "结果是 $arr[city] 元";      // 能解析
echo "结果是 $arr['city'] 元";    // ✗ 引号会破坏解析

复杂语法(花括号)

<?php
echo "结果是 {$arr['city']} 元";         // ✅ 推荐
echo "总计:{$obj->getTotal()} 元";      // ✅ 可以调用方法
echo "第 {$i}0 个";                       // ✅ 变量后面紧跟字母也必须用 {}
echo "用户:${name}";                      // 另一种写法,等价 {$name}

规则很简单:只要变量后面紧跟的是字母、数字、下划线或引号、[,就加花括号。这是避免歧义最省心的做法,我在项目里一律用花括号,不纠结。

一个真实的表现差异

<?php
$obj = new stdClass();
$obj->name = 'Tom';

echo "Hi $obj->name!";         // 可以,-> 属性名后面是 !
echo "Hi $obj->name's dog";    // 解析到 $obj->name 后面停,'s dog 照原样输出
echo "Hi {$obj->name}'s dog";  // 推荐写法

heredoc 和 nowdoc:多行字符串的正解

<?php
$name = "张三";

// heredoc:会插值,行为等同于双引号
$html = <<<HTML
<div class="user">
    <p>姓名:{$name}</p>
</div>
HTML;

// nowdoc:不插值,行为等同于单引号
$raw = <<<'SQL'
SELECT * FROM t WHERE name = '$name'   -- 这里的 $name 是字面
SQL;

几条实操要点:

  1. 结束标记必须顶到行首(PHP 7.3 起允许缩进,缩进量会从所有行里剥掉)。
  2. 结束标记后面除了 ; 不能有别的东西。
  3. <<< 后面紧跟标识符,不能有空格。
  4. heredoc 里的 " 和 ' 都不需要转义,写 SQL、HTML、JSON 模板时非常省事。
<?php
// PHP 7.3+ 支持缩进,缩进会自动剥离
$json = <<<JSON
    {
        "name": "$name",
        "ok": true
    }
    JSON;
echo $json;

编码:中文项目必须处理的事

PHP 的所有字符串函数默认按"字节"处理,不是按"字符"。中文字符在 UTF-8 下占 3 字节,所以:

<?php
$s = "你好世界";
echo strlen($s);       // 12  —— 字节数
echo mb_strlen($s);    // 4   —— 字符数

只要项目里有中文,字符串函数就应该全用 mb_ 系列,并且显式指定编码,别依赖 mb_internal_encoding() 的默认值:

<?php
mb_internal_encoding("UTF-8");

echo mb_substr("你好世界", 0, 2);        // 你好
echo mb_strlen("你好世界", "UTF-8");     // 4
echo mb_strtoupper("hello");             // HELLO

记住几个必然要用的对应关系:

字节函数 多字节替代
strlen mb_strlen
substr mb_substr
strpos mb_strpos
strtolower/upper mb_strtolower/upper
ucfirst mb_convert_case
str_split mb_str_split(PHP 7.4+)

另外,json_encode() 默认会把中文转成 \uXXXX,观感差:

<?php
echo json_encode(['name' => '张三'], JSON_UNESCAPED_UNICODE);
// {"name":"张三"}

常用函数,按用途分组

查找与判断

<?php
$s = "Hello World";

var_dump(str_contains($s, "World"));        // true  —— PHP 8.0
var_dump(str_starts_with($s, "Hello"));      // true  —— PHP 8.0
var_dump(str_ends_with($s, "ld"));           // true  —— PHP 8.0

var_dump(strpos($s, "o"));                   // int(4)  —— 第一次出现的位置
var_dump(strrpos($s, "o"));                  // int(7)  —— 最后一次

strpos 的老坑:找不到时返回 false,找到在下标 0 时返回 0,两者在 if 里都是假。所以必须用 === false:

<?php
if (strpos($s, "Hello") === 0) {   // ✅ 正确
    echo "在开头";
}

// if (!strpos($s, "Hello")) { ... }  // ✗ 下标 0 时误判为"没找到"

现在有 str_contains() 就没必要再纠结这个坑了,新代码优先用它。

截取与替换

<?php
echo substr("abcdef", 1, 3);      // bcd
echo substr("abcdef", -2);        // ef    负索引从末尾算
echo substr("abcdef", 0, -1);     // abcde 去掉最后一个

echo str_replace("a", "A", "banana");            // bAnAnA
echo str_ireplace("A", "x", "banana");           // 忽略大小写
echo substr_replace("abcdef", "XY", 2, 2);       // abXYef

str_replace() 的第一个参数可以是数组,做批量替换:

<?php
echo str_replace(
    ["{name}", "{age}"],
    ["张三", "28"],
    "姓名:{name},年龄:{age}"
);

这是最简单的模板引擎,做邮件模板、短信模板够用了。

格式化输出

<?php
printf("%s 今年 %d 岁,余额 %.2f 元\n", "张三", 28, 99.5);

$line = sprintf("%05d", 42);        // 00042
echo sprintf("%-10s|", "left");     // 左对齐补空格
echo sprintf("%'*10s", "hi");       // *******hi
echo number_format(1234567.891, 2); // 1,234,567.89  —— 金额显示神器

sprintf 比手工拼接字符串安全得多,尤其是做 SQL、命令行参数、日志格式化的时候。但它不等于 SQL 转义——做 SQL 请用预处理语句。

修剪、补白、重复

<?php
echo trim("  abc  ");              // abc
echo trim("xxabcxx", "x");         // abc   第二参数是"要剥掉的字符集合"
echo str_pad("5", 3, "0", STR_PAD_LEFT);   // 005
echo str_repeat("-", 20);          // --------------------

// 生成随机字符串(注意不是密码学安全)
echo substr(str_shuffle(str_repeat("abcdefghijklmnopqrstuvwxyz0123456789", 3)), 0, 8);

生成密码/令牌必须用 random_bytes() 或 random_int(),str_shuffle 的随机源是 rand,不够安全:

<?php
$token = bin2hex(random_bytes(16));   // 32 位十六进制

拆分与合并

<?php
$csv = "a,b,c";
$parts = explode(",", $csv);        // ['a','b','c']
echo implode(" | ", $parts);        // a | b | c

// 按长度切
print_r(str_split("abcdef", 2));    // ['ab','cd','ef']
print_r(mb_str_split("你好世界", 2)); // ['你好','世界'] PHP 7.4+

explode() 有个限制:分隔符不能是空字符串(会抛 ValueError)。要按字符拆用 str_split 或 mb_str_split。

输出到 HTML 与终端

<?php
$userInput = '<script>alert(1)</script>';

echo htmlspecialchars($userInput, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
// <script>alert(1)</script>

echo nl2br("第一行\n第二行");   // 把换行转成 <br />

任何用户输入输出到 HTML,必须走 htmlspecialchars()(或模板引擎的自动转义)。这是 PHP 项目最常见的 XSS 入口。

nl2br() 只把 \n 转成 <br />,它不做 HTML 转义,所以要先用 htmlspecialchars 再 nl2br:

<?php
echo nl2br(htmlspecialchars($text, ENT_QUOTES, "UTF-8"));

字符串与数组互转

<?php
$s = "abc";
echo $s[0];        // a    用下标读单个字符
echo $s[-1];       // c    PHP 7.1+ 支持负下标

$s[0] = "X";       // 就地改字符(字符串在 PHP 里可这样改,注意别越界)

什么时候用字符串,什么时候不该

几个经验判断:

  1. 多字段的数据不要拼成字符串传。"1,2,3" 这种在参数传递里很常见,但一旦字段里出现逗号就崩。用数组或 JSON。
  2. JSON 一定用 json_encode/json_decode,不要手工拼。
  3. 模板不要用字符串拼接。哪怕只是简单的邮件模板,用 heredoc 也比 $a . $b . $c 可读。
  4. 字符串比较用 ===,大小写不敏感用 strcasecmp() 或 mb_strtolower() 后再比。
  5. 需要正则时用 PCRE(preg_match/preg_replace),但简单的查找替换不要上正则——正则的性能和可维护性都更差。

坑清单

  • 单引号里 \n 不换行。
  • strpos 返回 0 与 false 混淆,必须 === false。
  • 中文用 strlen/substr 得到的是字节数,会切坏字。
  • htmlspecialchars 要指定 ENT_QUOTES 和编码,别用默认。
  • heredoc 结束标记必须顶格(7.3 前),后面不能带注释。
  • 变量后面紧跟字母("$name0")必须用 {$name}0。
  • explode("") 抛异常,按字符拆要用 str_split。