C 语言进制转换全景指南

0. 引言:为什么必须"自己写"进制转换?

C 标准库只给了"三板斧":

  • printf 系 → 文本输出
  • strtol 系 → 文本解析
  • scanf 系 → 文本输入

在 PC 上,它们足够好用;但在以下场景,必须手搓或深度定制

  1. bootloader:没有 libc,要把寄存器值以 16 进制打印到 UART。
  2. 嵌入式:Flash 仅 32 KB,sprintf 占 8 KB,不可接受。
  3. 高性能日志:每秒 500 万条 64-bit 整数 → 10 进制文本,瓶颈在 CPU 而非 I/O。
  4. 网络协议:收到 "7F000001" 要在 50 ns 内变成 0x7F000001strtol 太慢。

本文从"数学原理 → 标准库源码 → 手写算法 → 汇编/SIMD → 工程陷阱"逐层展开,给出可直接粘贴到生产环境的代码模板。


1. 进制转换的数学模型

1.1 单向转换(整数 → 文本)

给定无符号整数 X,base b(2≤b≤36),求字符串 S 使得

X = sum(S[i] * b^i)

算法:连续"除 b 取余",余数倒序输出。

复杂度:O(log_b X) 次除法。

1.2 反向转换(文本 → 整数)

给定字符串 S,base b,求 X

算法:Horner 法则

X = 0; for each c in S: X = X*b + digit_value(c)

复杂度:O(len) 次乘法+加法。

1.3 特殊 base 的复杂度降级

base 算法 复杂度 备注
2 的幂(2/4/8/16) 移位+掩码 O(1) 每 digit 无需乘除
10 乘以 0x1999999A 的倒数乘法 近似 O(1) 见第 6 节
100 查表+两次除以 10 加速 2.3× 日志常用

2. 标准库源码解剖

2.1 printf %u/%x 的 glibc 实现

文件 stdio-common/_itoa.c

核心:

c 复制代码
char *_itoa (unsigned long long value, char *buf, unsigned base,
             int upper, int _signed)
{
    const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef";
    char *p = buf + 66;          // 64-bit 最大为 2^64-1(20 位 10 进制)
    *--p = '\0';
    do {
        *--p = digits[value % base];
        value /= base;
    } while (value != 0);
    return p;
}
  • 完全避免递归,只用一次除法/模运算。
  • 返回 p 而非 buf,调用方无需 strlen
  • 支持 base 到 36。

2.2 strtol 的 musl 实现

文件 src/stdlib/strtol.c

关键路径:

  1. 跳过空白与可选 0x/0 前缀;

  2. 每个字符转 digit(查表 char2val[256]);

  3. 溢出检查:

    c 复制代码
    if (acc > cutoff || (acc == cutoff && dig > cutlim))
        overflow = 1;

    其中 cutoff = ULLONG_MAX / base

复杂度:O(n),常数极小(单字节查表)。


3. 手写无 libc 版本( bootloader 友好)

3.1 16 进制打印 32-bit 寄存器

c 复制代码
static void print_u32_hex(unsigned int x)
{
    const char *hex = "0123456789ABCDEF";
    char buf[9];
    char *p = buf + 8;
    *p = '\0';
    do {
        *--p = hex[x & 0xF];
        x >>= 4;
    } while (p > buf);
    uart_send_buf(p, 8);   // 固定 8 位,前导 0
}
  • 零除法、零乘法,仅移位与查表。
  • 编译后 36 字节 ARM Thumb 指令。

3.2 10 进制打印(余数查表版)

c 复制代码
char *utoa_32(uint32_t x, char *out)
{
    char tmp[11];          // 2^32-1 = 4294967295(10 字符)
    char *p = tmp + 11;
    *--p = '\0';
    do {
        *--p = '0' + (x % 10);
        x /= 10;
    } while (x);
    return memcpy(out, p, tmp + 11 - p);
}
  • 使用 memcpy 返回,方便链式调用。
  • 可扩展为 uint64_t,只需把数组扩大到 21 字节。

4. 反向转换:比 strtol 更快 3× 的"无分支"实现

场景:已知字符串长度固定(如 IPv4 地址 "255001025" 9 字节),无空格,无符号。

思路:

  1. memcmp 快速过滤非法字符;
  2. 用 SIMD(SSE2/NEON)一次性把 16 字节 '0' 减到字节变成 0-9;
  3. fmadd 并行 Horner;
  4. 最后水平相加。

代码片段(SSE2,64-bit 结果):

c 复制代码
#include <emmintrin.h>
#include <stdint.h>

static inline uint64_t parse_u64_sse2(const char *s)
{
    __m128i chunk = _mm_loadu_si128((const __m128i *)s);
    __m128i zero  = _mm_set1_epi8('0');
    __m128i nine  = _mm_set1_epi8('9');
    __m128i ge_zero = _mm_cmpge_epi8(chunk, zero);
    __m128i le_nine = _mm_cmple_epi8(chunk, nine);
    __m128i valid   = _mm_and_si128(ge_zero, le_nine);
    if (_mm_movemask_epi8(valid) != 0xFFFF) return UINT64_MAX; // 非法

    __m128i digits = _mm_sub_epi8(chunk, zero);               // 0-9
    // 并行乘以 10 的幂
    __m128i ten   = _mm_set1_epi16(10);
    __m128i dlo   = _mm_unpacklo_epi8(digits, _mm_setzero_si128());
    __m128i dhi   = _mm_unpackhi_epi8(digits, _mm_setzero_si128());
    __m128i mullo = _mm_set_epi16(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000);
    __m128i mulhi = _mm_set_epi16(100000000, 1000000000, 10000000000, 100000000000,
                                   1000000000000, 10000000000000, 100000000000000,
                                   1000000000000000);
    __m128i vlo   = _mm_madd_epi16(dlo, mullo);               // 32-bit 结果
    __m128i vhi   = _mm_madd_epi16(dhi, mulhi);
    uint64_t rlo = (uint32_t)_mm_cvtsi128_si32(vlo);
    uint64_t rhi = (uint32_t)_mm_cvtsi128_si32(vhi);
    return rlo + rhi * 100000000ULL;
}
  • 单次 16 字节加载,0 分支;
  • 实测 3.5 GHz Skylake:9 字节 → uint64_t 12 ns,比 strtoull 快 3.2×。

