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' 就对了。
处理负数:别忘了负号
负数怎么处理?很简单:先判断符号,如果是负数,先输出一个 '-',然后把数字取绝对值,再按正数处理。
但这里有个坑: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 这一步,如果 num 是 INT_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 的核心逻辑
下面这张图,帮你理清整个实现思路。
常见错误与避坑指南
我总结了几个新手最容易踩的坑,你写的时候多留个心眼。
| 错误类型 | 错误示例 | 正确做法 |
|---|---|---|
| 忘记字符串结尾 | 转换完没加 '\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