在C和C ++中使用无符号整数


23

我有一个很简单的问题,困扰了我很长时间。我正在处理网络和数据库,因此要处理的许多数据是32位和64位计数器(无符号),32位和64位标识ID(也没有用于符号的有意义的映射)。我几乎从不处理任何可以表示为负数的实际单词问题。

我和我的同事经常使用无符号的类型,如uint32_tuint64_t对这些问题,并因为它发生如此频繁,我们也将它们用于数组索引和其他常见的整数用途。

同时,我正在阅读的各种编码指南(例如Google)都不鼓励使用无符号整数类型,据我所知,Java和Scala都没有无符号整数类型。

因此,我想不出正确的做法:在我们的环境中使用带符号的值将非常不便,同时编码指南要求严格执行此操作。


Answers:


31

关于这一点,有两种思想流派,但永远不会达成共识。

第一种观点认为,有些概念本质上是无符号的,例如数组索引。对那些使用带符号的数字没有意义,因为这可能会导致错误。它还可能对事物施加不必要的限制-使用带符号的32位索引的数组只能访问20亿个条目,而切换到无符号的32位数字则可以允许40亿个条目。

第二种观点认为,在任何使用无符号数的程序中,迟早您都会进行混合的有符号-无符号算术运算。这会产生奇怪和意想不到的结果:将大的无符号值强制转换为带符号将产生一个负数,反之,将一个负数强制转换为无符号将产生一个正数。这可能是错误的重要来源。


8
编译器检测到混合的有符号无符号算术问题;只需保持您的构建无警告(警告级别足够高)。此外,int短于键入:)
rucamzu 2014年

7
坦白:我支持第二种思想,尽管我了解无符号类型的注意事项:int对于数组索引而言,99.99%的时间绰绰有余。有符号-无符号算术问题更为常见,因此在应避免的方面具有优先权。是的,编译器会对此发出警告,但是在编译任何大型项目时会收到​​多少警告?忽视警告是危险的,也是不当行为,但在现实世界中……
Elias Van Ootegem 2014年

11
为答案+1。 警告前方意见Bl贬不一:1:我对第二种思想的反应是:我敢打赌,任何从C 的无符号整数类型中获得意外结果的人都将在C语言中具有未定义的行为(而不是纯学术性的行为)他们使用签名整数类型的非平凡C程序。如果您不太了解C,以至于认为无符号类型是更好使用的类型,我建议您避免使用C。2:在C中,数组索引和大小只有一种正确的类型,那就是size_t,除非有特殊情况否则有充分的理由。
mtraceur '16

5
您遇到麻烦而没有混合的签名。只需计算unsigned int减去unsigned int。
gnasher729

4
Simon不会对您有任何疑问,而只会对第一个思想流派提出质疑,即认为“有些概念本质上是无符号的,例如数组索引”。特别是:“在C中,数组索引只有一种正确的类型……”, 胡说!。我们DSPer始终使用负索引。尤其是偶数或奇数对称脉冲响应是非因果的。和LUT数学。我处于第二个学派,但是我认为在C和C ++中同时使用带符号和无符号整数会很有用。
罗伯特·布里斯托

21

首先,Google C ++编码指南不是一个很好的遵循准则:它避免了异常,增强等现代C ++的必需品。其次,仅仅因为某条准则适用于X公司并不意味着它就适合您。我将继续使用无符号类型,因为您非常需要它们。

C ++的一个好的经验法则是:int除非您有充分的理由使用其他东西,否则请选择。


8
那根本不是我的意思。构造函数用于建立不变式,由于它们不是函数,因此无法简单地return false确定是否建立不变式。因此,您可以将事物分开并为对象使用init函数,或者可以抛出std::runtime_error,让堆栈平移,让所有RAII对象自动清理自身,并且开发人员可以在方便的情况下处理异常。你这样做。
bstamour 2014年

