当前位置: 首页 > news >正文

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
核心:

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. 溢出检查:
    if (acc > cutoff || (acc == cutoff && dig > cutlim))overflow = 1;
    
    其中 cutoff = ULLONG_MAX / base

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


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

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

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 进制打印(余数查表版)

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 结果):

#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%。
    用法:
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 ”技巧:

// 将 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若业务拒绝负数,手动首字符检查
localeprintf("%'u", 123456); 千位分隔符嵌入式关闭 locale;日志服务器统一 C.UTF-8
序列点printf("%d %d\n", i, i++);开启 -Wsequence-point-Werror

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

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 节手写 hex36 B
嵌入式日志 uint32_t3.2 节 utoa_32~200 B20 M/s
服务器 64-bit → 10 进制倒数乘法 + memcpy1 kB560 M/s
网络协议固定长度解析SIMD 无分支2 kB80 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!

http://www.dtcms.com/a/542080.html

相关文章:

  • 前端速通—Vue_简介 第一个Vue程序 el:挂载点 data:数据对象 Vue指令
  • Vue 3 + Element Plus 动态通用表单组件设计与实现
  • 网站开发用到什么技术wordpress的安装界面
  • c 开发商城网站开发nodejs同时做网站和后台管理
  • 开发中的英语积累 P10:Capability、Enterprise、Transport、Loop、Active、Host
  • 网站建设开发步骤wordpress用旧的编辑器
  • 劳力士手表网站官方网站是 优帮云
  • 中国建设银行网站查询余额网络销售渠道
  • 【FPGA】时序逻辑计数器——仿真验证
  • 2025年--Lc217-145. 二叉树的后序遍历(递归版,带测试用例)-Java版
  • 做直播网站要多少钱北京网页设计
  • 门户网站标题居中加大seo个人博客
  • 【音视频】RTP协议快速上手
  • 阿里云可以做几个网站做网站南昌
  • DM8 分区表学习笔记
  • 做网站有没有免费空间英文网站建设注意什么
  • 3.枚举算法(一)
  • 网站开发需求收集网站建设和维护工作内容
  • 建设银行昆山分行网站wordpress本站导航在哪里
  • 房地产网站建设策划书如何对网站进行维护
  • Lambert W 函数简要探讨
  • 为什么要避免使用 `SELECT *`?
  • 网站怎么做外链接中国建设银行e路护航网银安全组件
  • 网页设计网站名字做网站开发数据库怎么写
  • 创建es索引
  • Spring Boot项目中如何实现接口幂等
  • 深圳高端网站开发网上卖产品怎么推广
  • hexo框架做网站wordpress 变换模板
  • 国美在线网站建设淄博做网站推广
  • 网站提交网址网店美工实训报告总结2000字