前言
strcmp()和strcasecmp()是一对孪生函数:前者区分大小写,后者不区分大小写,其余行为一致,都做二进制安全的字节比较。
关于它们,有三个反复出现的误解。
第一个是返回值。手册明确只保证「小于零 / 等于零 / 大于零」这三种情况,不保证具体的整数。所以if (strcmp($a, $b) === -1)这种写法是不可靠的,正确姿势是=== 0、< 0、> 0。
第二个是「strcmp()能防时序攻击」。不能。它是普通的字节比较,一发现不同就返回;比较哈希值、校验签名这类场景要用hash_equals()。而校验用户密码,连hash_equals()都不该直接用——应该用password_verify()。
第三个是「strcmp()能比较数字大小」。也不能。它逐字节比,strcmp('10', '9')的结果是负数,因为字符'1'比'9'小。要按数值比就先转成数字,要按「人能接受的顺序」比就用strnatcmp()。
本文讲清这两个函数的签名、返回值的正确用法、字节比较带来的各种后果,以及在现代 PHP 里该用哪些新函数替代它们。
一、签名与返回值
以 PHP 官方手册为准:
strcmp(string $string1, string $string2): int
strcasecmp(string $string1, string $string2): int两个函数的返回值语义完全相同:
| 返回值 | 含义 |
|---|
| 小于 0 | $string1小于$string2 |
| 等于 0 | 两个字符串相等 |
| 大于 0 | $string1大于$string2 |
注意这里刻意没有写「返回 -1」「返回 1」——手册的承诺只有符号。历史实现里有的直接返回两个字节的差值,有的做了归一化,具体数字不是可依赖的接口。稳妥写法一律是判断符号:
<?php
// 适用于 PHP 8.0+
// 相等判断:必须是 === 0
var_dump(strcmp('abc', 'abc') === 0); // bool(true)
var_dump(strcmp('abc', 'abd') === 0); // bool(false)
// 大小关系:判断符号
var_dump(strcmp('abc', 'abd') < 0); // bool(true)
var_dump(strcmp('abd', 'abc') > 0); // bool(true)
// 大小写:小写字母的 ASCII 值更大
var_dump(strcmp('a', 'A') > 0); // bool(true)
// strcasecmp:忽略 ASCII 大小写
var_dump(strcasecmp('Hello', 'hello') === 0); // bool(true)
var_dump(strcasecmp('Hello', 'World') === 0); // bool(false)二、strcmp() 的比较规则
strcmp()的比较规则可以用三句话概括:
- 二进制安全:按字符串的实际长度逐字节比较,内容中带
\0也不会提前结束。 - 按字节的数值大小比较:本质上是比较字符的编码值,所以小写字母大于大写字母,数字字符之间也是按字符比。
- 前缀相同的两个字符串,短的更小。
<?php
// 适用于 PHP 8.0+
// 数字字符比较不是数值比较
var_dump(strcmp('10', '9') < 0); // bool(true) '1' < '9'
var_dump(10 < 9); // bool(false) 这才是数值比较
// 前缀相同时,短的更小
var_dump(strcmp('abc', 'abcd') < 0); // bool(true)
var_dump(strcmp('', '') === 0); // bool(true)
// 二进制安全:中间的 NUL 字节照样参与比较
var_dump(strcmp("a\0b", "a\0c") < 0); // bool(true)关于中文:strcmp()比较的是 UTF-8 字节序列,得到的是字节序,既不是拼音序,也不是笔画序。给中文做「按拼音排序」这种需求,strcmp()帮不上忙,需要 intl 扩展的Collator,或者自己把中文转成拼音/编码后再排。这一点在建索引、做排序列表时是硬约束,别指望靠strcmp()蒙对。
三、strcasecmp() 的边界:只折叠 ASCII 大小写
strcasecmp()的「不区分大小写」仅限ASCII 的A到Z。也就是说,它只把英文字母的大小写视为等价,非 ASCII 字符(中文、带重音符号的拉丁字母等)完全按原样比较。
<?php
// 适用于 PHP 8.0+
var_dump(strcasecmp('abc', 'ABC') === 0); // bool(true) ASCII 折叠
var_dump(strcasecmp('straße', 'STRASSE') === 0); // bool(false) 非 ASCII 不做折叠
var_dump(strcasecmp('中文', '中文') === 0); // bool(true) 完全相同的字节序列
// 需要多字节的忽略大小写比较时,先统一转小写再比
$s1 = mb_strtolower('Hello 世界', 'UTF-8');
$s2 = mb_strtolower('hello 世界', 'UTF-8');
var_dump(strcmp($s1, $s2) === 0); // bool(true)要点是:strcasecmp()不读取mb_internal_encoding(),也不认识 UTF-8,它就是一个逐字节的 ASCII 比较器。涉及到多字节文本的忽略大小写比较,老老实实mb_strtolower()两边之后再strcmp()。
四、在什么场景用它们,以及现代替代方案
场景一:当排序回调。这是这两个函数在实战中最常见的用法。排序只需要返回值的符号,所以strcmp()可以直接当回调传进去。
<?php
// 适用于 PHP 8.0+
$fruits = ['banana', 'Apple', 'cherry'];
$a = $fruits;
usort($a, 'strcmp'); // 区分大小写:大写 'A' 的编码更小,排在前面
var_dump($a); // ['Apple', 'banana', 'cherry']
$b = $fruits;
usort($b, 'strcasecmp'); // 忽略大小写
var_dump($b); // ['Apple', 'banana', 'cherry']
$c = $fruits;
sort($c, SORT_STRING); // 等价于按字符串比较排序
var_dump($c); // ['Apple', 'banana', 'cherry']场景二:比较相等。单纯判断两个字符串是否相等时,===比strcmp()更直接,也没有函数调用开销。但要注意它和==的差别:==在两侧都是「数字字符串」时会做数值比较,而strcmp()永远按字节比。
<?php
// 适用于 PHP 8.0+
var_dump('10' == '10.0'); // bool(true) 松散比较:按数字比
var_dump(strcmp('10', '10.0') === 0); // bool(false) 按字节比:长度都不同
var_dump('10' === '10.0'); // bool(false) 严格比较:类型与内容都要求一致这就是「判断字符串相等到底该用哪个」的答案:普通业务判断用===;需要明确「按字节比较、不参与任何数值语义」时用strcmp(...) === 0。绝对不要用==比较两个来路不明的字符串,数字字符串的松散比较会带来意外相等。
场景三:前缀、包含关系。这类需求现在有更直白的函数可用,不需要再借道strcmp():
| 需求 | 现代写法(PHP 8.0+) | 老写法 |
|---|
| 是否以某串开头 | str_starts_with($s, $prefix) | strncmp($s, $prefix, strlen($prefix)) === 0 |
| 是否包含某串 | str_contains($s, $needle) | strpos($s, $needle) !== false |
| 是否以某串结尾 | str_ends_with($s, $suffix) | 手动substr()比较 |
| 自然顺序比较 | strnatcmp($a, $b) | 无(strcmp做不到) |
| 按当前区域设置比较 | strcoll($a, $b) | 无 |
场景四:比较敏感数据。这里必须换函数。比较两个哈希值、两个签名、两个令牌时,strcmp()会「一发现不同就返回」,比较耗时会泄露「前多少个字节是对的」,这就是时序攻击的入口。正确做法是用hash_equals():
<?php
// 适用于 PHP 8.0+(hash_equals 自 PHP 5.6 起提供)
$expected = hash_hmac('sha256', $message, $key);
$received = $_POST['signature'] ?? '';
if (!hash_equals($expected, $received)) {
exit("签名校验失败\n");
}而校验用户密码连hash_equals()都不该直接上手,应该用password_verify()——它会从哈希串里读取盐值和算法参数,自己完成比对:
<?php
// 适用于 PHP 8.0+
$hash = password_hash('用户密码', PASSWORD_DEFAULT);
if (password_verify($_POST['password'] ?? '', $hash)) {
echo "密码正确\n";
} else {
echo "密码错误\n";
}千万不要自己去比md5($input) === $stored,也不要自己用strcmp()拼比对逻辑。
常见坑点
- ❌ 写成
if (strcmp($a, $b)),以为返回真就是相等。✅ 返回0才是相等,非零是不等;条件应写成if (strcmp($a, $b) === 0)。 - ❌ 依赖具体返回值:
strcmp($a, $b) === -1。✅ 手册只保证符号,不同实现返回的数字可能不同;判断< 0、=== 0、> 0。 - ❌ 用
strcmp()比较数字大小,strcmp('10', '9')得到负数。✅ 它逐字节比;按数值比要先转成数字,按「符合直觉的顺序」比用strnatcmp()。 - ❌ 用
strcasecmp()比较含中文或带重音字母的字符串,以为它会忽略大小写差异。✅ 它只折叠 ASCII 的 A 到 Z;多字节文本先mb_strtolower($s, 'UTF-8')再strcmp()。 - ❌ 用
strcmp()或strcasecmp()校验密码或比较令牌。✅ 密码用password_verify(),其他敏感数据的比对用hash_equals()。 - ❌ 用
strcmp()判断前缀:strncmp($s, $prefix, strlen($prefix)) === 0写起来容易错。✅ PHP 8.0 起直接用str_starts_with($s, $prefix)。 - ❌ 认为
strcmp($a, $b) === 0和$a == $b等价。✅ 对数字字符串两者结论可能相反,例如'10'与'10.0'用==相等、用strcmp()不等。 - ❌ 向这两个函数传
null(常见于读配置项没读到的时候)。✅ 参数是非可空string,PHP 8.1 起传null会触发废弃提示,先做类型转换。
总结
| 对比项 | strcmp() | strcasecmp() |
|---|
| 大小写 | 区分 | 不区分(仅 ASCII) |
| 比较单位 | 字节,二进制安全 | 字节,二进制安全 |
| 返回值 | 只保证符号(小于 0 /0/ 大于 0) | 同左 |
| 数字字符串 | 按字符比,'10' < '9' | 同左 |
| 中文 | 按 UTF-8 字节序,不是拼音序 | 非 ASCII 不折叠 |
| 判等写法 | strcmp($a, $b) === 0 | strcasecmp($a, $b) === 0 |
| 典型用途 | 排序回调、明确的字节比较 | 忽略大小写的排序、键名匹配 |
| 不该用的场合 | 密码、令牌、签名比对 | 同左 |
strcmp()和strcasecmp()的核心是「按字节、看符号」:返回值只判断符号不判断数值,比较单位是字节所以不做数值比较、也不是多字节安全的。普通判等用===就够了;涉及密码用password_verify(),涉及其他敏感数据用hash_equals();前缀和后缀判断在 PHP 8.0 以后交给str_starts_with()这类语义化函数。把这几次分流做对,字符串比较这一类问题就基本清干净了。