为什么在C++中更喜欢使用有符号变量而不是无符号变量?

73

我希望更好地了解为什么选择 int 而不是 unsigned

个人而言,除非有充分的理由,否则我从未喜欢过带符号的值。例如数组中的项数、字符串的长度或内存块的大小等,这些东西通常不可能为负值。这样的值没有任何可能的含义。在这些情况下,为什么更倾向于 int,因为它在所有这些情况下都是具有误导性的?

我之所以问这个问题是因为Bjarne Stroustrup和Chandler Carruth建议在这里(约在12:30)更倾向于使用int而不是unsigned

我可以理解使用int而不是shortlong的论点 - int是目标机器体系结构最自然的数据宽度。

但是带符号的类型总是让我感到烦恼。在典型的现代CPU体系结构上,带符号的值是否真的更快?是什么使它们更好?


14
为了无法观看1小时视频的读者着想:Stroustrup和Carruth说他们为什么偏爱有符号数? - us2012
12
我更喜欢使用int而不是unsigned,因为:1.它更短(我是认真的!),2.它更通用且更直观(例如,我希望假设1-2等于-1而不是一些晦涩的巨大数字),3.如果我想通过返回一个超出范围的值来表示错误会怎么样? - user529758
6
在11:08:"没有简单的指导可以提供"。 - Robᵩ
3
好的,但是他们至少需要将int上溢定义为有效行为并使二进制补码强制执行。否则,对于许多现在没有办法绕过unsigned int的合理任务来说,int将变得完全无法使用。 - Christian Rau
2
@john 这太荒谬了(即使你承认它在历史上是荒谬的),甚至一开始就很荒谬。C/C++允许接近底层的编程 - 包括能够与总线上的寄存器设备进行互操作,并进行标志位操作等。无符号避免了有符号值的符号传递问题。对我来说,这比被“意外”的下溢更常见。 - Mordachai
显示剩余18条评论
13个回答

40

根据评论请求:我更喜欢使用int而不是unsigned,因为...

  1. 它更短(我是认真的!)

  2. 它更通用和更直观(即我喜欢能够假设1-2是-1而不是某些晦涩的巨大数字)

  3. 如果我想通过返回超出范围的值来表示错误怎么办?

当然有反对意见,但这些是我喜欢将我的整数声明为int而不是unsigned的主要原因。当然,在其他情况下,unsigned是更好的工具,我只是特别回答“为什么有人喜欢默认为有符号”的问题。


9
我认为说“这更短”需要加上“(我是认真的)”这句话很令人难过。 - ChiefTwoPencils
2
@BobbyDigital 的确如此。我们应该更加关注正确性、可读性和风格等方面,而不是过于担心“效率”问题。 - user529758
7
如果 int 数据类型的正数范围对你的需求已经足够,那么 UINT_MAX 就是一个很好的超出范围的值,可以用来表示错误情况。实际上,在代码中可以使用 -1 来达到这个目的,因为当它转换为无符号整型时,会被解释为 UINT_MAX - AnT stands with Russia
5
打字时输入“unsigned”并不会导致腕隧道综合症 ;) - Mordachai
1
@Mordachai 这不是关于可写性,而是关于可读性。 - user529758
显示剩余7条评论

34

让我简述一下这个视频,专家们简明地说了:

Andrei Alexandrescu:

  • 没有简单的指导方针。
  • 在系统编程中,我们需要不同大小和有符号性质的整数。
  • 许多转换和奥妙规则掌握算术(例如对于auto),因此要小心。

Chandler Carruth:

  • 以下是一些简单的指南:
    1. 使用带符号的整数,除非您需要二进制补码算术或位模式
    2. 使用最小的整数即可。
    3. 否则,如果认为可以计数,则使用int,如果超过数量,则使用64位整数。
  • 停止担心并使用工具告诉你何时需要不同类型或大小。