5
我看不到应用程序的类型有何不同。每当您在对象上调用构造函数时,都在使用参数建立不变量。如果不能满足该不变性,则您需要发出错误信号,否则您的程序就不会处于良好状态。由于构造函数无法返回标志,因此引发异常是自然的选择。请就为什么商业应用程序不能从这种编码风格中受益提供一个可靠的论据。
bstamour 2014年

8
我高度怀疑所有C ++程序员中有一半不能正确使用异常。但是无论如何,如果您认为您的同事没有能力编写现代C ++,那么一定要远离现代C ++。
bstamour 2014年

6
@ zzz777不使用异常吗?是否有由公共工厂函数包装的私有构造函数,它们捕获异常并执行什么操作-返回a nullptr?返回一个“默认”对象(可能意味着什么)?您什么都没解决-您只是将问题隐藏在地毯下,希望没有人发现。
Mael

5
@ zzz777如果您还是要使盒子崩溃,为什么还要关心它是否由于异常而发生signal(6)?如果使用异常,则50%知道如何处理它们的开发人员可以编写良好的代码,其余的可以由其同级承担。
IllusiveBrian

6

其他答案缺少现实世界中的示例,因此我将添加一个。我(个人)尝试避免使用无符号类型的原因之一。

考虑使用标准size_t作为数组索引:

for (size_t i = 0; i < n; ++i)
    // do something here;

好的,完全正常。然后,考虑由于某些原因,我们决定更改循环的方向:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

现在,它不起作用。如果我们将其int用作迭代器,则不会有问题。在过去的两年中,我已经两次看到这样的错误。一旦它在生产中发生并且很难调试。

对我来说,另一个原因是令人讨厌的警告,它使您每次都这样写:

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

这些都是次要的事情,但它们加起来。我觉得如果到处都只使用带符号的整数,代码会更干净。

编辑: 当然,这些示例看起来很愚蠢,但我看到有人犯了这个错误。如果有避免这种麻烦的简单方法,为什么不使用它呢?

当我用VS2015或GCC编译以下代码时,我看不到带有默认警告设置的警告(即使使用-Wall for GCC)。您必须请求-Wextra才能在GCC中获得有关此警告。这是您应该始终使用Wall和Wextra进行编译(并使用静态分析器)的原因之一,但是在许多现实生活项目中,人们却不这样做。

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}

对于带符号的类型,您可能会更加错误。。。您的示例代码是如此死脑筋,而且很明显,如果您要求警告,任何体面的编译器都会发出警告。
Deduplicator

1
过去,我采取了for (size_t i = n - 1; i < n; --i)使它正常工作的恐怖手段。
西蒙B

2
说到size_t反向的for循环,有一种编码准则为for (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong

2
@rwong Omg,这很丑。为什么不只是使用int?:)
Aleksei Petrenko '18

1
@AlexeyPetrenko-注意,当前的C和C ++标准都不能保证该int大小足以容纳的所有有效值size_t。特别是,int可能只允许数字最多2 ^ 15-1,并且通常在内存分配限制为2 ^ 16(或在某些情况下甚至更高)的系统上允许这样做。 long尽管仍然不能保证有效,但也许是比较安全的选择。仅size_t保证在所有平台上和所有情况下均可使用。
Jules

4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

这里的问题是您以一种不明智的方式编写了循环,从而导致了错误的行为。循环的构造就像初学者可以教有符号类型(这是正确的),但根本不适合无符号值。但这不能用作反对使用无符号类型的计数器参数,这里的任务是简单地使您的循环正确。可以很容易地修复此问题,以使其可靠地用于无符号类型,如下所示:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

这种改变只是简单地还原了比较和递减操作的顺序,在我看来,这是在反向循环中处理无符号计数器的最有效,最简单,最简洁的方法。使用while循环时,您会做(直觉)相同的事情:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

不会发生下溢,就像在带符号的计数器循环的众所周知的变体中那样,隐式地覆盖空容器的情况,并且与带符号的计数器或正向循环相比,循环的主体可以保持不变。您只需要习惯一开始看起来有些奇怪的循环结构即可。但是,当您看到12次之后,就再也没有无法理解的东西了。

