58. itoa 的实现:如何将整数转换为字符串?

说实话,itoa 这个函数在标准 C 库里是没有的。很多初学者第一次碰到它,是在某些嵌入式开发环境或者教材的习题里。我自己刚入行那会儿,也以为它是标准函数,结果换了个编译器就报未定义,搞得一头雾水。

嗯,这里要说明白:itoa 不是 ANSI C 标准的一部分。它是某些平台(比如 Windows 的某些库)提供的扩展函数。所以,如果你想让代码跨平台,最好自己动手写一个。

今天我们就来聊聊,怎么自己实现一个 itoa。说白了,就是把一个整数,比如 12345,变成字符串 "12345"

核心思路:从低位到高位,逐位提取

你想想看,整数在内存里是二进制存的,但我们人眼习惯看十进制。怎么把十进制数字一位一位抠出来?

很简单:反复取模 10,再除以 10

  • 12345 % 10 = 5,得到个位
  • 12345 / 10 = 1234,去掉个位
  • 重复,直到数字变成 0

但这里有个问题:我们最先得到的是个位,而字符串里个位应该放在最后。所以,要么先倒着存,最后再反转;要么从字符串末尾往前填。

我个人习惯用第二种方法——从后往前填。这样省去一次反转,效率更高。

基础实现:处理正整数

我们先写一个只处理正整数的版本。代码很简单,但有几个细节要注意。

void itoa_positive(int num, char *str) {
    int i = 0;
    // 先处理 0 的特殊情况
    if (num == 0) {
        str[i++] = '0';
        str[i] = '\0';
        return;
    }

    // 从后往前填
    int start = i;
    while (num > 0) {
        str[i++] = (num % 10) + '0';
        num /= 10;
    }
    str[i] = '\0';

    // 反转字符串
    int end = i - 1;
    while (start < end) {
        char temp = str[start];
        str[start] = str[end];
        str[end] = temp;
        start++;
        end--;
    }
}

这里有个小技巧:(num % 10) + '0' 是把数字转成字符。因为 ASCII 码里 '0' 到 '9' 是连续的,所以加个 '0' 就对了。

我的经验: 我在一个嵌入式项目里写过类似的代码,当时处理器没有硬件除法器,取模操作特别慢。后来我改成查表法,把 0-9 的字符提前存好,速度提升了不少。如果你在资源受限的 MCU 上跑,可以考虑这个优化。

处理负数:别忘了负号

负数怎么处理?很简单:先判断符号,如果是负数,先输出一个 '-',然后把数字取绝对值,再按正数处理。

但这里有个坑:INT_MIN 的绝对值会溢出。你想想看,INT_MIN 是 -2147483648,它的绝对值 2147483648 已经超出了 int 能表示的最大值 2147483647。直接取反会出问题。

我曾经在这个问题上栽过跟头。调试了半天,发现负数转字符串总是少一位,最后才意识到是溢出导致的。

解决办法:用 unsigned int 来存绝对值。因为 unsigned int 能表示更大的正数范围。

void itoa(int num, char *str) {
    int i = 0;
    int is_negative = 0;

    // 处理 0
    if (num == 0) {
        str[i++] = '0';
        str[i] = '\0';
        return;
    }

    // 处理负数
    if (num < 0) {
        is_negative = 1;
        num = -num;  // 这里其实有风险,后面改进
    }

    // 转换数字部分
    int start = i;
    while (num > 0) {
        str[i++] = (num % 10) + '0';
        num /= 10;
    }

    // 添加负号
    if (is_negative) {
        str[i++] = '-';
    }
    str[i] = '\0';

    // 反转
    int end = i - 1;
    while (start < end) {
        char temp = str[start];
        str[start] = str[end];
        str[end] = temp;
        start++;
        end--;
    }
}
注意: 上面这段代码对 INT_MIN 是有问题的。因为 num = -num 这一步,如果 numINT_MIN,结果还是负数(溢出)。正确的做法是用 unsigned int 来存。

