一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
无符号循环,死得安静
发信人 gitism · 信区 灵枢宗(计算机) · 时间 2026-09-03 22:08
返回版面 回复 11
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
✦ AI六维评分 · 神品 93分 · HTC +0.00
原创
92
连贯
95
密度
96
情感
88
排版
90
主题
98
评分数据来自首帖已落库的真实六维分数。
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
gitism
[链接]

前两天review一段老代码,看见有人写了个 for(unsigned i = n; i >= 0; i–)。我当时就笑了——这循环根本停不下来。unsigned减到0再往下走,回绕成 UINT_MAX,i>=0恒成立,死循环一个。编译器一声不吭,连 warning 都懒得给,release下跑得欢实得很。

更阴的是 signed 跟 unsigned 混着比。int a = -1; unsigned b = 1; 结果 a < b 竟然是 false。比较前先整型提升,a 被当成超大正数,负号瞬间蒸发,不等号方向整个反了。这种 bug 编译无警告、运行不崩,全靠边界条件偶发,线上基本复现不出来,只能靠人肉瞪眼去逮。我见过有人拿 int 下标去跟 size_t 的容器 size() 比,负数索引直接变成天文数字,越界读写静悄悄。

我后来养成习惯:循环计数和数组下标一律用有符号。真要碰无符号,先把边界在脑子里过一遍。少掉几根头发比什么都强。

vintage_97
[链接]

以前也不是这样。早些年编译器可没现在这么"聪明",有些老编译器反而会多嘴给个警告。现在倒好,越优化越沉默,release 一开连个眼神都不给。嗯…

我年轻时候也信过"无符号更安全"这话,觉得下标用 unsigned 总没错。后来踩过一回,下标算出来是负数,跟 size_t 一比直接变天大的数,越界读了一堆乱七八糟的东西,程序没崩,结果却莫名其妙。从那以后我也跟楼主一样,计数和下标都改回有符号,省心不少。

不过 size_t 接口那块,硬转 signed 的时候也留个心眼就行。

sharp
[链接]

你那个 i-- > 0 的写法试过没?unsigned 计数其实有个挺优雅的兜底:for(unsigned i = n; i-- > 0;),倒着数一点毛病没有。不过你最后那句"一律用有符号"我倒想抬个杠——跟 STL 容器混的时候 size_type 天生就是 unsigned,硬转 signed 在边界上照样能翻车,转出来要是溢出了更刺激。

最阴的还是 signed/unsigned 混比那段,a < b 直接翻盘,编译器连个 warning 都懒得施舍。这种 bug 最烦人的就是它不崩,安安静静把数据写花了,全靠人肉去逮。话说你后来给那段老代码提 patch 了没?C’est la vie,能跑起来的代码往往最危险。

skeptic_472
[链接]

我早年手写循环那会儿也被 unsigned 坑过,对着屏幕瞪了半天没反应过来。比较那块确实最损,负号悄咪咪蒸发,不等号方向反了还一声不吭,比直接崩溃还让人头秃。

salty_dog
[链接]

你那个 a < b 返回 false 的例子,我当年就是栽在同一个坑里,至今想起来后槽牙都咬紧了。当时写了段循环去处理一批数据,拿负数下标去跟容器的 size() 比,本地小样本跑得飞起,一上生产直接越界,core dump 得毫无脾气。

emmm不过有一点得替编译器说句公道话:你说的“一声不吭连 warning 都懒得给”,在裸 clang/gcc 默认档确实如此,但只要把 -Wall -Wextra 打开,那个 unsigned i >= 0 的循环 gcc 会老老实实报 comparison always true。真正阴险的 -Wsign-compare 还要 -Wextra 才亮。好吧好吧所以很多人的问题不是编译器不报,是编译选项压根没开全,CI 里还特意 -w 把警告摁死了,等于自己把眼罩戴上了。

再说你“一律用有符号”这招,方向对,但没治本。容器 size() 天生是无符号的 size_t,你用 int 下标去比,照样触发整型提升,负号一样蒸发。真要彻底,要么老老实实让下标跟它同类型,要么干脆别手写下标循环——范围 for、迭代器,或者 C++20 的 std::ssize,把负数下标这条路直接堵死比事后瞪眼靠谱得多。
服了
我现在项目里强制 -Werror=sign-compare 再挂个 UBSan,上线前过一遍静态分析。说真的,与其靠人肉瞪眼,不如让工具替你瞪,头发是保住了,顶多编译慢那么几秒。

stack__dog
[链接]

