关于在C族语言中使用有符号整数

在我自己的代码中使用整数值时,我总是试着考虑签名,问自己整数应该是有符号还是无符号。

当我确定该值永远不需要为负数时,我会使用无符号整数。
而且我不得不说这种情况大多数时间都会发生。

在阅读其他人的代码时,我很少看到无符号整数,即使代表的值不能为负数。

所以我问自己: “这有充分的理由吗,或者人们只是使用签名整数,因为不关心»

我在这里和其他地方搜索了这个主题,我不得不说,当它适用时,我找不到使用无符号整数的充分理由。

我遇到了这些问题: «默认int类型:有符号或无符号?»和« 你应该总是对C中的数字使用’int’,即使它们是非负的吗? »这两个都提供以下示例:

for( unsigned int i = foo.Length() - 1; i >= 0; --i ) {} 

对我来说,这只是糟糕的设计。 当然,它可能会导致无限循环,无符号整数。
但是在循环之前检查foo.Length()是否为0是如此困难?

所以我个人认为这不是一直使用有符号整数的好理由。

有些人也可能会说有符号整数可能很有用,即使对于非负值,也可能提供错误标记,通常为-1

好吧,拥有一个特定值意味着“错误”是件好事。
但是,对于那个具体的价值,像UINT_MAX这样的东西有什么问题呢?

我实际上是在问这个问题,因为它可能会导致一些巨大的问题,通常在使用第三方库时。

在这种情况下,您经常需要处理有符号和无符号值。

大多数情况下,人们只是不关心签名,只是将一个unsigned int分配给signed int ,而不检查范围。

我不得不说我对编译器警告标志有点偏执 ,所以在我的设置中,这样的隐式转换将导致编译器错误。

对于那种东西,我通常使用函数或宏来检查范围,然后使用显式转换分配,如果需要则引发错误。

这对我来说似乎合乎逻辑。

作为最后一个例子,因为我也是Objective-C开发人员(请注意,此问题仅与Objective-C无关):

 - ( NSInteger )tableView: ( UITableView * )tableView numberOfRowsInSection: ( NSInteger )section; 

对于那些不熟悉Objective-C的人, NSInteger是一个有符号整数。
对于特定部分,此方法实际上检索表视图中的行数。

结果永远不会是负值(顺便说一下,作为节号)。

那么为什么要使用有符号整数呢?
我真的不明白。

这只是一个例子,但我总是看到那种东西,包括C,C ++或Objective-C。

所以,我只是想知道人们是否只是不关心那种问题,或者是否最终有一个良好有效的理由不对这种情况使用无符号整数。

期待听到您的答案:)

  • signed返回值可能会产生更多信息(想想错误数字, 0有时是有效答案, -1表示错误,请参阅man read )……这可能与图书馆的开发人员特别相关。

  • 如果你担心使用unsigned而不是signed而获得的额外一点,那么你可能正在使用错误的类型。 (也有种“过早优化”的说法)

  • python,ruby,jscript等语言在没有signedunsigned情况下做得很好。 这可能是一个指标……

对于广泛无符号整数,有一个重量级参数:

过早优化是万恶之源。

我们都至少有一次被无符号整数咬了。 有时像在你的循环中,有时在其他情况下。 无符号整数会给你的程序增加一个危险,即使它很小。 而你正在引入这种危险来改变一点的含义。 一个小的,微小的,微不足道的 – 但是它的符号意义的位。 另一方面,我们在面包和黄油应用中使用的整数通常远低于整数范围,更多的是10 ^ 1而不是10 ^ 7。 因此,在绝大多数不需要的情况下,无符号整数的不同范围。 当它需要时,很可能这个额外的位不会削减它(当31太小,32很少就足够了)并且你无论如何都需要更宽或任意宽的整数。 在这些情况下,实用的方法是只使用有符号整数,并避免偶尔出现下溢错误。 您作为程序员的时间可以更好地利用。

在我自己的代码中使用整数值时,我总是试着考虑签名,问自己整数应该是有符号还是无符号。

当我确定该值永远不需要为负数时,我会使用无符号整数。 而且我不得不说这种情况大多数时间都会发生。