Bjarne Stroustrup:

  • 使用int,直到有理由不使用。
  • 只使用无符号来表示位模式。
  • 永远不要混合有符号和无符号

抛开对符号性的担忧,从专家们的观点中得出我的一个观点:

使用适当的类型,当不确定时使用int,直到你确信。


2
我觉得这个答案很有趣,但是你能详细解释一下“如果你认为你可以计算项目,请使用int”吗?特别是当我们必须与size_t变量进行比较时,这是否与“永远不要混合有符号和无符号”规则相冲突? - Alberto Moriconi
2
他只是在我的原帖中引用了视频中演讲者给出的答案。他们确实会再次回到这个话题上,并且包括Herb Sutter说,在size_t的情况下,标准库“搞错了...对此表示抱歉。” - Mordachai
1
关于“如果你认为你可以计算项目,请使用int”的建议,我们实际上是在与使用诸如(有符号)long之类的类型进行对比,这将不会违反“永远不要混合有符号和无符号”的规则。 - Prashant Kumar

20

有几个原因:

  1. 对于无符号数,进行算术运算总是产生无符号结果。当从可合理导致负结果的整数量中减去某个数值时(例如从货币数量中减去以产生余额或从数组索引中减去以产生元素之间的距离),这可能会导致问题。如果操作数是无符号的,则会得到一个完全定义但几乎肯定毫无意义的结果,并且 result < 0 比较将始终为假(现代编译器将幸运地提醒您)。

  2. unsigned 具有污染与有符号整数混合时的算术的不良属性。因此,如果你加上一个有符号和无符号的数字并问结果是否大于零,你会受到影响,特别是当无符号整型隐藏在 typedef 后面时。


2
#2 曾经咬过我一次。啊啊啊! - user529758
27
如果signed1 - signed2发生溢出,那么它也不安全,因为会导致未定义的行为。 - Ben Voigt
6
除非signed1和/或signed2非常大(超过可表示值的一半),否则有符号的情况不会溢出。相比之下,从一个无符号值中减去任何东西都可能导致它溢出。 - supercat
2
@Mordachai:有很多不创建此异常的原因。这会阻止编译器进行许多有用的优化。请参见http://blog.regehr.org/archives/213 进行讨论。 - Rob Napier
2
@RobNapier for(unsigned i = count; i--; ) //whatever 这是最短的循环结构,而且它对无符号数绝对没有问题 :-) - cmaster - reinstate monica
显示剩余17条评论

18

除了纯社会学原因外,没有理由更喜欢 signed 而不是 unsigned。即有些人认为普通程序员不够能力和/或注意力足以编写关于 unsigned 类型的适当代码。这通常是各种“演讲者”使用的主要论据,而不管那些演讲者多么受尊敬。

实际上,有能力的程序员很快就能开发和/或学习基本的编程惯用语和技能,使他们能够编写关于无符号整数类型的适当代码。

还需要注意的是,有符号和无符号语义之间的根本差异始终存在(以表面上不同的形式)于 C 和 C++ 语言的其他部分中,比如指针算术和迭代器算术。这意味着在一般情况下,程序员并没有真正避免处理与无符号语义特定问题的选项,以及它带来的“问题”。也就是说,无论你想不想,你必须学会使用以其左端突然终止并在此处右端终止(而不是在某个距离处)的范围,即使你坚决避免使用 unsigned 整数。

此外,正如您可能知道的那样,标准库的许多部分已经相当严重地依赖于 unsigned 整数类型。将有符号运算强制混合到其中,而不是学习使用无符号运算,只会导致非常糟糕的代码。

唯一一个想到可以在某些情况下更喜欢 signed 的真正原因是,在混合整数/浮点代码中,signed 整数格式通常直接由 FPU指令集支持,而 unsigned 格式则根本不受支持,这使得编译器必须生成额外的代码以在浮点值和 unsigned 值之间进行转换。在这种代码中,signed 类型可能表现得更好。