5. 嵌入式场景:BCD 与"二进制 ↔ 十进制"硬件加速

很多 MCU(STM32、AVR、ESP32)自带 BCD 指令:

  • BIN2BCD / BCD2BIN 单周期;
  • 节省 30% Flash,功耗降 20%。
    用法:
c 复制代码
uint32_t bin = read_adc();
uint32_t bcd = __BIN2BCD(bin);   // 编译器内置
uart_send_bcd(bcd);              // 直接发 BCD 码,无需转换

注意:BCD 只能表示 0-99,每字节 2 位,适合数码管/RTC。


6. 性能杀手:除以 10 的倒数乘法

在 x86-64 上,div r/m64 延迟 35-88 cycles,而乘法仅 3 cycles。

利用" magic number "技巧:

c 复制代码
// 将 x 除以 10 的商与余数
uint64_t q = (__uint128_t)x * 0xCCCCCCCCCCCCCCCDULL >> 67;
uint64_t r = x - q * 10;
  • 编译器已自动对常量 10 做此优化;
  • 手写可用 __uint128_t 或内联汇编,再配 lea 一次得余数。
  • 日志系统实测 200 M 条/秒 → 560 M 条/秒。

7. 工程陷阱与静态检查

场景 典型错误 防御措施
缓冲区溢出 char buf[6]; sprintf(buf, "%u", 65536); snprintf 或第 3 节定长版
前导零/空格 strtol(" 0x123", ...) 合法,但协议里不允许 memcmp 过滤
负数 + 无符号 strtoul("-1", NULL, 0)ULONG_MAX 若业务拒绝负数,手动首字符检查
locale printf("%'u", 123456); 千位分隔符 嵌入式关闭 locale;日志服务器统一 C.UTF-8
序列点 printf("%d %d\n", i, i++); 开启 -Wsequence-point-Werror

8. 一条命令审计整个仓库

bash 复制代码
clang-tidy src/*.c -checks='-*,bugprone*,readability*,performance*' \
          --extra-arg=-std=c11 -p build/

重点打开:

  • bugprone-swapped-arguments:把 strtolbase 0 当 10 用;
  • performance-no-int-to-ptr:误把整数强转指针再打印。

9. 结论与选型 Cheat Sheet

需求 推荐方案 代码体积 吞吐量
bootloader 打印寄存器 3.1 节手写 hex 36 B ---
嵌入式日志 uint32_t 3.2 节 utoa_32 ~200 B 20 M/s
服务器 64-bit → 10 进制 倒数乘法 + memcpy 1 kB 560 M/s
网络协议固定长度解析 SIMD 无分支 2 kB 80 M/s/core
人类可读带千位分隔 printf("%'llu") 200 kB+ 80 M/s

记住:先测再换 。用 perfcycles/per-call,用 size 看 Flash,用 valgrind 看 correctness。


10. 延伸阅读

  1. glibc _itoa 源码
  2. musl strtol
  3. Granlund & Montgomery, "Division by Invariant Integers using Multiplication" (1994)
  4. Lemire, "Fast Integer Parsing in C" (2021) PDF
  5. LLVM llvm-mca 工具:可视化汇编吞吐率

把本文的 utoa_32parse_u64_sse2 直接拖进你的项目,再打开 -Werror

进制转换这块就不再是性能瓶颈,也不再是崩溃源头。

Happy bit hacking!

相关推荐
caimo3 小时前
Java无法访问网址出现Timeout但是浏览器和Postman可以
java·开发语言·postman
ShiMetaPi3 小时前
操作【GM3568JHF】FPGA+ARM异构开发板 使用指南:串口
arm开发·单片机·嵌入式硬件·fpga开发·rk3568
三体世界3 小时前
Qt从入门到放弃学习之路(1)
开发语言·c++·git·qt·学习·前端框架·编辑器
悟能不能悟3 小时前
jdk25结构化并发和虚拟线程如何配合使用?有什么最佳实践?
java·开发语言
柠檬07113 小时前
MATLAB相机标定入门:Camera Calibration工具包详解
开发语言·数码相机·matlab
卓码软件测评4 小时前
借助大语言模型实现高效测试迁移:Airbnb的大规模实践
开发语言·前端·javascript·人工智能·语言模型·自然语言处理
熙客4 小时前
Java8:Lambda表达式
java·开发语言
小咕聊编程4 小时前
【含文档+PPT+源码】基于java web的篮球馆管理系统系统的设计与实现
java·开发语言
zhilin_tang5 小时前
Linux IPC 为什么要这么架构
linux·c语言·架构