为什么在C和C++的循环中使用int而不是unsigned int?

53

这是一个相当愚蠢的问题,但为什么在C或C ++中定义数组的for循环时通常使用int而不是unsigned int

for(int i;i<arraySize;i++){}
for(unsigned int i;i<arraySize;i++){}

我认识到在执行除了数组索引以外的操作时使用 int 和使用 C++ 容器时使用迭代器的好处。这是否只是因为在循环遍历数组时没有关系?或者我应该避免全部使用它,而改用其他类型,如 size_t


19
写得更少。 - slartibartfast
就像为什么你不总是用别人的名字和姓氏称呼他们,而只是用他们的名字来称呼他们一样? - user482594
18
实际上,我更喜欢使用 size_t 作为索引,它保证足够大且输入量比 unsigned int 更少。 - Matteo Italia
2
一篇非常好的文章,解释了为什么我们需要 size_tptrdiff_t:http://www.viva64.com/en/a/0050/ - Blagovest Buyukliev
@Blagovest:那篇文章的动机部分不错,但其余部分充满了错误信息(类型等价性、在size_t中存储指针的能力等),并完全忽略了ptrdiff_t的有符号溢出和范围问题。我不敢称之为“非常好”。 - R.. GitHub STOP HELPING ICE
"unsigned" 和 "usigned int" 是相同的。 - Z boson
11个回答

43

从逻辑角度来看,使用 int 进行数组索引更加正确。

C 和 C++ 中的 unsigned 语义并非真正意义上的“非负”,而更像是“位掩码”或“模数整数”。

为了理解为什么 unsigned 不适合作为“非负”数字的类型,请考虑以下荒谬的说法:

  • 将一个可能为负数的整数加到一个非负整数上,你会得到一个非负整数
  • 两个非负整数之差总是一个非负整数
  • 将一个非负整数乘以一个负整数,你会得到一个非负结果

显然以上语句都没有任何意义...但这确实是 C 和 C++ 中 unsigned 的语义。

对于容器的大小,实际上使用 unsigned 类型是 C++ 的设计失误,不幸的是,为了向后兼容,我们不得不永远使用这种错误的选择。你可能喜欢“unsigned”的名称,因为它类似于“非负数”,但名称是无关紧要的,关键是语义...而unsigned与“非负数”相去甚远。

因此,在大多数向量循环中,我个人更喜欢以下的形式:

for (int i=0,n=v.size(); i<n; i++) {
    ...
}

(当然,假设向量大小在迭代期间不会更改,并且实际上需要该索引作为主体,否则 for(auto& x : v)...更好)。

尽快摆脱无符号类型并使用普通整数的好处是避免了由于unsigned size_t设计错误而产生的陷阱。例如:

// draw lines connecting the dots
for (size_t i=0; i<pts.size()-1; i++) {
    drawLine(pts[i], pts[i+1]);
}

如果pts向量为空,那么上面的代码将会有问题,因为在这种情况下pts.size()-1是一个巨大的无意义的数字。处理表达式a < b-1a+1 < b即使对于常用值来说也不相同,就像在雷区跳舞一样。

从历史上看,将size_t定义为无符号整数的理由是为了能够使用额外的位来表示值,例如在16位平台上能够有65535个元素而不仅仅是32767个。在我看来,即使在当时,这种错误的语义选择所带来的额外成本也不值得这种收益(如果现在32767个元素不够用,那么65535个元素也不会够用很长时间)。

无符号整数非常棒且非常有用,但不适合表示容器大小或索引;对于大小和索引,普通的有符号整数工作得更好,因为它们的语义是您所期望的。

无符号整数是理想的类型,当您需要模算术属性或者想要在比特级别上操作时。


