PHP 在 64 位平台上的整数上限由 PHP_INT_MAX 常量定义,通常是 9223372036854775807。一旦需要处理更长的十进制字符串,例如某些加密库输出的 256 位数值、雪花 ID 拼接结果或区块链交易哈希,仍然把它当作普通数字传给 dechex 或 hexdec,就会得到浮点近似值,低位数字完全失真。十进制转十六进制本质上是一个按位除法问题,只要绕过整数类型限制,用字符串模拟除法或者使用任意精度扩展,就能获得精确结果。下面围绕这两种思路展开。

为什么直接转换会出现精度丢失
PHP 的整数类型在 64 位系统上是有符号整数,范围为 -9223372036854775808 到 9223372036854775807。当字符串数值超过这个范围时,如果执行 (int) $num 或把字符串参与算术运算,解释器会先尝试转换为整数,失败后退回浮点数。浮点数采用 IEEE 754 双精度格式,尾数只有 53 个二进制有效位,换算成十进制大约是 15 到 16 位可靠精度。一个 30 位以上的十进制整数经过浮点转换后,末尾十几位会被舍入成 0 或错误数字,而十六进制转换需要依赖完整的每一个十进制位,因此错误会被进一步放大。
例如下面的代码看似执行成功,但结果已经不可信:
<?php $num = '123456789012345678901234567890'; $wrong = dechex((int) $num); echo $wrong; ?>
这段代码发生两次问题:首先 (int) $num 得到的是浮点数 1.2345678901234568E+29,其次 dechex 要求 int 参数,在弱类型下可能产生不可预知的结果或直接触发错误。即便使用 hexdec 也只能反向解析十六进制,而且同样受浮点限制。所以针对大整数字符串,不能依赖 PHP 原生整数或浮点转换。
使用GMP扩展一步完成转换
GMP 是 GNU Multiple Precision 算术库的 PHP 封装,内部使用可变长度数字表示整数,不受 PHP_INT_MAX 限制。如果服务器已经启用 gmp 扩展,十进制大整数字符串转十六进制只需两行代码:使用 gmp_init 按十进制构建 GMP 对象,再调用 gmp_strval 输出十六进制字符串。
<?php
function decToHexWithGmp(string $decimal): string
{
if (!function_exists('gmp_init')) {
throw new RuntimeException('GMP extension is not available');
}
$number = gmp_init($decimal, 10);
return gmp_strval($number, 16);
}
$decimal = '123456789012345678901234567890';
echo decToHexWithGmp($decimal);
// 输出:18ee90ff6c373e0ee4e3f0ad2
?>
注意 gmp_strval 第二个参数指定输出进制,可以取 2 到 62 之间的值,其中 16 表示十六进制。默认输出为小写字母,如果业务要求大写,可以在返回前调用 strtoupper。反过来验证结果时,把十六进制字符串再用 gmp_init($hex, 16) 构建 GMP 对象,并用 gmp_strval($number, 10) 转回十进制即可。整个过程没有浮点参与。
GMP 方案的一个细节是前导零。比如十进制字符串为 000255,转换后得到 ff 而不是 0000ff,因为 GMP 把输入当作数值而非定长字符串。若需要固定长度十六进制,可以配合 str_pad 补充左侧零位。此外,GMP 函数命名较长,但性能远高于纯 PHP 字符串模拟,适合频繁调用或数百位以上数字的场景。
不安装扩展的纯 PHP 长除法实现
在共享主机或受限环境中,gmp 扩展往往不可用,bcmath 也可能未启用。这时可以直接操作十进制字符串,模拟小学除法的过程:把整个字符串看成一个数字,从最高位开始反复除以 16,每次取余数,余数对应十六进制的一位数。循环直到商变为 0。
实现原理如下:用一个变量保存当前余数,依次读取十进制字符串的每个字符。对每一位执行 当前余数 * 10 + 当前数字,然后对 16 求商和余数,商就是本轮商字符串的下一位,余数继续和下一位组合。当处理完所有位后,商字符串去掉前导零,余数就是本轮得到的十六进制最低位。把余数转成字符后拼到结果左侧,再用商字符串进入下一轮除法。
<?php
function decToHexWithoutExtension(string $decimal): string
{
if ($decimal === '') {
throw new InvalidArgumentException('Decimal string cannot be empty');
}
if ($decimal === '0') {
return '0';
}
$hexMap = '0123456789abcdef';
$hex = '';
while ($decimal !== '0') {
$remainder = 0;
$quotient = '';
$length = strlen($decimal);
for ($i = 0; $i < $length; $i++) {
$current = $remainder * 10 + (int)$decimal[$i];
$quotient .= (string)intdiv($current, 16);
$remainder = $current % 16;
}
$hex = $hexMap[$remainder] . $hex;
$decimal = ltrim($quotient, '0');
if ($decimal === '') {
$decimal = '0';
}
}
return $hex;
}
echo decToHexWithoutExtension('123456789012345678901234567890');
// 输出:18ee90ff6c373e0ee4e3f0ad2
?>
这个函数把十进制字符串中每个字符当作 0 到 9 的数字处理。因为 16 是固定值,intdiv 和 % 只处理不超过 159 的小整数,不会超出 PHP 整数范围。整个过程只依赖字符串拼接和基本算术,所以对输入长度没有限制,只是长度越大循环次数越多。对于 100 位十进制数字,外层循环大约执行 83 次左右,内层每次扫描当前商长度,总体时间可以接受;对于数千位数字,性能会明显下降。
如果想要进一步优化,可以用从低位开始的分块除法,或者把十六进制映射表改为 dechex($remainder)。但要注意余数始终小于 16,使用 dechex 没有溢出风险,只不过这样代码对 PHP 内置函数依赖更少,映射表可读性也更好。实际项目中如果只是偶尔处理几十位长度,这个纯 PHP 版本足够稳定。
边界测试与两种方案对比
验证转换正确性不能只看一两个示例。可以准备多组不同长度和包含前导零的十进制字符串,同时运行 GMP 版本和纯 PHP 版本,比较输出是否一致。下面测试代码会随机生成若干位数字,并检查两种实现的结果。
<?php
$testLengths = [1, 5, 30, 100, 256];
foreach ($testLengths as $length) {
$decimal = '';
for ($i = 0; $i < $length; $i++) {
$decimal .= (string)random_int(0, 9);
}
$decimal = ltrim($decimal, '0');
if ($decimal === '') {
$decimal = '0';
}
$gmpResult = function_exists('gmp_init')
? decToHexWithGmp($decimal)
: null;
$manualResult = decToHexWithoutExtension($decimal);
if ($gmpResult !== null && $gmpResult !== $manualResult) {
echo "Mismatch for input: {$decimal}\n";
}
}
?>
从结果和性能看,GMP 扩展方案是最省心的选择,它由 C 语言底层实现任意精度算术,面对几百位甚至上千位数字时仍然很快。纯 PHP 长除法方案的优势是零依赖,代码可以迁移到任何 PHP 运行环境,适合一次性的脚本工具或对性能要求不高的业务。如果系统已经安装了 bcmath 但未安装 GMP,也可以使用 bcdiv 和 bcmod 实现类似的长除法,但代码会更繁琐,且同样需要手动拼接十六进制字符。
最后要提醒,大整数转十六进制得到的字符串并不是加密后的安全哈希。如果需要安全摘要,应当使用 hash 或 password_hash 函数。十六进制编码只负责表达数值,不会增加随机性或不可逆性。在上述方法中,务必确认输入字符串只包含数字字符,否则纯 PHP 函数会给出未定义行为,GMP 也可能抛出异常。