如果初学者课程不仅会显示带符号类型的正确循环,而且会显示无符号类型的正确循环,我将很幸运。这样可以避免将IMHO归咎于不知情的开发人员,而不是归咎于未签名的类型。

高温超导


1

存在无符号整数是有原因的。

例如,考虑将数据作为单个字节处理,例如在网络数据包或文件缓冲区中。您可能偶尔会遇到24位整数之类的野兽。从三个8位无符号整数轻松地进行位移位,而对于8位有符号整数则不那么容易。

或考虑使用字符查找表的算法。如果字符是8位无符号整数,则可以按字符值索引查找表。但是,如果编程语言不支持无符号整数,该怎么办?您将对数组具有负索引。好吧,我想您可以使用类似的方法,charval + 128但这很丑陋。

实际上,许多文件格式都使用无符号整数,如果应用程序编程语言不支持无符号整数,则可能会出现问题。

然后考虑TCP序列号。如果编写任何TCP处理代码,则肯定要使用无符号整数。

有时,效率如此重要,以至于您确实需要额外的无符号整数。考虑以数百万计的物联网设备。然后可以证明有大量的编程资源可用于微优化。

我认为,避免使用无符号整数类型(混合符号算术,混合符号比较)的理由可以通过带有适当警告的编译器来克服。这些警告通常默认情况下是不启用的,但是请参见例如-Wextra或单独查看-Wsign-compare(在C by中自动启用-Wextra,尽管我认为在C ++中不是自动启用)和-Wsign-conversion

但是,如有疑问,请使用带符号的类型。很多时候,这是一个行之有效的选择。并启用那些编译器警告!


0

在许多情况下,整数实际上并不表示数字,但是例如位掩码,id等。基本上,将1加到整数上不会产生任何有意义的结果。在这种情况下,请使用unsigned。

在许多情况下,您都使用整数进行算术运算。在这些情况下,请使用带符号的整数,以避免出现零附近的错误行为。请参阅大量带有循环的示例,其中将循环运行到零可能使用非常不直观的代码,或者由于使用无符号数字而被破坏。有一个论点“但是索引永远不会为负”-当然,但是索引的差异例如为负。

在索引超过2 ^ 31但不超过2 ^ 32的极少数情况下,您不使用无符号整数,而是使用64位整数。

最后,是一个不错的陷阱:在循环“ for(i = 0; i <n; ++ i)a [i] ...”中,如果i为32位无符号,并且内存超过32位地址,则编译器无法优化通过增加指针来访问[i],因为在i = 2 ^ 32-1时,我会回绕。即使n永远不会变大。使用带符号的整数可以避免这种情况。


-5

最后,我在这里找到了一个很好的答案:J.Viega和M.Messier撰写的“安全编程指南”(http://shop.oreilly.com/product/9780596003944.do

带符号整数的安全性问题:

  1. 如果功能需要正参数,则很容易忘记检查下限。
  2. 负整数大小转换产生的不直观的位模式。
  3. 由负整数的右移操作产生的不直观的位模式。

有符号<->无符号转换存在问题,因此不建议使用mix。


1
为什么这是一个好答案?什么是食谱3.5?整数溢出等怎么说?
Baldrickk '18

根据我的实际经验,这是一本非常不错的书,在我尝试过的所有方面都提供了宝贵的建议,并且在这项建议中也很坚定。与之相比,长度大于4G的数组发生整数溢出的危险似乎微不足道。如果必须处理这么大的数组,我的程序将进行很多微调,以避免性能下降。
zzz777 '18

1
这不是关于这本书是否好。您的答案并未提供使用该食谱的任何理由,并且并非每个人都会有该书的副本来查找。看一下如何写一个好的答案的例子
Baldrickk

FYI刚刚了解使用无符号整数的另一个原因是:一,可以很容易地检测过低:youtube.com/...
zzz777
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.