每次声明变量时要仔细考虑哪种类型最合适是非常好的做法! 这意味着你要小心谨慎。 您不仅应该考虑签名,还应该考虑您希望此类型具有的潜在最大值。

不需要使用签名类型的原因与性能无关,但与类型安全无关。 签名类型可能导致许多潜在的,微妙的错误:

  • C中存在的各种forms的隐式促销可能会导致您的类型以意外和可能危险的方式更改签名。 整数提升规则 ,它是通常的算术转换的一部分,赋值时的左值转换 ,例如VA列表使用的默认参数提升 ,等等。

  • 当使用任何forms的按位运算符或类似的硬件相关编程时,有符号类型是危险的,并且很容易导致各种forms的未定义行为。

通过声明未签名的整数,您可以自动跳过上述大量危险。 类似地,通过将​​它们声明为unsigned int或更大,可以消除由整数提升引起的大量危险。

在编写坚固耐用,可移植且安全的代码时,尺寸和符号都很重要。 这就是为什么你应该总是使用stdint.h的类型而不是C的原生的所谓“原始数据类型”的原因。


所以我问自己:“这有充分的理由吗,或者人们只是使用签名整数,因为不关心»?

我并不认为这是因为他们不关心,也不是因为他们懒惰,即使宣称所有内容有时也被称为“草率打字” – 这意味着笨拙地挑选类型而不是意味着懒得打字。

我宁愿相信这是因为他们对我上面提到的各种事情缺乏更深入的了解。 有一些令人恐惧的经验丰富的C程序员不知道C中的隐式类型促销如何工作,也不知道签名类型与某些运算符一起使用时如何导致定义不明确的行为。

这实际上是一个非常频繁的微妙错误来源。 许多程序员发现自己正盯着编译器警告或特殊的bug,他们可以通过添加一个演员来消除它。 但他们不明白为什么,他们只是添加演员并继续前进。


for(unsigned int i = foo.Length() – 1; i> = 0; –i){}

对我来说,这只是糟糕的设计

的确是。

曾几何时,向下计数循环会产生更有效的代码,因为编译器选择添加“分支如果为零”指令而不是“分支如果更大/更小/相等”指令 – 前者更快。 但这是在编译器真的很愚蠢的时候,我不认为这种微观优化会再次相关。

