☰
C++有符号和无符号之间的转换问题
2026/11/5 2:13:48 网站建设 项目流程

前言


C++ 的整数类型分为有符号和无符号两套,它们参与同一个表达式时会按一套固定的「寻常算术转换」(usual arithmetic conversions)规则统一类型。问题在于,这套规则会把有符号数悄悄转成无符号数,负数随即变成一个非常大的正数。表达式仍然合法,编译器最多给个警告,结果却与直觉相反。


常见的误解有三个。第一,以为-1 < 1u为真,实际上-1被转换成无符号最大值,比较结果为假。第二,以为unsigned变量递减到 0 之后会变成 -1,实际上会回绕成该类型的最大值,循环因此永不结束。第三,以为只要把类型写清楚就不会混用,忽略了sizeof、size()、vector::size_type这些接口返回的都是无符号类型。


本文按「转换规则、赋值转换、比较陷阱、安全写法」四部分讲。文中会明确区分「良定义的回绕」「实现定义」和「UB」,因为这三者在有符号/无符号话题里经常被混为一谈。


一、整型提升与寻常算术转换


第一步是整型提升(integral promotion):bool、char、short以及它们的无符号版本,在参与算术运算时通常先提升为int(如果int能表示其全部值),否则提升为unsigned int。所以两个unsigned char相加,结果类型往往是int,不是unsigned char。


第二步是寻常算术转换,规则可以简化为:



  1. 如果两个操作数类型相同,结束。

  2. 如果两者符号性相同,较低等级的类型转换为较高等级。

  3. 如果无符号类型的等级不低于有符号类型,有符号操作数转换为无符号类型。

  4. 否则,如果有符号类型能表示无符号类型的全部值,无符号转换为有符号;否则两者都转换为该有符号类型对应的无符号类型。


现实中最常触发的是第 3 条:int与unsigned int运算,int转成unsigned int。long与unsigned int在 32 位平台上也可能触发类似的转换。




表达式转换后类型说明



int + unsigned intunsigned int有符号转无符号

long + unsigned int(32 位)unsigned long等级比较后转无符号

long long + unsigned intlong long有符号能表示全部无符号值

unsigned char + unsigned charint先整型提升

char + intintchar提升为int



二、赋值与显式转换:回绕还是实现定义


把一个超出目标类型范围的值赋给无符号类型,标准规定结果是「对 2 的 N 次方取模」,这是良定义的。例如 32 位unsigned赋-1,结果是4294967295;unsigned char赋300,结果是44。


把超出范围的值赋给有符号类型,C++20 之前是实现定义行为(implementation-defined behavior),实现必须选一个结果并记录,通常是补码回绕,但标准不保证。C++20 起,标准要求有符号整数采用补码表示,超出范围的转换按模 2 的 N 次方回绕,结果变为良定义。也就是说,同一段代码在 C++17 和 C++20 下的「可移植性保证」不同。


// 适用于 C++17
#include <cstdint>

void demo() {
std::uint8_t a = 300; // 300 % 256 = 44,良定义
std::int8_t b = static_cast<std::int8_t>(200); // C++17 实现定义,通常为 -56
std::uint32_t c = static_cast<std::uint32_t>(-1); // 4294967295,良定义
(void)a; (void)b; (void)c;
}

还有一类容易被忽略的转换:浮点到整数。如果浮点值超出目标整数类型的可表示范围(包括把负数转成无符号),行为是 UB,不是回绕。所以static_cast<unsigned>(-1.5)不能想当然地认为得到某个固定值。


三、比较与循环:负数变成最大值


危险主要集中在比较和循环条件上,因为这里不会产生编译错误,只会产生错误结果。


// 适用于 C++17
#include <cstddef>
#include <iostream>
#include <vector>

int main() {
const int i = -1;
const unsigned u = 1;
std::cout << (i < u) << '\n'; // 按标准语义为 0,i 转成 4294967295

std::vector<int> v{1, 2, 3};
for (std::size_t k = v.size(); k-- > 0; ) { // 先比较、再自减
std::cout << v[k] << ' ';
}
std::cout << '\n';
}

