为什么在 C + + 中更喜欢签名而不是未签名?

我想更好地理解为什么选择 int而不是 unsigned

就我个人而言,我从来不喜欢有符号的值,除非有合理的理由。例如数组中的项的计数、字符串的长度或内存块的大小等,所以这些东西通常不可能是负数。这样的价值没有任何可能的意义。既然 int在所有这些情况下都具有误导性,为什么还要选择它呢?

我之所以这样问,是因为比雅尼·斯特劳斯特鲁普和钱德勒•卡鲁斯都建议人们更喜欢 int而不是 unsigned

我可以看到在 shortlong上使用 int的参数-int是目标机器体系结构的“最自然”数据宽度。

但是签字转让不签字一直让我很恼火。在典型的现代 CPU 体系结构中,有符号值真的更快吗?是什么让他们变得更好?

8619 次浏览

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

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

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

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

当然也有反参数,但这些是我喜欢将整数声明为 int而不是 unsigned的主要原因。当然,这并不总是正确的,在其他情况下,一个 unsigned只是一个更好的工具为一个任务,我只是回答“为什么会有人愿意默认签名”的问题具体。

有几个原因:

  1. unsigned上的算术总是产生无符号,当减去可以合理地产生负结果的整数量时,这可能是一个问题ーー考虑减去货币量来产生余额,或者数组指数来产生元素之间的距离。如果操作数是无符号的,那么您将得到一个完全定义但几乎肯定没有意义的结果,并且 result < 0比较将始终是假的(幸运的是,现代编译器会提醒您这一点)。

  2. unsigned具有污染算法的讨厌特性,因为它会与有符号整数混合在一起。因此,如果您添加一个有符号和无符号,并询问结果是否大于零,您可能会受到影响,特别是当无符号整数类型隐藏在 typedef之后时。

现代建筑的速度是一样的。unsigned int的问题在于它有时会产生意外的行为。这可能会产生不会在其他情况下显示出来的错误。

通常当你从一个值中减去1时,值会变小。现在,对于 signedunsigned int变量,有一段时间减去1会产生一个大得多的值。unsigned intint之间的关键区别在于,对于 unsigned int,产生矛盾结果的值是一个常用的值——-0——-,而对于带有符号的数字,它远离正常操作。

至于为错误值返回 -1——现代思想认为,抛出异常比测试返回值更好。

的确,如果你正确地保护你的代码,你就不会有这个问题,如果你在任何地方都严格地使用无符号,你就不会有问题(假设你只是在加,而不是在减,并且你从来没有接近过 MAX _ INT)。我在任何地方都使用无符号整型。但这需要很多纪律。对于很多程序来说,你可以使用 int并花时间在其他 bug 上。

int是首选的,因为它是最常用的。unsigned通常与位操作相关联。每当我看到一个 unsigned,我认为它是用于位旋转。

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

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

速度不是问题。

回答实际的问题: 对于大量的事情来说,这并不重要。int可以比较容易处理像第二个操作数比第一个操作数大的减法这样的事情,而且你仍然可以得到一个“预期的”结果。

在99.9% 的情况下,速度完全没有差别,因为有符号和无符号数字的唯一不同指令是:

  1. 让数字变长(用符号表示有符号,用零表示无符号)——两者都需要同样的努力。
  2. 比较-一个有符号的数字,处理器必须考虑任何数字是否为负数。但是,与有符号或无符号数字进行比较的速度是相同的——只是使用了不同的指令代码来表示“设置最高位的数字比未设置最高位的数字小”(本质上)。[学术上讲,几乎总是使用不同比较结果的操作——最常见的情况是条件跳转或分支指令——但无论哪种情况,都是相同的努力,只是输入被认为意味着略有不同的东西]。
  3. 乘法和除法。显然,如果结果是有符号的乘法运算,则需要进行符号转换,如果设置了一个输入的最高位,则无符号运算不应更改结果的符号。同样,这种努力是(尽可能接近我们所关心的)相同的。

(我认为还有一两种情况,但结果是一样的——签名或未签名真的无关紧要,执行操作的努力对两者都是一样的)。

我能想到的一个很好的理由是,以防检测到溢出。

