在循环中计数时,有符号值和无符号值的区别是什么?

14

所以我在程序中有一个普通的 for 循环,遍历了一个对象向量(这些对象是我定义的类型,如果这很重要的话):

for(int k = 0; k < objects.size(); k++){ ... }

...当我编译时,我收到了这个警告:

warning: comparison between signed and unsigned integer expressions 

这很有道理,因为我认为对于一个向量,size() 返回一个 size_t。但是为什么会有影响呢?难道一个元素的数量(甚至内存块)不是可以计数的整数吗?更重要的是,由于我的程序有多个这样的循环并且经常导致segfault,这可能是其中的一部分原因吗?


1
警告的另一个要点是,如果有符号值(理所当然地)为负数,在转换为无符号类型时,结果往往会非常大,因此比较可能不会得到程序员预期的结果。 - Daniel Fischer
4个回答

11

已经有很好的回答了,但我也想补充一下我的意见:正确的做法是:

for (typename std::vector<MyObject>::size_type i = 0; i < object.size(); ++i) { ... }
只有那些渴望成为语言专家的人才会写出这样的内容,即使他们也可能在阅读到精华之前停下来。使用C++11,您可以利用 decltype
for (decltype(object.size()) i = 0; i < object.size(); ++i) { ... }

或者您可以利用 auto

for (auto i = object.size() - object.size(); i < object.size(); ++i) { ... }

或者你可以直接使用size_t,但是你仍然可能对溢出有疑虑,因为vector<MyObject>size_type可能比size_t更大。(事实并非如此,但没有保证):

for (size_t i = 0; i < object.size(); ++i) { ... }

那么一个诚实的程序员应该怎么做呢?

最简单的解决方案就是STL从一开始就一直在推广的方案。只不过在一开始时,编写这个方案也很麻烦:

for (typename std::vector<MyObject>::iterator_type it = object.begin(); it != object.end(); ++it) { ... }

现在,C++11确实对您有所帮助。您有一些非常好的替代方案,从简单的开始:

for (auto it = object.begin(); it != object.end(); ++it) { ... }

但更好的是(请敲鼓声)……:

for (auto& val : object) { ... }

那是我会使用的。


编辑以添加:

Cory Nelson在评论中指出,还可以使用以下代码缓存object.end()的结果:

for (auto it = object.begin(), end = object.end(); it != end; ++it) { ... }

事实证明,for (var : object) 语法生成的代码与 Cory Nelson 提出的代码非常相似(因此我鼓励他和您只使用后者)。然而,它与其他语法具有微妙的不同语义,包括原始帖子所讨论的迭代。如果您在迭代过程中以一种改变容器大小的方式修改容器,则必须非常仔细地考虑问题,否则会引发灾难。

迭代可能在迭代过程中被修改的 vector 的唯一方法是使用整数索引,就像原始帖子中所述的那样。其他容器则更加宽容。您可以使用循环迭代 STL 映射(map),并在每次迭代中调用 object.end(),(据我所知)即使面对插入和删除也能正常工作,但不要尝试在 unordered_map 或 vector 上尝试这样做。如果您总是在结尾推送(push)并在前面弹出(pop),则可以使用 deque 进行迭代,这在广度优先遍历中使用 deque 作为队列时非常方便;我不确定是否可以从 deque 的末端弹出。

实际上应该有一个简单的摘要,概述容器类型对迭代器和元素指针(它们并不总是与迭代器相同)的容器修改的影响,因为这一切都是由标准规定的,但我从未在任何地方找到过。如果您发现了这样的内容,请告诉我。


如果您不想在每次迭代中调用end(),这对于不支持范围for的编译器很有好处:您错过了for(auto it = object.begin(), end = object.end(); it != end; ++it) { ... } - Cory Nelson
@CoryNelson,是的,for (auto& val : object) 就是这个意思。然而,对于大多数STL容器,也许所有的容器,执行 object.end() 实际上没有什么代价。(特别是对于向量,迭代器只是指针,end() 是一个微不足道的访问器。) - rici
有趣的是,至少在 VC++ 中,vector 的迭代器在任何情况下都不是指针了。当然,它们会被优化为等效的指针。 - Cory Nelson
@CoryNelson:有趣。那么它们除了一个指针之外还有其他的东西吗?这有什么好处呢?(在gnu libstdc++中,如果有适当的Alloc trait,则它们不是指针,这可能对非标准分配器是必要的,但通常它们只是通过一长串typedefs转换为T*。) - rici
@rici:在您的最后建议中,是否也可以获得一个公共整数计数器来显示回合数,或者这是不可能的(除非创建一个单独的循环计数器)? - arc_lupus
@arc_lupus:你需要自己添加循环计数器。 - rici

11
object.size()返回一个大于k的最大可表示值的值时,问题就会出现。由于k是有符号的,相对于size_t1,它只有一半的最大值。
现在,在您特定的应用程序中可能不会发生这种情况(在典型的32位系统上,您的集合中的对象数量将超过20亿个),但使用正确的类型始终是一个好主意。 1.预先反驳:是的,在使用典型的二进制补码算术和intsize_t用相同比特数表示的机器上才成立。

3

在大多数情况下,这并不重要,直到您的向量包含的元素超过有符号整数可以表示的数量。


2

在有关循环变量的有符号/无符号比较的警告流中,重要的警告可能会丢失。甚至一些有符号/无符号比较警告也很重要!因此,通过定义一个大小函数来消除不重要的警告,就像这样:

#include <stddef.h>    // ptrdiff_t
#include <utility>     // std::begin, std::end

typedef ptrdiff_t Size;
typedef Size Index;

template< class Type >
Size nElements( Type const& c )
{
    using std::begin;  using std::end;
    return end( c ) - begin( c );
}

那么你可以只是简单地写例如。
for( int i = 0;  i < nElements( v );  ++i ) { ... }

或者,使用迭代器,例如:

for( auto it = begin( v );  it != end( v );  ++it ) { ... }

或者使用C++11基于范围的for循环。

for( auto const& elem : v ) { ... }

无论如何,在最高实用的警告级别下进行干净的编译是很重要的,以便消除那些段错误和其他错误。

另一个你应该关注的领域是C样式转换:摆脱它们!;-)


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