因此,很少有理由进行减计数循环。 无论谁提出这个论点,都可能无法想到这个问题。 该示例可能已被重写为:

 for(unsigned int i=0; i 

这段代码不应该对性能产生任何影响,但是循环本身变得更容易阅读,同时修复了你的例子所带来的bug。

就目前的性能而言,人们应该花时间思考哪种forms的数据访问在数据缓存使用方面是最理想的,而不是其他任何东西。


有些人也可能会说有符号整数可能很有用,即使对于非负值,也可能提供错误标记,通常为-1。

这是一个不好的论点。 良好的API设计使用专用错误类型进行错误报告,例如枚举。

而不是像一些业余爱好者级别的API

 int do_stuff (int a, int b); // returns -1 if a or b were invalid, otherwise the result 

你应该有类似的东西:

 err_t do_stuff (int32_t a, int32_t b, int32_t* result); // returns ERR_A is a is invalid, ERR_B if b is invalid, ERR_XXX if... and so on // the result is stored in [result], which is allocated by the caller // upon errors the contents of [result] remain untouched 

然后,API将始终为此错误类型保留每个函数的返回值。

(是的,许多标准库函数滥用返回类型进行error handling。这是因为它包含了许多古老的函数,这些函数来自于良好的编程实践发明之前的时间,并且由于向后兼容的原因它们被保留了它们的方式因为你在标准库中找到写得不好的函数,所以你不应该自己编写一个同样糟糕的函数。)


总的来说,听起来你知道自己在做什么,并且给了签名一些想法。 这可能意味着知识方面,你实际上已经领先于你所指的那些post和指南的人。

例如,谷歌风格指南是值得怀疑的。 关于许多使用“权威certificate”的其他此类编码标准也可以说类似。 仅仅因为它说谷歌,美国国家航空航天局或Linux内核,无论实际内容的质量如何,人们都盲目地吞下它们。 这些标准中有好的东西,但它们也包含主观意见,推测或明显的错误。

相反,我建议改用真正的专业编码标准,例如MISRA-C 。 它对签名,类型提升和类型大小等内容强制执行了大量的思考和关注,其中不太详细/不太严肃的文档只是跳过它。

还有CERT C ,它不像MISRA那样详细和细致,但至少是一个健全的专业文档(更侧重于桌面/托管开发)。

来自C FAQ :

C FAQ中的第一个问题是我们应该决定使用哪种整数类型?

如果您可能需要较大的值(大于32,767或小于-32,767),请使用long。 否则,如果空间非常重要(即,如果有大型arrays或许多结构),请使用short。 否则,使用int。 如果明确定义的溢出特性很重要而负值不重要,或者如果要在操作位或字节时避免出现符号扩展问题,请使用相应的无符号类型之一。

另一个问题涉及类型转换:

如果操作涉及有符号和无符号整数,则情况稍微复杂一些。 如果无符号操作数较小(可能我们在unsigned int和long int上操作),那么较大的有符号类型可以表示较小的无符号类型的所有值,那么无符号值将转换为较大的有符号类型,结果有更大的签名类型。 否则(即,如果有符号类型不能表示无符号类型的所有值),则两个值都将转换为公共无符号类型,结果具有无符号类型。

你可以在这里找到它。 因此,基本上使用无符号整数,主要用于算术转换可能会使情况复杂化,因为您必须使所有整数无符号,或者冒着使编译器和您自己混淆的风险,但只要您知道自己在做什么,这本身并不是一个风险。 但是,它可能会引入简单的错误。

什么时候使用无符号整数是一件好事? 一种情况是使用按位运算:

<<运算符将其第一个操作数左移第二个操作数给出的位数,在右侧填入新的0位。 类似地,>>运算符将其第一个操作数右移。 如果第一个操作数是无符号的,>>从左边填充0位,但如果第一个操作数是有符号的,如果高位已经是1,则>>可能填充1位。(这样的不确定性是一个原因在使用按位运算符时,使用所有无符号操作数通常是个好主意。)

从这里开始我已经看到了这个地方:

如果最好使用无符号整数来表示从不为负的值,我们就可以在main函数int main(int argc, char* argv[])使用unsigned int int main(int argc, char* argv[]) 。 有一件事是肯定的,argc永远不会消极。

编辑:

正如评论中所提到的, main的签名是由于历史原因,显然它早于unsigned关键字的存在。

无符号整数是过去的工件。 这是从处理器可以更快地执行无符号算术的时间开始。

这是一个被认为是邪恶的过早优化的情况。

实际上,2005年当AMD推出x86_64(或AMD64,它是如何被称为),x86的64位架构时,它们带来了过去的重影:如果有符号整数用作索引而编译器无法certificate它永远不会是负数,必须插入一个32到64位的符号扩展指令 – 因为默认的32到64位扩展是无符号的 (如果你将32位值移入其中,64位寄存器的上半部分会被清除) 。

但我建议不要在任何算术中使用无符号 ,无论是指针算术还是简单的数字。

for( unsigned int i = foo.Length() - 1; i >= 0; --i ) {}

任何最近的编译器都会警告这样的构造, 条件总是为真或类似。 使用带符号的变量可以避免这些陷阱。 而是使用ptrdiff_t

一个问题可能是c ++库,它经常使用size_t的无符号类型,这是必需的,因为在具有某些启动开关的32位系统上有一些非常大的大小(在2 ^ 31和2 ^ 32之间)的罕见极端情况(/ 3GB的窗户)。

还有更多,我认为签名和无符号之间的比较,其中签名值自动提升为无符号,因此在之前是一个小的负数时变成一个巨大的正数。

使用unsigned存在的一个例外是:对于位字段,标志,掩码,这是很常见的。 通常,将这些变量的值解释为幅度根本没有意义,并且读者可以从该类型中推断出该变量将以位来解释。

结果永远不会是负值(顺便说一下,作为节号)。 那么为什么要使用有符号整数呢?

因为您可能希望将返回值与有符号值进行比较,这实际上是负值。 在这种情况下,比较应该返回true ,但是C标准指定在这种情况下签名的get被提升为unsigned而你将获得false 。 我不知道ObjectiveC。