1
我认为你是正确的,因为Java(一种“改进”的C ++)不支持无符号整数。此外,我认为编写该行的正确方式是:size_t arr_index; for (size_t i=1; i<=pts.size(); i++) { arr_index = i - 1; } - carlos
2
@carlos:不,如果size_t被正确定义,那将是正确的方式。不幸的是,设计错误使size_t成为了一个无符号数,因此这些值最终具有位掩码语义。除非您认为容器的大小是位掩码,否则使用size_t是错误的选择。标准C++库不幸地做出了这个选择,但没有人强迫我在我的代码中重复同样的错误。我的建议是尽快放弃size_t,而是使用常规整数,而不是将逻辑弯曲成这样,以便它也可以与size_t一起工作。 - 6502
4
不仅仅是16位平台。使用当前的size_t,你可以在3G / 1G内存拆分的IA-32 Linux上使用大小为2.1G的vector<char>。如果size_t是有符号的,如果你将向量从<2G增加到更大的值会怎样呢?突然间,大小会变成负数。这根本没有任何意义。编程语言不应强制实施这种人为的限制。 - Ruslan
1
@Ruslan:令人惊讶的是,即使是相当不错的程序员也会坚持这个非常薄弱的论点:一个包含单个字节的单个数组占用大部分地址空间的想法是完全荒谬的,我确信这并不经常面临,但显然被“无符号大小”狂热者认为非常重要。拥有一种数据类型,能够使用所有位并具有“非负”整数的语义,这将是很好的,但不幸的是,在C++中不存在这样的类型,使用unsigned而不是它是无意义的。 - 6502
1
我知道你在这个答案上投入了很多精力,我很感激你的努力和示例。不幸的是,这个答案是错误的,而 unsigned size_t 设计是逻辑和显然的选择(并且不是一个错误)。我添加了自己的答案来进一步解释。当然,你可以自由地不同意我的答案并给它点踩。 - Myst
显示剩余11条评论

32

这是一个更普遍的现象,很多人都没有使用正确的整数类型。现代C语言有语义化的typedef,其比原始的整型类型更容易使用。例如,所有的“大小”应该被定义为 size_t 类型。如果你在应用程序变量中系统地使用这些类型,循环变量也会更容易使用。

我曾经看到过几个难以检测的bug,其中一些来自于使用了 int 等类型。当处理大矩阵等数据时,代码突然崩溃。只要使用正确的类型编写代码就可以避免这种情况发生。


12
一个大小的正确类型是size_t,不幸的是,size_t本身使用了错误的类型(无符号),这是许多bug的根源。我更喜欢在代码中使用语义上正确的类型(例如int)而不是使用形式上正确但语义上错误的类型。使用int可能会在非常大(极其大)的值时遇到bug...使用无符号值的行为比较接近每天使用的疯狂行为(0)。 - 6502
1
@6502,对此的看法似乎存在很大差异。您可以查看我的博客文章:http://gustedt.wordpress.com/2013/07/15/a-praise-of-size_t-and-other-unsigned-types/ - Jens Gustedt
4
@JensGustedt认为语义错误不是一种观点,除非您认为当b有一个元素而a没有时a.size() - b.size()应该约为40亿。某些人认为unsigned对于非负数是一个绝妙的想法,你是正确的,但我的印象是他们过于注重名称而忽略了实际含义。在认为无符号类型对于计数器和索引是个坏主意的人中,Bjarne Stroustrup是其中之一...请参见https://dev59.com/iWkw5IYBdhLWcg3wDWTL - 6502
2
@6502,正如我所说,意见各不相同。而且SO不应该是讨论意见的地方,尤其是那些没有参与讨论的人的意见。Stroustrup对于很多事情都是一个参考,但并不适用于C。 - Jens Gustedt
2
@6502 不,我是说宣称“无符号”和“非负”是不同概念是荒谬的。它们是相同的概念。问题不在于无符号。问题在于两个无符号值的减法应该是带符号的。“a.size()”应该是“size_t”,但“a.size()- b.size()”应该是“ptrdiff_t”,就像两个指针的减法不会给你一个指针,而是一个“ptrdiff_t”。毕竟,指针基本上与“size_t”相同。 - Miles Rout
显示剩余3条评论

5
这纯粹是懒惰和无知。你应该始终使用正确的索引类型,除非你有进一步的信息限制了可能索引的范围,否则 size_t 是正确的类型。
当然,如果维度是从文件中的单字节字段读取的,则您知道它在 0-255 范围内,int 将是一个完全合理的索引类型。同样,如果您循环固定次数,比如 0 到 99,则 int 也可以使用。但还有另一个不使用 int 的原因:如果您在循环体中使用 i%2 来区分偶数/奇数索引,那么当 i 为有符号时,i%2 比无符号时要昂贵得多...

1
请仅翻译文本内容:看我的回答中的第三点,它并不是“纯粹”的懒惰/无知。 - chacham15
4
虽然如此,这并不改变代码错误的事实。以下是修复它的一种方法:for (size_t i=10; i-->0; ) - R.. GitHub STOP HELPING ICE