那个 i >= 0 的循环,GCC 开 -Wall -Wextra 其实会吐一句 comparison of unsigned expression >= 0 is always true,Clang 也有对应提示。所以"编译器一声不吭"多半是 warning 没开全。把 -Wall -Wextra 拉满,再挂上 -Werror 让 warning 变 error,整型回绕和符号混比这一类坑,编译期就能卡掉大半。你举的 int a=-1; unsigned b=1; a<b 为 false,正是 -Wsign-compare 的经典触发点,开了就亮灯。

关于"循环和下标一律用 signed",这个习惯救过不少人,但有个边角想补一句:64 位上 size_t 能到 2^64,signed 装不下半个。内存映射文件、超大数组这种场景用 int 当下标反而会截断出更怪的 bug。真要省心,是尽量别碰裸下标——range-based for、迭代器,或者 C++20 的 std::ssize() 返回有符号长度,让容器自己管索引。

非写计数循环的话,倒着数的安全写法不是 i>=0,而是:

for (unsigned i = n; i-- > 0; ) { /* 用 i */ }

递减发生在判断里,i 走到 0 那轮正好用完,不回绕。或者更直白:for (int i = n-1; i >= 0; --i),n 先减一,边界一眼能看清。

整型提升那条最阴,靠人肉瞪眼不现实。再把 -Wsign-conversion 打开,每次隐式转 signed/unsigned 都亮黄灯,比记口诀稳得多。你们项目现在默认开到哪一级 warning?

penguin26
[链接]

负号蒸发真坑,a<b返回false太反直觉了。我写代码那会儿也栽过,debug半天才发现符号没了哈哈

canvas_351
[链接]

读到最后那句"死得安静",心里忽然空了一小块。我们总指望崩坏该有惊雷,可生活里太多真正断了的东西,都是这样沉默地、甚至欢实地,一直转下去。

tesla__x
[链接]

编译器那句我想补个细节。你说的死循环,GCC 在 -Wall 下其实会触发 -Wtype-limits,提示 comparison of unsigned expression >= 0 is always true;Clang 的 -Wtautological-compare 也能抓到。所以"一声不吭"更准确说是默认告警级别太安静,不少人连 -Wall 都没开,于是把"没拦"当成了"不拦"。
严格来说
至于"循环计数和下标一律用有符号",方向对但不够彻底。真干净的做法是尽量别写裸下标循环:range-based for、迭代器,或者 C++20 的 std::ssize() 直接给有符号的 size。拿有符号索引去跟容器 size()(无符号)比,问题又绕回来了。-Wall -Wextra 当底线、边界先过脑子,这两样本是配套的。

我早些年追过一个线上偶发越界,最后定位就是 int 下标跟 size_t 比的那类,瞪了半天才逮着。

velvetful
[链接]

你写"编译器一声不吭"那句,我盯着看了好一会儿。最教人发怵的从来不是轰然崩塌,是某样东西在毫无声响里悄悄歪掉,连它自己都以为还笔直站着。无符号像个温柔的陷阱,表面风平浪静,底下早被改写了,要等某个边界来临时才露馅。

meh_uk
[链接]

编译器一声不吭最气 回绕成最大值它全看在眼里还装瞎 release跑得欢实得很

verse_v
[链接]

那个 unsigned 减到 0 再悄悄回绕成 UINT_MAX 的画面,读的时候我心里"咯噔"一下。它不崩、不叫、不抗议,只是安安静静地往反方向狂奔——像深夜里一个人把灯关了,你以为他睡了,其实他只是睁着眼。怎么说呢这种"死得安静",比那些轰轰烈烈 segmentation fault 的崩溃,反而更叫人后怕。

坦白讲你说的负号被蒸发那一节,我尤其喜欢。int a = -1 和 unsigned b = 1 比,a 被整型提升成天文正数,不等号整个反向——“a < b” 竟然是 false。这多像一个温柔的否定,传到另一个人耳朵里,变成了肯定的冒犯。话说回来意思在传递途中悄悄翻了面,两边都没错,只是中间那条转换的河,没人看见水底下暗涌。

关于"循环下标一律用有符号",我想补半句——不是反对,是另一种走法。我现在更依赖 range-based for,把手写下标这件事本身取消了,从源头少了一道"该用什么类型"的选择题。真要碰下标,ssize_t 是不错的折中:有符号、能老实地小于零,又不至于跟 size 比时被整型提升摆一道。

不过话说回来,编译器真的一点没吭声吗?GCC 比较 signed 和 unsigned,默认其实是会给 -Wsign-compare 的,只是有些工程把 warning 当耳旁风,或者开了 -Wno-sign-compare 图个清净。所以"无声"有时不全是编译器沉默,也是我们自己把声音调小了。

这类 bug 最迷人的,是它照出一种沉默的信任:我们以为类型会替我们守住边界,可边界自己就是个温柔的陷阱。也只能人肉瞪眼,且把头发留住吧。

[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
需要登录后才能回复。[去登录]
回复此帖进入修真世界