对于数组中的项计数、字符串长度或内存块大小等用例,可能会溢出一个无符号 int,即使查看变量也可能没有发现差异。如果它是一个有符号的整型数,那么这个变量将小于零,并且显然是错误的。

当您想要使用该变量时,您可以简单地检查它是否为零。这样,您就不必像无符号整数那样在每次算术运算之后检查溢出。

除了纯粹的社会学原因,没有理由更喜欢 signed而不是 unsigned,也就是说,有些人认为一般的程序员没有能力和/或足够的专注来编写适合 unsigned类型的代码。这通常是各种“演讲者”使用的主要理由,无论这些演讲者可能多么受人尊敬。

实际上,胜任的程序员可以快速开发和/或学习基本的编程习惯用法和技能,这些习惯用法和技能允许他们使用无符号整数类型编写正确的代码。

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

此外,您可能已经知道,标准库的许多部分已经非常依赖于 unsigned整数类型。将有符号算术强加到混合中,而不是学习如何使用无符号算术,只会导致灾难性的错误代码。

在某些情况下,真的选择 signed的唯一原因是,在整数/浮点数混合编码中,整数格式通常直接受 FPU 指令集的支持,而 unsigned格式根本不受支持,这使得编译器为浮点数值和 unsigned值之间的转换生成额外的代码。在这样的代码中,signed类型可能性能更好。

但同时在纯整数代码中,unsigned类型的性能可能优于 signed类型。例如,整数除法通常需要额外的纠正代码来满足语言规范的要求。只有在负操作数的情况下才需要进行校正,因此在没有真正使用负操作数的情况下会浪费 CPU 周期。

在我的实践中,只要有可能,我都会坚持使用 unsigned,只有在必要的时候才会使用 signed

unsigned类型相比,int类型更接近于数学整数的行为。

仅仅因为某种情况不需要表示负值,就选择 unsigned类型是幼稚的。

问题是 unsigned类型的行为几乎为零。任何试图计算小的负值的运算,都会产生一些大的正值。(更糟糕的是: 它是实现定义的。)

a < b这样的代数关系意味着 a - b < 0在无符号域中被破坏,即使对于像 a = 3b = 4这样的小值也是如此。

如果 i被设置为无符号,那么像 for (i = max - 1; i >= 0; i--)这样的降序循环将无法终止。

无符号异常可能导致一个问题,这个问题会影响代码,而不管该代码是否期望只表示正数。

无符号类型的优点是,对于无符号类型,某些操作是在位级别上不可移植地定义的。无符号类型缺少符号位,因此通过符号位进行移位和屏蔽不成问题。无符号类型适用于位掩码,也适用于以独立于平台的方式实现精确算法的代码。无符号操作将模拟二者的补语语义,即使在非二者的补语机器上也是如此。编写多精度(bignum)库实际上需要将无符号类型的数组用于表示,而不是有符号类型。

无符号类型也适用于数字的行为类似于标识符而不是算术类型的情况。例如,IPv4地址可以用32位无符号类型表示。你不会把 IPv4地址加在一起。

C 语言中的整数类型和许多从它派生出来的语言有两种常见的用法: 表示数字,或表示抽象代数环的成员。对于那些不熟悉抽象代数的人来说,一个戒指背后的基本概念是,将一个戒指的两个项目相加、相减或相乘,应该得到另一个戒指的项目——它不应该在戒指之外崩溃或产生价值。在32位机器上,将 unsigned0x12345678添加到 unsigned0xFFFFFFFF 并不会“溢出”——它只会产生结果0x12345677,这是为整数环同余 mod 2 ^ 32定义的(因为将0x12345678添加到0xFFFFFF 的算术结果,即0x112345677,与0x12345677 mod 2 ^ 32一致)。

从概念上讲,两种用途(表示数字,或表示整数环同余模2 ^ n 的成员)可以同时用于有符号和无符号类型,并且对于这两种用法,许多操作是相同的,但是存在一些差异。除此之外,试图将两个数相加,除了得到正确的算术和之外,不应期望得到任何其他结果。虽然是否应该要求一种语言生成必要的代码来保证它不会(例如,异常将被抛出)是有争议的,但有人可能会说,用于使用整数类型表示数字的代码这样的行为比产生一个算术上不正确的值更可取,编译器不应该被禁止这样的行为。