但同时,在纯整数代码中,unsigned 类型可能比 signed 类型表现得更好。例如,整数除法通常需要添加额外的纠正代码以满足语言规范的要求。只有在使用负操作数的情况下才需要纠正,因此它会在没有实际使用负操作数的情况下浪费 CPU 循环。

在我的实践中,我尽可能地使用 unsigned,只有在必要时才使用 signed


6
我不同意。这并不是关于能力,而是关于习惯的问题。(就像何时使用类和结构体一样)有很多能力出众的程序员可以完美地告诉你何时可以使用无符号或有符号的值,但由于这些“社会化”的原因,他们仍然选择使用有符号的值。(我认为甚至缩进也是出于这个目的——是的,目的是使代码更易读,但这也是使用 int 的理由)。 - Luchian Grigore
我倾向于同意这个评论,因为当变量的值是无符号的时候,比如在一个只有正值的循环中 for (unsigned int i=0; i < 5; ++i),我会使用 unsigned。我觉得这样会给它增加一些额外的类型说明符,但我也能理解你的观点,仅仅使用 int 可以使代码更加简洁。 - bjackfly
即使你有信心变量永远不会是负数,甚至在上面的循环中也不要使用unsigned。想象一下,如果有人在循环体中添加类似这样的内容:if(i-3<0){/范围中间的某些操作/}。如果i是unsigned,上述代码将永远不会被执行。是的,上面的代码假设“某人”不擅长使用unsigned,但这种情况比人们想象的要经常发生。 - Michael
6
@Michael:并不是真的。这是那些听起来“正确”的伪智慧之一。比如人们用来证明“尤达式”语法的那个。例如,他们说应该写3 == x而不是x == 3,以避免错误地使用赋值而非==。但实际上这是一个根本不存在的问题。使用正常语法x == 3的人根本不会犯这样的错误。对于“无符号整数(unsigned)”,也是同样的情况。有能力的开发者永远不会编写像这样的代码i - 3 < 0,因为表达式i < 3自然、与有无符号无关。 - AnT stands with Russia
1
@AnT:减去零并进行比较可能没有特别有用,但是减去另一个数字并进行比较可能会有用。我认为(uint32_t)(x-y) < z是一种合理的方法来检查y是否在x的某个距离内但不低于它,尽管最好能够以成语化的方式编写它,而不必命名特定类型(虽然0u+x-y < z应该适用于所有情况,其中xy是相同的无符号类型,无论它比int大还是小,但我认为0u+不被认为是一种公认的习惯用法)。 - supercat
1
但实际上,这是一个从未发生过的虚假问题。 - Jean-Michaël Celerier

9
在C语言和许多派生自它的语言中,整数类型有两个常见用途:表示数字或表示抽象代数环的成员。对于不熟悉抽象代数的人来说,环背后的主要概念是将环中的两个项相加、相减或相乘应产生另一个环中的项——它不应崩溃或产生环外的值。在32位计算机上,将无符号0x12345678加上无符号0xFFFFFFFF并不会"溢出"——它只是产生了结果0x12345677,这对于模2^32同余的整数环来说是定义良好的(因为将0x12345678加到0xFFFFFFFF的算术结果,即0x112345677,与0x12345677 mod 2^32同余)。
从概念上讲,这两种用途(表示数字或表示模2^n同余的整数环的成员)都可以由有符号和无符号类型来实现,并且许多操作对这两种用途都是相同的,但也存在一些差异。其中之一是,试图添加两个数字不应期望产生除了正确的算术和之外的任何东西。虽然是否应该要求语言生成必要的代码以保证它不会这样做(例如会抛出异常),这是有争议的,但可以争论的是,对于使用整数类型表示数字的代码来说,这种行为优于产生算术不正确的值,编译器不应禁止这种行为。
C标准的实现者决定使用有符号整数类型表示数字,使用无符号类型表示模2^n同余的整数代数环的成员。相比之下,Java使用有符号整数表示这些环的成员(尽管它们在某些上下文中被解释得不同;例如,不同大小的有符号类型之间的转换与无符号类型之间的转换行为不同),Java既没有无符号整数,也没有任何原始整数类型在所有非异常情况下都表现为数字。
如果一种语言提供了有符号和无符号表示数字和代数环数字的选择,那么使用无符号数字来表示始终为正的量可能是有意义的。然而,如果唯一的无符号类型表示代数环的成员,而表示数字的唯一类型是有符号类型,则即使一个值始终为正,它也应该使用设计用于表示数字的类型来表示。
顺便说一下,(uint32_t)-1等于0xFFFFFFFF的原因是将有符号值强制转换为无符号值相当于添加无符号零,并且将整数添加到无符号值的定义是根据代数环的规则将其大小加或减到/从无符号值中,规则指定如果X=Y-Z,则X是该环中唯一的成员,使得X+Z=Y。在无符号数学中,0xFFFFFFFF是唯一的数字,当它加上1时,产生无符号零。