4

区别不大。使用int的好处是它是有符号的。因此,int i < 0 是有意义的,而unsigned i < 0 就没有多少意义。

如果计算索引,那可能会有好处(例如,如果某个结果为负数,你可能永远不会进入一个循环)。

当然,这样写起来更简单 :-)


typedef unsigned us; 和它还有更多需要编写的内容。 - user142019
3
@WTP - 你是那些即使在“:-)”旁边也无法理解讽刺的人之一吗?嗯,我想那里没有治愈的方法...... (说明:原文使用了一种带有表情符号的讽刺语气,对@WTP的理解能力提出质疑。翻译保留了原文的意思和语气,并将其转化为中文。) - littleadv
负数的大小或负数的索引没有意义。 - Miles Rout
@MilesRout:试图操作负数项通常与试图操作非常大的正数项有不同的影响。如果将一个没有任何项的集合传递给一个应该操作除最后一项之外所有项的函数,那么将要处理的项数被识别为-1似乎比被识别为SIZE_MAX更加清晰。 - supercat

2
使用int来索引数组是一种传统的做法,但仍然被广泛采用。 int只是一个通用的数字类型,不对应平台的寻址能力。如果它比实际长度短或长,尝试索引超出非常大的数组时可能会遇到奇怪的结果。
在现代平台上,off_tptrdiff_tsize_t保证了更高的可移植性。
这些类型的另一个优点是它们给代码读者提供了上下文。当你看到上述类型时,你知道代码将进行数组下标或指针算术运算,而不仅仅是任何计算。
因此,如果你想编写弹性、可移植和上下文敏感的代码,你可以牺牲一些按键次数来实现。
甚至GCC支持一个typeof扩展,它可以让你从重复输入同样的类型名中解脱出来:
typeof(arraySize) i;

for (i = 0; i < arraySize; i++) {
  ...
}

然后,如果你改变arraySize的类型,i的类型也会自动更改。


2
公平地说,在除了最不常见的32位和64位平台之外,你需要至少有40亿个元素才能出现这样的问题。而且,具有较小“int”的平台通常也具有更少的内存,使得“int”在一般情况下足够使用。 - user395760
1
@delnan:这并不是那么简单。这种推理方式在过去曾导致非常严重的漏洞,甚至包括像DJB这样自认为是安全之神的人。 - R.. GitHub STOP HELPING ICE

1

这真的取决于程序员。有些程序员喜欢类型完美主义,所以他们会使用与其比较的任何类型。例如,如果他们正在迭代一个C字符串,你可能会看到:

size_t sz = strlen("hello");
for (size_t i = 0; i < sz; i++) {
    ...
}

如果他们只是做了10次某事,你可能仍然会看到int

for (int i = 0; i < 10; i++) {
    ...
}

0

我使用 int 类型是因为它需要更少的物理输入,而且这并不重要 - 它们占用相同的空间,除非你的数组有数十亿个元素,否则如果你没有使用 16 位编译器(我通常不会),你不会溢出。


5
дёҚдҪҝз”ЁintиҝҳеҸҜд»ҘжҸҗдҫӣжӣҙеӨҡжңүе…іеҸҳйҮҸзҡ„дёҠдёӢж–ҮдҝЎжҒҜпјҢеҸҜиў«и§ҶдёәиҮӘжҲ‘и®°еҪ•д»Јз ҒгҖӮжӯӨеӨ–пјҢиҜ·еңЁжӯӨеӨ„йҳ…иҜ»пјҡhttp://www.viva64.com/en/a/0050/ - Blagovest Buyukliev

0

因为除非你有一个大小大于2GB的char数组,或者4GB的short类型或8GB的int类型等,否则变量是否带符号并不重要。

所以,为什么要打更多的字,当你可以打更少的字呢?


