声明变量为无符号的重要性

11

如果您知道一个变量永远不会为负数,将其声明为无符号变量是否很重要?这有助于防止输入到不应该接受负数的函数中的除负数以外的任何其他内容吗?

14个回答

21

在语义上非负的值,使用unsigned声明变量是一个很好的风格和良好的编程实践。

然而,需要注意的是它不能防止您犯错。向无符号整数赋负值是完全合法的,这些值会根据无符号算术规则隐式地转换为无符号形式。一些编译器可能会在这种情况下发出警告,而另一些则会悄悄地处理。

还值得注意的是,使用无符号整数需要掌握一些专门的无符号技巧。例如,与此问题相关的常见例子之一是反向迭代。

for (int i = 99; i >= 0; --i) {
  /* whatever */
}

使用有符号的i时,上述循环看起来很自然,但是它无法直接转换为无符号形式,这意味着

for (unsigned i = 99; i >= 0; --i) {
  /* whatever */
}

这种方法并没有达到预期的效果(实际上是无限循环)。在这种情况下使用正确的技术应该是要么

for (unsigned i = 100; i > 0; ) {
  --i;
  /* whatever */
}
或者
for (unsigned i = 100; i-- > 0; ) {
  /* whatever */
}

这经常被用作反对无符号类型的论据,即所谓的上述无符号版本的循环看起来“不自然”和“难懂”。但实际上,我们在处理的问题是在闭合开区间的左端附近工作的普遍问题。在C和C++中,这个问题以多种不同的方式表现出来(例如使用“滑动指针”技术进行数组的向后迭代,使用迭代器进行标准容器的向后迭代)。也就是说,无论上述无符号循环对你来说可能看起来多么不优雅,都没有办法完全避免它们,即使你从不使用无符号整数类型。因此,最好学习这些技术,并将它们包含到您已经建立的惯用语集中。


2
我专门使用unsigned进行位操作,极少情况下用于更大的范围。我知道如何使用unsigned编码,但我不指望我的所有同事每次都能正确理解它。PS:正确的技巧是使用>0而不是>=0 ;-). - Peter G.
5
还有臭名昭著的“箭头操作符”,即 i --> 0 - Potatoswatter
3
如果您不进行类型转换,那么 v.size()==0v.size()-1 的结果将为 UINT_MAX(或类似高值),导致循环错误 :(. 我认为 std::vector<>::size_type 是无符号的,这是一个糟糕的设计,并且我不信任 C++ 标准库的设计 - 我只需要看看 vector<bool>, foo_facet 和所有其他“臭名昭著”的事情。 :) 这是我认为 Java 做出了好的设计原则的情况之一。 - Johannes Schaub - litb
1
@AndreyT,我想知道为什么这是良好的风格和编程实践。您将获得什么?这个答案展示了如何使用“unsigned”进行一些优秀的技巧,但实际上并没有提出使用“unsigned”的论据。如果您的回答包含使用无符号类型的好处清单,它将会更好。 - Johannes Schaub - litb
2
@AndreyT 这个人有一个很好的总结,与我的观点相符:http://groups.google.com/group/comp.lang.c++.moderated/msg/5bce424269082624 ,特别是自然/模数行为部分。 - Johannes Schaub - litb
显示剩余8条评论

6

它不能防止人们滥用您的接口,但至少他们应该会得到一个警告,除非他们添加了C风格的转换或static_cast来使其消失(在这种情况下,您无法再帮助他们)。

是的,这样做有价值,因为它正确地表达了您所希望的语义。


你不需要使用 C 风格的转换来转换(消除)无符号类型。只需使用简单的静态转换即可。 - Edward Strange

4

一个小细节是它可以减少可能需要的数组边界检查测试量......例如,不必再写:

int idx = [...];
if ((idx >= 0)&&(idx < arrayLength)) printf("array value is %i\n", array[idx]);

您可以直接编写:

unsigned int idx = [...];
if (idx < arrayLength) printf("array value is %i\n", array[idx]);