2
吹毛求疵:字段允许除了加法单位元以外的任何元素进行除法。如果你拥有的只有 +-*,那么代数结构就是一个 - user2005819
@ChrisWhite:谢谢。已更正上述内容。我已经很久没有学过抽象代数了;我最初说的是“群”,但群不支持乘法。 - supercat
@Chris:但是无符号整数类型除了加法单位元之外,确实可以进行除法运算——它只是基于自然算术进行舍入,而不是模等价类。 - Ben Voigt
@BenVoigt 当然,当然。但是这个“除法”不是乘法的倒数,因此不能使集合成为一个域。但这都是语义问题,我想我们都知道我们在谈论什么 :) - user2005819
@Chris:你能想象如果C++在其原始类型之一上实际上进行伽罗瓦域除法会导致多大的混乱吗? - Ben Voigt
@BenVoigt: 除了相对较少的处理器具有高效处理此类事物的指令之外,我认为一门语言包含具有“不寻常”算术行为的原始类型是没有问题的(例如:反向进位加法,伽罗瓦域乘法等)。在不存在运算符或方法重载的情况下,应允许隐式转换,但如果存在不需要隐式转换的重载,则不应该考虑隐式转换。 - supercat

8

现代架构上速度是一样的。使用 unsigned int 的问题在于它有时会产生意外的行为,这可能会导致本来不会出现的错误。

通常情况下,从一个值中减去 1,那么这个值就会变小。然而,在使用 signedunsigned int 变量时,有时减 1 会生成一个远比原值大得多的值。unsigned intint 的关键区别在于,使用 unsigned int 时生成悖论结果的值是一个常用的值——0——而对于 signed 来说,这个数字则远离正常操作。

至于返回 -1 作为错误值,现代思想认为最好抛出异常而不是测试返回值。

如果您正确地保护您的代码,就不会出现这个问题。如果您到处都使用 unsigned,仅进行加法运算而不进行减法运算,并且永远不接近 MAX_INT,那么您就没问题了。我也在所有地方使用 unsigned int。但这需要很多纪律性。对于许多程序,您可以使用 int 并花时间处理其他错误。


14
“unsigned int”的问题在于它有时(在溢出的情况下)会产生意外的行为。”而“signed int”的问题在于它有时(在溢出的情况下)会产生未定义的行为。考虑到这些选择,“unsigned”看起来相当不错 ;) - Ben Voigt
2
当然,对于完全不同的值,溢出会发生,因此对于有符号类型来说,溢出很少是一个问题。 - Ben Voigt
1
@BenVoigt,此外,“意外”只有在不了解隐式转换规则时才会出现(这就是我所说的“反直觉”)。幸运的是,C和C++标准完全明确定义了无符号溢出(至少就我对此的了解而言)。 - user529758
1
@H2CO3:除了无符号类型中我所知道的唯一一种UB情况 - 位移操作数超出范围,我不知道还有其他。 - Ben Voigt
根据我的经验,使用unsigned(有纪律地)会遇到许多选择int的API。因此,我认为这本身就是使用int的一个论点,因为这样你就不必处理各种API中int-unsigned边界的问题,这绝对是个问题(然而,如果我能自己决定,我会告诉API编写者在像计数、大小和索引这样的情况下要更加谨慎地使用unsigned,因为它们不能为负)。 - Mordachai
显示剩余7条评论