改进版:安全处理 INT_MIN

下面这个版本,我用了 unsigned int 来避免溢出。这也是我在实际项目中最终采用的写法。

void itoa_safe(int num, char *str) {
    int i = 0;
    unsigned int unum;

    // 处理 0
    if (num == 0) {
        str[i++] = '0';
        str[i] = '\0';
        return;
    }

    // 处理负数:用 unsigned 存绝对值
    if (num < 0) {
        str[i++] = '-';
        unum = (unsigned int)(-(num + 1)) + 1;  // 避免溢出
    } else {
        unum = (unsigned int)num;
    }

    // 从后往前填数字
    int start = i;
    while (unum > 0) {
        str[i++] = (unum % 10) + '0';
        unum /= 10;
    }
    str[i] = '\0';

    // 反转数字部分
    int end = i - 1;
    while (start < end) {
        char temp = str[start];
        str[start] = str[end];
        str[end] = temp;
        start++;
        end--;
    }
}

这里 unum = (unsigned int)(-(num + 1)) + 1 这行有点绕。说白了,就是先加 1 再取反,最后再加回来,这样就不会溢出。这是处理 INT_MIN 的标准技巧。

支持其他进制:不只是十进制

很多 itoa 的实现还支持指定进制,比如转成十六进制或二进制。我们也可以加上这个功能。

void itoa_base(int num, char *str, int base) {
    char *digits = "0123456789ABCDEF";
    int i = 0;
    unsigned int unum;

    if (base < 2 || base > 16) {
        str[0] = '\0';
        return;
    }

    if (num == 0) {
        str[i++] = '0';
        str[i] = '\0';
        return;
    }

    if (num < 0 && base == 10) {
        str[i++] = '-';
        unum = (unsigned int)(-(num + 1)) + 1;
    } else {
        unum = (unsigned int)num;
    }

    int start = i;
    while (unum > 0) {
        str[i++] = digits[unum % base];
        unum /= base;
    }
    str[i] = '\0';

    int end = i - 1;
    while (start < end) {
        char temp = str[start];
        str[start] = str[end];
        str[end] = temp;
        start++;
        end--;
    }
}

这样,itoa_base(255, buf, 16) 就能得到 "FF"。我在调试寄存器的时候经常用这个功能,直接把数值转成十六进制打印出来,比看十进制直观多了。

知识体系:itoa 的核心逻辑

下面这张图,帮你理清整个实现思路。

itoa 实现核心逻辑 输入:整数 num num == 0 ? 输出 "0" num < 0 ? 加负号,取绝对值 逐位取模,从后往前填

常见错误与避坑指南

我总结了几个新手最容易踩的坑,你写的时候多留个心眼。

错误类型 错误示例 正确做法
忘记字符串结尾 转换完没加 '\0' 最后一定要 str[i] = '\0'
INT_MIN 溢出 num = -num 直接取反 unsigned int 存绝对值
缓冲区不够大 只分配了 4 字节 至少 12 字节(含负号和结尾)
反转时索引越界 end = i 而不是 i-1 注意 i 指向的是结尾符位置
避坑指南: 我曾经在一个通信协议栈里用了别人写的 itoa,结果在某个边界条件下缓冲区溢出,导致整个系统崩溃。从那以后,我每次用 itoa 都会检查缓冲区大小,并且强制要求调用方传入缓冲区长度。安全第一,真的不是说说而已。

总结

自己实现 itoa,其实就是在练习三个基本功:

  • 数字拆解:用取模和除法逐位提取
  • 字符转换:数字加 '0' 变成字符
  • 边界处理:0、负数、INT_MIN、缓冲区大小

这些技巧在很多地方都用得上,比如把时间戳转成字符串、把 ADC 采样值打印出来等等。你把这个函数写熟了,以后遇到类似的需求,心里就有底了。


公众号:蓝海资料掘金营,微信deep3321