如果 [...] 返回一个负值,你将在第一种情况下捕获错误。然而,在第二种情况下,你将无法捕获错误,但是你将使用另一个由无符号包装行为产生的“随机”正索引。这更糟糕。 - Johannes Schaub - litb
@Johannes:我没有看到那个。如果idx是负数,它将被更改为一个非常大的无符号数字(32位int超过20亿)。假设arrayLength是一个合理的数字,第二种情况将捕获错误。 - David Thornley
@David 嗯,这是那种思维方式的一个主要问题,不一定是这种特定情况的问题。如果你有一个 len 而没有上限,那该怎么办?应用此答案中所应用的原则意味着您不再进行任何检查,然后对该大长度进行操作。这样不好。 - Johannes Schaub - litb

4
其他答案都很好,但有时会导致混淆。这就是我认为为什么一些语言选择不使用无符号整数类型的原因。
例如,假设您有一个结构来表示屏幕对象,它看起来像这样:
struct T {
    int x;
    int y;
    unsigned int width;
    unsigned int height;
};

这个想法是因为宽度不可能是负数。那么,你使用什么数据类型来存储矩形的右边缘呢?

int right = r.x + r.width; // causes a warning on some compilers with certain flags

当然,它仍然不能保护您免受任何整数溢出。因此,在这种情况下,即使width和height在概念上不能为负,将它们设置为unsigned除了需要一些强制转换以消除有关混合有符号和无符号类型的警告之外,实际上并没有真正的好处。最终,至少对于像这样的情况,最好只将它们全部设置为int,毕竟,您很可能不需要窗口足够大才需要将其设置为unsigned。


2
这个rect例子很好。请注意:在上述情况下,如果r.x是负数,则r.x + r.width将导致完全奇怪的结果:例如,-5 + 4u会得到UINT_MAX - Johannes Schaub - litb
有时候我希望编程语言能够定义无符号的7、15、31等位整数类型;这样的类型将被处理为无符号,但是在这些类型和更大的(即使只多一位)有符号类型之间的操作将会是有符号的。在某些处理器上,使用有符号或无符号值进行某些操作可能会更快。例如,如果一个32位处理器在加载时总是对16位值进行符号扩展,那么“无符号15位”值可以在一条指令中加载;而“无符号16位”值则需要两条指令。编译器可以使用更短的代码来处理无符号15位类型。 - supercat
这是一个很好的例子,其中无符号并不合理,但是完全从一种语言中删除无符号值(如Java)只会导致其他问题。我已经多次遇到在Java程序中出现微妙且难以识别的错误,其中有人编写了一些二进制I/O例程,当他们尝试处理大于0x7F的字节时,失败并表现出奇怪的方式。 - Porculus

4

它有两个作用:

1)为无符号值提供双倍的范围。当“有符号”时,最高位被用作符号位(1表示负数,0表示正数),当“无符号”时,您可以使用该位进行数据。例如,char类型从-128到127,unsigned char类型从0到255。

2)它影响>>运算符的操作,特别是右移负值时的操作。


这并不是真正的符号位。1表示负数,但0表示非负数。 - csj
@csj:如果是零的“真实”符号位,你会分配什么值?声明“0表示正数”是正确的;“0意味着正数”不是他所说的。 - Potatoswatter
符合C++平台的不需要使用二进制补码。在实践中,使用“unsigned”可以使可用值的范围增加一倍。 - strager
@Potatoswatter 我在不必要地追求学究式的精确。当“符号位”为零时,整数本身可能是零,既不是正数也不是负数。 - csj

4
如果您知道一个变量永远不会是负数,那么将其声明为无符号的是否重要?
确实并不重要。一些人(例如Stroustrup和Scott Meyers,参见"Unsigned vs signed - Is Bjarne mistaken?")反对仅因为变量表示无符号量就将其声明为无符号的想法。如果使用unsigned的目的是要表明变量只能存储非负值,则需要进行某种检查。否则,你所得到的只有:
  • 一个类型,它默默隐藏错误,因为它不允许负值暴露
  • 相应有符号类型正范围的两倍
  • 定义的溢出/位移等语义