8
  1. 默认使用int:它与语言的其余部分更加协调。

    • 最常见的领域用途是正常算术,而不是模数算术。
    • int main() {} //看到了一个unsigned吗?
    • auto i = 0; // i的类型是int
  2. 只有在模数算术和位扭曲时才使用unsigned(特别是移位)

    • 具有不同于常规算术的语义,请确保你想要的是这个
    • 对于签名类型进行位移是微妙的(请参见@ChristianRau的评论)
    • 如果您需要一个32位机器上的> 2Gb向量,请升级您的操作系统/硬件
  3. 永远不要混合有符号和无符号算术

    • 这方面的规则很复杂和令人惊讶(其中任何一个都可以转换为另一个,取决于相对类型大小)
    • 打开-Wconversion -Wsign-conversion -Wsign-promo(gcc在这方面比Clang更好)
    • 标准库在std :: size_t上犯了错误(来自GN13视频的引用)
    • 如果可以,请使用范围for,
    • for(auto i = 0; i < static_cast<int>(v.size()); ++i)如果必须
  4. 除非您确实需要它们,否则不要使用短或大类型

    • 当前体系结构的数据流非常适合32位非指针数据(但请注意@BenVoigt有关较小类型的缓存效果的注释)
    • charshort节省空间但受到积分晋升的影响
    • 你真的会数到所有int64_t吗?

1
最佳的时间性能通常取决于你可以将多少数据放入缓存中...然后小型类型轻松击败32位。 - Ben Voigt
1
"对有符号类型进行位移操作是未定义行为" - 不完全是,但它可能是。 - Christian Rau
@ChristianRau 我忘记读5.8/3关于右移位运算,无论如何,这就是不在有符号类型上使用位移的全部原因:太微妙了。 - TemplateRex
2
你提供了一组指南,但解释很少。特别是你那复杂的 for 循环需要一些解释。(我甚至可以说这是一个糟糕的指南 - 使用 for (auto i = 0u; i < v.size(); ++i) 代替!- 或者,更好的是,for (auto i : indices(x))。) - Konrad Rudolph
@KonradRudolph 我也更喜欢使用range-for循环而不是索引循环。我进行类型转换的原因是为了防止无符号整数传播到我的代码库中(参见第1点)。不幸的是,标准库在size()函数中使用size_t,所以我选择了丑陋的强制类型转换路径,而不是将自己的变量放弃给unsigned的更美观的路径。 - TemplateRex
显示剩余5条评论

7
回答实际问题:对于大多数情况,这并不重要。使用int可以更容易地处理第二个操作数大于第一个操作数的减法,并且仍然可以得到“预期”的结果。
在99.9%的情况下,绝对没有速度差异,因为有符号和无符号数字唯一不同的指令是:
1.使数字变长(用符号或零填充)-做这两件事需要相同的努力。
2.比较-对于有符号数字,处理器必须考虑任一数字是否为负数。但是,使用有符号或无符号数字进行比较的速度相同-只是使用不同的指令代码来表示“具有最高位设置的数字小于具有最高位未设置的数字”(本质上)。[严谨地说,几乎总是使用比较结果的操作不同-最常见的情况是条件跳转或分支指令-但无论如何,付出的努力是相同的,只是输入被认为是略微不同的东西]。
3.乘法和除法。显然,如果是有符号乘法,则需要对结果进行符号转换,而无符号乘法则不应更改结果的符号,如果其中一个输入的最高位设置。同样,付出的努力(就我们所关心的而言)是相同的。
(我认为还有一两种情况,但结果是相同的-无论是有符号还是无符号,执行操作的努力都是相同的)。

