前言
十进制转二进制的数学依据只有一条:任何一个非负整数都可以唯一地表示成 2 的幂的线性组合。把系数从高到低排开,就是二进制位。实现上有两条常见路径:反复除以 2 取余数,或者用位运算逐位取出。前者更贴近数学定义,后者更直接地对应计算机的存储方式。
常见的误解集中在负数上。有人以为-5的二进制就是101前面加个负号,或者用「符号位加绝对值」表示;而 C++ 中整数在大多数平台上采用补码(two's complement),C++20 起标准也明确要求有符号整数使用补码表示。把负整数转换成无符号类型时,标准规定按模 2^N 取余,因此可以得到其补码位模式;但如果直接对负数做%和/,余数的符号是实现定义的,结果不能直接当二进制位用。
本文先讲除 2 取余的数学过程,再给出 C++17 的迭代与递归实现,最后说明负数、定长输出和小数部分的处理边界。
一、除 2 取余的数学过程
以 13 为例:
| 步骤 | 商 | 余数 |
|---|
| 13 ÷ 2 | 6 | 1 |
| 6 ÷ 2 | 3 | 0 |
| 3 ÷ 2 | 1 | 1 |
| 1 ÷ 2 | 0 | 1 |
余数依次是 1、0、1、1,但它们是从低位到高位产生的,所以结果要倒过来读:1101。验证:1×8 + 1×4 + 0×2 + 1×1 = 13。
这个过程的终止条件是商为 0。对 0 本身要单独处理,否则循环一次也不执行,会得到空串;正确结果是 "0"。
复杂度是 O(log n):每次除以 2,位数约为 log2(n)+1。对 64 位整数,最多 64 次循环,常数很小。
二、迭代实现
最直观的写法是用std::string收集余数,最后反转:
// 适用于 C++17
#include <algorithm>
#include <cstdint>
#include <string>
std::string toBinary(std::uint64_t value) {
if (value == 0) {
return "0";
}
std::string bits;
bits.reserve(64);
while (value != 0) {
bits.push_back(static_cast<char>('0' + (value & 1ULL)));
value >>= 1;
}
std::reverse(bits.begin(), bits.end());
return bits;
}这里用value & 1ULL代替value % 2,用右移代替除以 2。对无符号类型,两者语义等价,位运算更直接地表达「取最低位」。static_cast<char>('0' + ...)是必要的:'0' + value的运算结果是int,直接push_back会有窄化转换警告,显式转换让意图清楚。
如果要求固定宽度(例如始终输出 32 位),可以从高位向低位填:
// 适用于 C++17
#include <cstdint>
#include <string>
std::string toBinaryFixed(std::uint64_t value, unsigned width) {
if (width == 0 || width > 64) {
return {};
}
std::string bits(width, '0');
for (unsigned i = 0; i < width; ++i) {
const unsigned shift = width - 1 - i;
bits[i] = static_cast<char>('0' + ((value >> shift) & 1ULL));
}
return bits;
}当width == 64时,第一次移位的位数是 63,对 64 位无符号数是合法的;如果写成1ULL << width就会移位 64 位,那是未定义行为(undefined behavior, UB)。用(value >> shift) & 1不会遇到这个问题,因为shift最大是 63。
三、递归实现
递归版本更短,但要注意递归深度等于位数,对 64 位整数最多 64 层,栈空间足够;如果处理的是任意精度整数,深度可能很大,应改用迭代。
// 适用于 C++17
#include <cstdint>
#include <string>
void appendBinary(std::uint64_t value, std::string& out) {
if (value > 1) {
appendBinary(value >> 1, out);
}
out.push_back(static_cast<char>('0' + (value & 1ULL)));
}
std::string toBinaryRecursive(std::uint64_t value) {
std::string out;
appendBinary(value, out);
return out;
}递归先处理高位、再追加当前位,所以输出顺序天然正确。value == 0时value > 1为假,直接追加'0',结果就是 "0",不需要额外特判。
四、负数、定长表示与小数部分
C++ 的整数不是数学意义上的整数。对int取负、做除法、取余,都要先明确语义:
- 有符号整数溢出是 UB,
-INT_MIN就是典型例子。 - 负数取余的结果符号在 C++11 之前是实现定义的,C++11 起规定商向零取整、余数符号与被除数相同,因此
-13 % 2 == -1,不能直接映射成二进制位。 - 有符号负数的右移在 C++20 之前是实现定义的;C++20 起规定为算术右移(补符号位),但为了可移植性,处理位模式时应先转成无符号类型。
把int转成std::uint32_t是标准定义的行为:按模 2^32 取余。因此:
// 适用于 C++17
#include <cstdint>
#include <string>
std::string toBinary32(std::int32_t value) {
const std::uint32_t bits = static_cast<std::uint32_t>(value);
std::string out(32, '0');
for (int i = 31; i >= 0; --i) {
out[static_cast<std::size_t>(31 - i)] =
static_cast<char>('0' + ((bits >> i) & 1U));
}
return out;
}对-5,static_cast<std::uint32_t>(-5)得到 2^32 - 5,其 32 位位模式是 32 个 1 中最低三位为 011,即补码表示。这个结果是标准保证的,不依赖具体平台;但「32 位」这个宽度是代码写死的,换到 64 位类型就要相应调整。
小数部分
十进制小数转二进制用「乘 2 取整」:把小数部分不断乘 2,取整数位作为下一位,直到小数部分为 0 或达到精度上限。例如 0.625:
| 步骤 | 乘 2 | 取整 | 剩余小数 |
|---|
| 1 | 1.25 | 1 | 0.25 |
| 2 | 0.5 | 0 | 0.5 |
| 3 | 1.0 | 1 | 0.0 |
结果是 0.101。很多十进制小数(如 0.1)在二进制中是无限循环的,必须设定截断位数,否则循环不会停止。浮点数还有精度误差,不能用== 0.0判断结束,应使用「剩余部分小于 epsilon」或固定循环次数。
常见坑点
- ❌ 对负数直接用
% 2取位。
// 适用于 C++17
int n = -5;
while (n) { bits.push_back('0' + n % 2); n /= 2; } // 余数为负,字符错误✅ 先转成无符号类型,再按位处理。
// 适用于 C++17
std::uint32_t u = static_cast<std::uint32_t>(n);- ❌ 余数按产生顺序直接拼接。
// 适用于 C++17
while (n) { s += char('0' + n % 2); n /= 2; } // 得到的是反序✅ 追加后反转,或从高位向低位填。
// 适用于 C++17
std::reverse(s.begin(), s.end());- ❌ 忘记处理 0。
// 适用于 C++17
std::string s;
while (n) { /* ... */ } // n == 0 时返回空串✅ 单独返回 "0"。
// 适用于 C++17
if (n == 0) { return "0"; }- ❌ 用
1 << 31取最高位。
// 适用于 C++17
std::uint32_t mask = 1 << 31; // 1 是 int,左移到符号位,UB✅ 用无符号字面量。
// 适用于 C++17
std::uint32_t mask = 1U << 31;- ❌ 以为
char('0' + value)对任意位都成立。
// 适用于 C++17
char c = '0' + 2; // 这里恰好可以,但语义上只适用于 0/1✅ 明确只处理 0 和 1,或改用查表。
// 适用于 C++17
const char* digits = "01";
char c = digits[value & 1U];- ❌ 对浮点小数用
== 0.0判断结束。
// 适用于 C++17
while (fraction != 0.0) { fraction *= 2; /* ... */ } // 0.1 永不结束✅ 固定精度或使用容差。
// 适用于 C++17
for (int i = 0; i < 32 && fraction > 1e-12; ++i) { /* ... */ }- ❌ 假定有符号负数的二进制是「符号位 + 绝对值」。
// 适用于 C++17
// 认为 -5 是 100...0101✅ 按补码理解,或先转无符号查看真实位模式。
// 适用于 C++17
std::uint32_t bits = static_cast<std::uint32_t>(-5);- ❌ 用非标准的
itoa。
// 适用于 C++17
char buf[33];
itoa(n, buf, 2); // 不是标准函数,部分平台没有✅ 自己实现,或用std::bitset的to_string(需<bitset>)。
// 适用于 C++17
std::string s = std::bitset<32>(static_cast<std::uint32_t>(n)).to_string();总结
| 要点 | 做法 |
|---|
| 数学依据 | 除 2 取余,余数倒序 |
| 迭代 | 取最低位、右移、反转 |
| 定长输出 | 从高位到低位填,移位范围 0..width-1 |
| 递归 | 先递归高位,再追加当前位 |
| 负数 | 先转无符号,按模 2^N 得到补码位模式 |
| 小数 | 乘 2 取整,设定精度上限 |
| 0 | 单独返回 "0" |
十进制转二进制的代码很短,真正需要小心的是类型:有符号与无符号的转换、移位的位数、余数的符号。把「数学整数」和「C++ 整数对象」区分开,负数和小数就不会成为陷阱。