当然,这并不能阻止人们向您的函数提供负值,并且编译器也无法警告您任何这样的情况(考虑传递基于负运行时的int值)。为什么不在函数中断言呢?
assert((idx >= 0) && "Index must be greater/equal than 0!");

无符号类型也存在许多陷阱。在计算中使用它时要小心,因为它可能会暂时小于零(向下计数循环等),特别是在C和C++语言中自动提升无符号和有符号值之间发生的情况。
// assume idx is unsigned. What if idx is 0 !?
if(idx - 1 > 3) /* do something */;

2
为什么不在函数中进行断言呢?因为编译时优于运行时,并且减少断言是一个好习惯。正如你所指出的那样,使用 unsigned 并不能使 assert 消失,但它使条件更简单:x < max 而不是 0 <= x and x < max(或者,可以用一次断言代替两次)。就我个人而言,这是支持 unsigned 的一个非常有力的论据。 - Konrad Rudolph
2
@Konrad 我的观点是,使用unsigned将使函数无法捕获错误,因为函数中的参数根据定义始终为正数。编译器不能在所有情况下警告您,有时仅在调用方进行unsigned转换以消除警告将无法修复任何错误 - 相反,负值将会静默地环绕。 - Johannes Schaub - litb
因此,请写 assert(idx<N)。这将捕获最初为负数的数字。 - sellibitze
@sellibitze 不是这样的。如果 idx 是无符号的,并且您依赖于环绕,则会认为 UINT_MAX 是负数 -1。因此,您会将一半的 unsigned 范围丢弃以将负值检测为更高一半的正值。这完全是错误的。正确的方法是在它们为负时检测负值。否则,unsigned 对您来说什么都没有,甚至不是正范围的两倍。 - Johannes Schaub - litb
@litb: 如果 N 超过了 UINT_MAX/2,你就捕获不到所有的负数了,没错。但是反过来说,使用有符号整型同样有问题,因为大于 UINT_MAX/2 的数字通常无法用有符号整型表示。;-) 有符号整型 - > 无符号整型的转换在本质上是无损的,因为你可以恢复原始值。我不明白有什么大惊小怪的。我实际上在我的代码中利用了这个回绕技巧。它使我能够将 assert(0<=idx && idx<N) 变成 assert(idx<N)。没有任何伤害。行为完全相同。 - sellibitze
显示剩余5条评论

2
这与“const正确性”具有相同的价值。如果您知道某个值不应更改,请将其声明为const并让编译器帮助您。如果您知道一个变量应始终为非负数,则将其声明为unsigned,编译器将帮助您捕获不一致之处。 (这样做还可以在此上下文中使用unsigned int而不是int来表示两倍大的数字。)

1

它还可以避免您在与其他接口交互时不得不进行无符号转换。例如:

for (int i = 0; i < some_vector.size(); ++i)

这通常会让任何需要在没有警告的情况下编译的人感到非常恼火。


1

当不需要使用有符号值时,使用无符号值可以确保数据类型不表示低于所需下限的值,并增加最大上限。所有原本用于表示负数的位组合都用于表示更大的正数集。


1
它不会阻止负数输入到一个函数中;相反,它将把它们解释为大的正数。如果你知道错误检查的上限,这可能是有用的,但你需要自己进行错误检查。一些编译器会发出警告,但如果你经常使用无符号类型,可能会有太多的警告难以轻松处理。这些警告可以通过强制转换来覆盖,但这比仅使用有符号类型更糟糕。
如果我知道变量不应该是负数,我就不会使用无符号类型,而是如果变量不能为负数。例如,size_t 是一个无符号类型,因为一个数据类型根本不可能有负大小。如果一个值可能是负数但不应该是,更容易通过将其作为带符号类型并使用类似于 i < 0 或 i >= 0 的东西来表示(如果 i 是无符号类型,则无论其值如何,这些条件都将得到 false 和 true)。
如果你关心严格的标准符合性,那么了解无符号算术中的溢出是完全定义的,而在带符号算术中它们是未定义行为可能是有用的。

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