正确,高度相关......但并没有回答问题。仍然有用所以+1。 - Ben Voigt

3
int类型更接近于数学上的整数,而不是unsigned类型。
仅仅因为一个情况不需要负数来表示,就偏好使用unsigned类型是幼稚的。
问题在于,unsigned类型在零旁边有不连续的行为。任何试图计算一个小的负值的操作,都会产生一些大的正值(更糟的是:这个值是实现定义的)。即使对于像a = 3b = 4这样的小值,无法在unsigned域中保持代数关系,例如a < b意味着a - b < 0
如果将i设置为unsigned,则像for (i = max - 1; i >= 0; i--)这样的降序循环将无法终止。
无符号的怪癖可能会导致问题,这将影响代码,而不管该代码是否希望仅表示正值。
无符号类型的优点在于:某些在有符号类型的位级上没有可移植定义的操作,在无符号类型上是这样的。无符号类型没有符号位,因此通过符号位进行移位和掩码处理不是问题。无符号类型适用于位掩码以及以平台无关的方式实现精确算术的代码。即使在非补码机器上,无符号操作也会模拟二进制补码语义。编写多精度(bignum)库几乎需要使用无符号类型的数组来进行表示,而不是有符号类型。
对于像标识符一样行事而不是算术类型的数字,无符号类型也是适用的。例如,IPv4地址可以用32位无符号类型表示。您不会将IPv4地址相加。

1
你肯定知道模运算是完全数学的,对吧? - GManNickG
@GManNickG 这就是为什么我说“数学整数”而不是“数学”。在许多常见情况下,模算术是不合适的。 - Kaz
3
注意,虽然“for (i = max - 1; i >= 0; i--)”不会终止,但是“for (i = max - 1; i != -1; i--)”将按预期工作(而且与类型的有符号性无关)。 - AnT stands with Russia
1
@Kaz:你可能是指“自然数”。 - Ben Voigt
@BenVoigt 为什么我要调用自然数 {1, 2, 3, ...};它们在这里几乎没有相关性,并且作为一种类型,它们有缺点,比如不闭合于减法,在这方面它们比模同余更糟糕。 - Kaz
@Kaz:从那个角度来看,它们与C++中的有符号整数类型完全相同。我认为这就是讨论的主题。 - Ben Voigt

2

int是首选,因为它是最常用的。 unsigned通常与位操作相关联。每当我看到一个unsigned时,我就会认为它被用于位运算。

如果你需要更大的范围,请使用64位整数。

如果你正在使用索引迭代内容,类型通常具有size_type,你不应该关心它是有符号还是无符号。

速度不是问题。


2
@ott-- 我不明白。你所说的“设置标志”是什么意思?你是说对于无符号数,你会少设置一位吗?比如说...你只写了31位? - Luchian Grigore
2
@ott:有很多值,可能是大多数,从来不会是负数。因此,您的标志及其设置是不必要的。 - Ben Voigt
2
@ott-- 你有参考资料吗?我仍然看不出使用无符号类型如何节省设置哪些标志或位置。 - Luchian Grigore
2
@ott--:在大多数现代处理器上,有符号和无符号加法的指令不是更或多或少相同吗?此外,CPU 的速度不是由它需要做多少事情决定的,而是由延迟(周期数)和时钟(因此实际上是关键路径的长度)决定的[省略了诸如 OOO 执行或超标量架构之类的细节]。只要它不增加关键路径,它就不应该对速度产生任何影响,并且对功耗消耗可以忽略不计。 - Maciej Piechotka
1
@ott:如果你在谈论由ALU设置的CPU标志,你应该知道在许多体系结构中,这些标志对于有符号和无符号都是设置的。CPU没有太多关于数据类型的概念。 - Ben Voigt
显示剩余7条评论

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