
前言十进制转任意进制是进制转换里最基础的一种给定一个十进制整数和目标进制常见是 2、8、16也可以扩展到 36用「除基取余、逆序排列」得到结果。算法本身只有几行但边界不少0 怎么处理、负数怎么处理、余数 10 以上怎么映射成字母、进制超出范围怎么办。本文还把转换封装成一个类并重载operator、operator、operator用来说明运算符重载的几条基本规则哪些运算符必须写成非成员函数、返回值应该是什么类型、不同进制的对象相加该如何定义语义。很多教程只演示「能打印」却没说清operator为什么不能写成成员函数也没说清重载后的运算符应该遵守哪些约定。本文以 C17 为准先讲转换算法与边界再给出类设计与完整实现。所有转换结果都按标准语义推演不依赖某台机器的运行输出。一、除基取余的原理把一个十进制数N转成base进制反复执行digit N % baseN N / base得到的digit是从低位到高位的因此要逆序才是最终结果。例如 13 转 2 进制步骤NN % 2N / 211316260333114110余数依次是 1、0、1、1逆序后是 1101。这个 1101 是 13 的二进制表示。终止条件是N 0。但若一开始N就是 0循环一次也不执行结果会是空字符串因此 0 必须单独返回0。二、负数的处理负数的符号不参与进制转换应该先取出绝对值转换完再补-。问题在于long long的最小值LLONG_MIN取负会溢出-LLONG_MIN超出long long的表示范围有符号整数溢出是未定义行为undefined behavior, UB。正确做法是先转成无符号类型再取负// 适用于 C17const bool negative value 0;unsigned long long magnitude negative? 0ULL - static_castunsigned long long(value): static_castunsigned long long(value);把负数转成unsigned long long时标准规定按模2^N转换再用 0 减去它得到的正好是绝对值的无符号表示。对LLONG_MIN这一步得到2^63可以安全地参与除法和取余。三、数字到字符的映射进制 2 到 36 需要 36 个符号。惯例是数值字符0–90–910–35A–Z超过 10 的余数不能直接0 digit否则会得到:、;之类的标点。必须分支处理// 适用于 C17char digitChar(unsigned int digit) {if (digit 10) return static_castchar(0 digit);return static_castchar(A (digit - 10));}反过来解析时a和A通常都接受但输出统一用大写避免同一数值出现两种写法。进制上限取 36是因为 10 个数字加 26 个字母刚好 36 个符号若允许更大进制就需要自定义符号表。四、完整转换函数、类封装与运算符重载// 适用于 C17#include algorithm#include stdexcept#include stringchar digitChar(unsigned int digit) {if (digit 10) return static_castchar(0 digit);return static_castchar(A (digit - 10));}std::string toBase(long long value, int base) {if (base 2 || base 36) {throw std::invalid_argument(base must be in [2, 36]);}if (value 0) return 0;const bool negative value 0;unsigned long long magnitude negative? 0ULL - static_castunsigned long long(value): static_castunsigned long long(value);std::string digits;digits.reserve(64);while (magnitude 0) {const unsigned int digit static_castunsigned int(magnitude % static_castunsigned long long(base));digits.push_back(digitChar(digit));magnitude / static_castunsigned long long(base);}if (negative) digits.push_back(-);std::reverse(digits.begin(), digits.end());return digits;}digits.reserve(64)只是减少扩容次数64 足够容纳 64 位整数在 2 进制下的长度最多 64 位加符号。magnitude % base的结果一定小于 36转成unsigned int安全。负号在逆序之前追加到末尾逆序后自然落到开头。用类封装并重载运算符把「值 进制」封装成RadixNumber可以让同一个数值以不同进制呈现也让运算符重载有明确的载体// 适用于 C17#include iostream#include stdexcept#include stringclass RadixNumber {public:RadixNumber(long long value, int base) : value_(value), base_(base) {if (base 2 || base 36) {throw std::invalid_argument(base must be in [2, 36]);}}long long value() const noexcept { return value_; }int base() const noexcept { return base_; }std::string str() const { return toBase(value_, base_); }friend std::ostream operator(std::ostream os, const RadixNumber n) {return os n.str();}friend bool operator(const RadixNumber lhs, const RadixNumber rhs) {return lhs.value_ rhs.value_ lhs.base_ rhs.base_;}friend RadixNumber operator(const RadixNumber lhs, const RadixNumber rhs) {if (lhs.base_ ! rhs.base_) {throw std::invalid_argument(cannot add numbers with different bases);}return RadixNumber(lhs.value_ rhs.value_, lhs.base_);}private:long long value_;int base_;};几个设计点operator写成friend非成员函数。因为左操作数是std::ostream不是RadixNumber若写成成员函数调用形式会变成n std::cout与习惯相反。operator返回std::ostream以便链式调用std::cout a b。返回void会破坏链式表达式。operator要求两个对象进制相同。不同进制相加在语义上不明确与其猜一个默认行为不如抛出异常让调用方显式转换。operator同时比较值和进制。若只比较值RadixNumber(10, 2)与RadixNumber(10, 10)会被判等但它们表示的是不同的字符串。构造函数校验进制范围避免对象处于非法状态。使用示例// 适用于 C17int main() {const RadixNumber a(255, 16);const RadixNumber b(10, 2);const RadixNumber c(-10, 2);std::cout a \n; // FFstd::cout b \n; // 1010std::cout c \n; // -1010const RadixNumber sum RadixNumber(5, 2) RadixNumber(3, 2);std::cout sum \n; // 1000return 0;}5 3 88 的二进制是 1000所以sum打印1000。operator返回的是新对象不修改任一操作数符合算术运算符的惯例。需要小心的语义operator里的lhs.value_ rhs.value_仍可能溢出long long。有符号溢出是 UB不是「回绕成负数」。如果业务值可能接近边界应在相加前检查或改用无符号类型并明确模运算语义。本文的示例只用于演示重载形式不承担大数运算。另外operator只输出数字本身不带进制前缀。若希望输出0xFF或0b1010应在str()里加前缀但这会改变「字符串表示」的含义解析时要对应处理。进制信息已经保存在对象里是否显示由调用方决定。常见坑点1. 余数 10 以上直接加0❌digits.push_back(static_castchar(0 digit));16 进制的 10 变成:。 ✅ 10 以上用A (digit - 10)。2. 忘记逆序❌ 直接把余数依次拼接13 得到1011。 ✅ 转换完std::reverse或从字符串尾部向前填。3. 0 返回空串❌ 循环条件while (value 0)输入 0 时结果为空。 ✅ 单独判断value 0返回0。4. 对LLONG_MIN直接取负❌long long m -value;最小值取负溢出UB。 ✅ 先转unsigned long long再用0ULL -求绝对值。5. 不校验进制❌base 1或base 37仍继续转换除法行为无意义或字符越界。 ✅ 构造/函数入口校验2 base 36。6.operator写成成员函数❌std::ostream operator(std::ostream) const;调用变成n std::cout。 ✅ 写成friend非成员函数左操作数是std::ostream。7.operator返回void❌void operator(...)无法链式输出。 ✅ 返回os的引用。8. 不同进制直接相加❌ 把RadixNumber(10, 2)与RadixNumber(10, 10)相加结果进制取谁没有依据。 ✅ 明确要求同进制不同进制时抛异常或先显式转换。总结环节做法边界取余value % base0 单独处理逆序std::reverse负号最后追加负数转无符号求绝对值LLONG_MIN不能直接取负字符映射0–9 / A–Z进制上限 36operator非成员friend返回ostreamoperator同进制相加溢出仍是 UB进制转换的算法只有「除、余、逆序」三步真正决定代码是否可靠的是 0、负数、最小值和进制范围这四个边界。把这四个边界处理干净再用类封装、用非成员函数重载输出运算符代码既能正确工作也能作为运算符重载的规范示例。