C 标准的实施者决定使用有符号整数类型来表示数字,使用无符号类型来表示整数同余 mod 2 ^ n 的代数环的成员。相比之下,Java 使用有符号整数来表示这些环的成员(尽管它们在某些上下文中的解释是不同的,例如,不同大小的有符号类型之间的转换,表现不同于无符号类型) ,而且 Java 既没有无符号整数,也没有任何在所有非例外情况下表现为数字的不定积分类型。

如果一种语言为数字和代数环数提供了有符号和无符号表示的选择,那么使用无符号数来表示永远为正的数可能是有意义的。但是,如果只有无符号类型表示代数环的成员,而表示数字的类型只有有符号类型,那么即使一个值总是正的,它也应该使用设计来表示数字的类型来表示。

顺便说一下,(uint32 _ t)-1为0xFFFFFFFF 的原因是,将一个有符号的值转换为无符号的值等价于加上无符号的零,而将一个整数加上无符号的值被定义为根据代数环的规则在无符号值之间加或减它的大小,该规则规定如果 X = Y-Z,那么 X 就是该环中唯一的成员,例如 X + Z = Y。在无符号数学中,0xFFFFFF 是唯一一个当加到无符号1时产生无符号零的数字。

让我解释一下这个视频,正如专家们简洁地说的那样。

Andrei Alexandrescu

  • 没有简单的指导方针。
  • 在系统编程中,我们需要不同大小和有符号的整数。
  • 许多转换和神秘的规则支配算术(如 auto) ,因此我们需要小心。

钱德勒 · 卡鲁斯(Chandler Carruth) :

  • 以下是一些简单的指导方针:
    1. 使用有符号整数,除非你需要二进制补数算术或位模式
    2. 使用足够的最小整数。
    3. 否则,如果您认为可以计数数据项,则使用 int; 如果数量超过您想要的数量,则使用64位整数。
  • 停止担心,使用工具来告诉你什么时候需要不同的类型或尺寸。

比雅尼·斯特劳斯特鲁普

  • 使用 int,直到有理由不这样做为止。
  • 只对位模式使用无符号。
  • 不要把签名和未签名混在一起

除了对符号性规则的谨慎之外,我的一句话从专家那里得到了启示:

使用适当的类型,当您不知道时,使用 int,直到您确实知道。

在进行简单的算术运算时,它会产生意想不到的结果:

unsigned int i;
i = 1 - 2;
//i is now 4294967295 on a 64bit machine

在进行简单的比较时,它给出了意想不到的结果:

unsigned int j = 1;
std::cout << (j>-1) << std::endl;
//output 0 as false but 1 is greater than -1

这是因为在执行上面的操作时,有符号整型被转换为无符号整型,它会溢出并转换为一个非常大的数字。

  1. 默认情况下使用 int : 它与语言的其他部分相处得更好

    • 最常见的域名使用是常规算术,而不是同余关系
    • int main() {} // see an unsigned?
    • auto i = 0; // i is of type int
  2. 只使用 unsigned进行模运算和位调整 (特别是移位)

    • 与常规算术有不同的语义,确保它是你想要的
    • 位移符号类型是微妙的(参见@ChristianRau 的评论)
    • 如果您需要在32位机器上使用 > 2Gb 的矢量,请升级您的 OS/硬件
  3. 永远不要混合有符号和无符号算术

    • 这方面的规则是 很复杂,并且令人惊讶(根据相对类型大小,两者都可以转换为另一种)
    • 打开 -Wconversion -Wsign-conversion -Wsign-promo(这里的 gcc 比 Clang 好)
    • 标准图书馆在使用 std::size_t时出了错(引自 GN13视频)
    • 如果可以,请使用 range-for,
    • 如果必须的话,for(auto i = 0; i < static_cast<int>(v.size()); ++i)
  4. 除非实际需要,否则不要使用短型或大型类型

    • 当前的体系结构数据流很好地迎合了32位非指针数据(但请注意@BenVoigt 关于较小类型的缓存效果的评论)
    • charshort节省空间,但遭受整体促销
    • 你真的要数到 int64_t吗?

For me, in addition to all the integers in the range of 0..+2,147,483,647 contained within the set of signed and unsigned integers on 32 bit architectures, there is a higher probability that I will need to use -1 (or smaller) than need to use +2,147,483,648 (or larger).