第二段用k-- > 0而不是k >= 0,是因为size_t永远大于等于 0,写>= 0会得到恒真条件。表达式k-- > 0先用当前值比较,再把k减 1;当k为 0 时比较失败、循环退出,自减产生的回绕值不会再被使用。


更隐蔽的是「空容器减一」:


// 适用于 C++17
#include <string>

bool has_tail(const std::string& s) {
return s.find(',') < s.size() - 1; // 空串时 size()-1 回绕成极大值
}

空串的size()是 0,size() - 1在size_t上回绕为SIZE_MAX,比较结果几乎总是真。正确写法是先判断!s.empty(),或直接比较pos != std::string::npos && pos + 1 < s.size()。


四、安全写法


第一,容器下标和长度统一用std::size_t,不要用int接size()。C++20 可以用std::ssize()得到有符号长度,循环变量用std::ptrdiff_t更自然。


第二,确实需要混比时,先做范围检查再转换,而不是直接static_cast:


// 适用于 C++17
#include <cstddef>
#include <limits>

bool to_int(std::size_t value, int& out) {
if (value > static_cast<std::size_t>(std::numeric_limits<int>::max()))
return false;
out = static_cast<int>(value);
return true;
}

第三,C++20 提供了std::cmp_less、std::cmp_greater、std::cmp_equal等函数(在<utility>中),它们按数学值比较,不受符号转换影响。C++17 及以前只能自己写比较函数,或借助编译器的-Wsign-compare警告。


第四,打开警告并把符号比较警告当作错误处理。GCC / Clang 的-Wsign-compare(-Wall通常包含)、MSVC 的/W4都会提示大部分混比。警告不是噪音,它标出的正是本文讨论的这类问题。


第五,注意char的符号性:char是否有符号是实现定义的,把char当小整数做算术时,结果可能随平台不同。需要明确语义时用signed char或unsigned char。


常见坑点



  1. ❌for (unsigned i = n; i >= 0; --i),条件恒真,循环不结束。


✅ 用for (unsigned i = n; i-- > 0; ),或改用有符号的int/ptrdiff_t。



  1. ❌if (-1 < vec.size()),-1转成极大无符号数,结果为假。


✅ 避免与无符号值比较负数;C++20 用std::cmp_less。



  1. ❌int n = v.size();在大容器上窄化,可能溢出。


✅ 用std::size_t,或 C++20 的std::ssize配合ptrdiff_t。



  1. ❌s.size() - 1在空串上直接使用。


✅ 先判空,或用pos + 1 < s.size()这类不产生下溢的写法。



  1. ❌unsigned a = 0; a -= 1;以为得到 -1。


✅ 无符号回绕是良定义的最大值;需要负数就用有符号类型。



  1. ❌static_cast<unsigned>(negative_double)期望回绕。


✅ 浮点到整数越界是 UB,必须先检查范围。



  1. ❌ 把char当作一定是 0..255 或 -128..127。


✅ 明确用unsigned char或signed char,不要依赖char的符号性。



  1. ❌ 依赖 C++17 下「有符号溢出回绕」的行为。


✅ 有符号溢出是 UB;C++20 起转换才按补码定义,但运算溢出仍需避免。


总结




场景规则风险



无符号赋值越界模 2^N 回绕,良定义逻辑值可能不是预期

有符号赋值越界C++17 实现定义;C++20 起补码回绕跨标准行为不同

混合算术有符号转无符号(常见情况)负数变极大值

混合比较同上条件判断反转

浮点转整数越界UB结果完全不可靠

char符号性实现定义跨平台差异



有符号与无符号的转换规则本身并不复杂,难的是它会在比较、循环、容器长度这些高频位置静默生效。把长度类型统一、把混比改成显式范围检查、把编译器警告当回事,三类问题基本可以清零。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询