1
但是,如果arraySize是可变的,并且您想编写防弹代码,则off_tptrdiff_tsize_t仍然具有一定的重要性。 - Blagovest Buyukliev
是的,如果你可能有如此巨大的数组,那绝对是必要的,但由于人们通常没有这样的需求,所以他们只使用简单易写的 int。例如,如果您正在使用 O(n^2) 对 int 数组进行排序,并且元素超过 2M,即使您拥有 8GB 的内存,您也基本上需要永远等待数组排序完成。因此,通常即使您正确编制索引,大多数程序在输入如此之大的情况下都是无用的。那么为什么要让它们防弹呢? - Shahbaz
1
@Shahbaz:如果传递一个巨大的数组导致排序需要数周才能完成,我们大多数人可能会觉得这只是不幸,但如果传递一个巨大的数组导致获得了 root shell,我们会认为这完全是不可接受的。 - R.. GitHub STOP HELPING ICE
@R.. 不要误会,我并不是在说这是好的,我只是回答了一个问题,即为什么人们总是使用 int - Shahbaz
我正在回复你最近的评论。 - R.. GitHub STOP HELPING ICE

0

除了输入更短的问题外,原因是它允许负数。

由于我们无法预先确定一个值是否可能为负数,大多数使用整数参数的函数都采用有符号类型。由于大多数函数使用有符号整数,因此在循环等方面使用有符号整数通常比使用无符号整数更容易。否则,您可能需要添加大量的类型转换。

随着我们向64位平台迈进,有符号整数的无符号范围应该足以满足大多数目的。在这些情况下,没有太多理由不使用有符号整数。


负值是一个关键点,而你的回答是唯一提到这一点的。但是,遗憾的是,有隐式标准转换在有符号和无符号参数类型之间,混合使用它们可能会导致问题,而不是你所描述的“必须添加一堆类型转换”的不便但安全的情况。而且,“随着我们转向64位平台,有符号整数的无符号范围...”实际上对于大多数编译器/操作系统并没有增加 - int仍然倾向于是32位,而long从32位移动到64位。 - Tony Delroy

0

在大多数情况下,使用有符号的 int 是一个错误,很容易导致潜在的错误和未定义的行为。

使用 size_t 可以匹配系统的字长(64位系统上为64位,32位系统上为32位),始终允许正确的循环范围,并最小化整数溢出的风险。

int 的建议是为了解决一个问题,即经验不足的程序员经常错误地编写反向 for 循环(当然,int 可能不在正确的循环范围内):

/* a correct reverse for loop */
for (size_t i = count; i > 0;) {
   --i; /* note that this is not part of the `for` statement */
   /* code for loop where i is for zero based `index` */
}
/* an incorrect reverse for loop (bug on count == 0) */
for (size_t i = count - 1; i > 0; --i) {
   /* i might have overflowed and undefined behavior occurs */
}

一般来说,有符号和无符号变量不应混在一起使用,因此有时使用 int 是不可避免的。然而,for 循环的正确类型通常是 size_t
关于有符号变量比无符号变量更好的这种误解,有一个很好的讲话,你可以在 YouTube (Signed Integers Considered Harmful by Robert Seacord) 找到它。 简而言之:有符号变量比无符号变量更危险,需要更多的代码(几乎在所有情况下都应该优先选择无符号变量,并且只有在逻辑上不期望出现负值时才能明确选择无符号变量)。
对于无符号变量,唯一需要考虑的是溢出边界,其行为被严格定义为环绕,并使用清晰定义的模数数学。
这允许进行单个边缘情况测试以捕获溢出,并且该测试可以在执行数学操作执行。
然而,对于有符号变量,溢出行为是未定义的(UB),负范围实际上比正范围更大 - 这些问题增加了必须测试和明确处理的边缘情况,执行数学运算之前。

比如说,INT_MIN * -1 等于多少?(预处理器会保护你,但如果没有它,你就会陷入困境)。

P.S.

至于 @6502 在他们的答案中提供的示例,整个问题又是试图走捷径和一个简单的缺失的 if 语句。

当一个循环假设数组中至少有两个元素时,这个假设应该事先进行测试。例如:

// draw lines connecting the dots - forward loop
if(pts.size() > 1) { // first make sure there's enough dots
  for (size_t i=0; i < pts.size()-1; i++) { // then loop
    drawLine(pts[i], pts[i+1]);
  }
}
// or test against i + 1 : which tests the desired pts[i+1]
for (size_t i = 0; i + 1 < pts.size(); i++) { // then loop
  drawLine(pts[i], pts[i+1]);
}
// or start i as 1 : but note that `-` is slower than `+`
for (size_t i = 1; i < pts.size(); i++) { // then loop
  drawLine(pts[i - 1], pts[i]);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接