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;
几条实操要点:
- 结束标记必须顶到行首(PHP 7.3 起允许缩进,缩进量会从所有行里剥掉)。
- 结束标记后面除了
;不能有别的东西。 <<<后面紧跟标识符,不能有空格。- 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,2,3"这种在参数传递里很常见,但一旦字段里出现逗号就崩。用数组或 JSON。 - JSON 一定用
json_encode/json_decode,不要手工拼。 - 模板不要用字符串拼接。哪怕只是简单的邮件模板,用 heredoc 也比
$a . $b . $c可读。 - 字符串比较用
===,大小写不敏感用strcasecmp()或mb_strtolower()后再比。 - 需要正则时用 PCRE(
preg_match/preg_replace),但简单的查找替换不要上正则——正则的性能和可维护性都更差。
坑清单
- 单引号里
\n不换行。 strpos返回 0 与 false 混淆,必须=== false。- 中文用
strlen/substr得到的是字节数,会切坏字。 htmlspecialchars要指定ENT_QUOTES和编码,别用默认。- heredoc 结束标记必须顶格(7.3 前),后面不能带注释。
- 变量后面紧跟字母(
"$name0")必须用{$name}0。 explode("")抛异常,按字符拆要用str_split。