如果你总是用C语言中的“int”作为数字,即使它们是非负数?

我总是使用unsigned int值永远不应该是负值。 但今天我注意到了我的代码中的这种情况:

void CreateRequestHeader( unsigned bitsAvailable, unsigned mandatoryDataSize, unsigned optionalDataSize ) { If ( bitsAvailable – mandatoryDataSize >= optionalDataSize ) { // Optional data fits, so add it to the header. } // BUG! The above includes the optional part even if // mandatoryDataSize > bitsAvailable. } 

我应该开始使用int而不是unsigned int来处理数字,即使它们不能为负数?

我应该总是…

“我应该永远……”的答案几乎肯定是“不”,有很多因素决定了你是否应该使用数据types – 一致性是非常重要的。

但是,这是一个非常主观的问题,搞乱无符号数据真的很容易:

 for (unsigned int i = 10; i >= 0; i--); 

导致无限循环。

这就是为什么一些风格指南,包括谷歌的C ++风格指南,阻止unsigned数据types。

在我个人看来,我没有遇到这些无符号数据types的问题引起的许多错误 – 我会说使用断言来检查你的代码,并明智地使用它们(而当你正在执行算术)。

有一件事没有提到, 交换有符号/无符号数字会导致安全错误 。 这是一个很大的问题,因为标准C库中的许多函数取/返回无符号数(fread,memcpy,malloc等都取size_t参数)

例如,从以下无害的例子(从真实的代码):

 //Copy a user-defined structure into a buffer and process it char* processNext(char* data, short length) { char buffer[512]; if (length <= 512) { memcpy(buffer, data, length); process(buffer); return data + length; } else { return -1; } } 

看起来无害,对吧? 问题在于length被签名,但在传递给memcpy时被转换为无符号。 因此,将长度设置为SHRT_MIN将validation<= 512testing,但会导致memcpy将超过512个字节复制到缓冲区 – 这允许攻击者覆盖堆栈上的函数返回地址,并在( SHRT_MIN工作)接pipe您电脑!

你可能会天真地说: “很明显,长度需要size_t或检查>= 0等于>= 0 ,我永远不会犯这个错误” 。 除此之外,我保证,如果你曾经写过任何不平凡的东西, 所以有Windows , Linux , BSD , Solaris , Firefox , OpenSSL , Safari , MS Paint , Internet Explorer , Google Picasa , Opera , Flash , Open Office , Subversion , Apache , Python , PHP , Pidgin , Gimp等等的作者。 …上 – 并且这些都是聪明的人,他们的工作就是知道安全。

总之, 总是使用size_t的大小。

男人, 编程很难 。

一些你应该使用无符号整数types的情况是:

  • 您需要将数据视为纯二进制表示。
  • 您需要使用无符号数字的模算术语义。
  • 您必须与使用无符号types的代码进行交互(例如接受/返回size_t值的标准库例程。

但是对于一般的算术,事情是,当你说某件事情“不可能是负面的”时,这并不意味着你应该使用无符号types。 因为你可以把一个负值赋给一个无符号的值,所以当你把它拿出来的时候它就会变成一个非常大的值。 所以,如果你的意思是否定的价值是被禁止的,比如对于一个基本的平方根函数,那么你就说明了这个函数的前提条件,你应该断言。 你不能断言什么是不可能的, 您需要一种方法来保存带外值,以便testing它们(这是getchar()返回int而不是char的相同types的逻辑getchar()

此外,签名与未签名的select也会对性能产生实际影响。 看看下面的(人为的)代码:

 #include <stdbool.h> bool foo_i(int a) { return (a + 69) > a; } bool foo_u(unsigned int a) { return (a + 69u) > a; } 

除了参数的types, foo都是一样的。 但是,当用c99 -fomit-frame-pointer -O2 -S编译时,你会得到:

         .file“try.c”
         。文本
         .p2align 4,,15
 .globl foo_i
         .type foo_i,@function
 foo_i:
         movl $ 1,%eax
         RET
         .size foo_i,。-foo_i
         .p2align 4,,15
 .globl foo_u
         .type foo_u,@function
 foo_u:
         movl 4(%esp),%eax
         leal 69(%eax),%edx
         cmpl%eax,%edx
         seta%al
         RET
         .size foo_u,。-foo_u
         .ident“GCC:(Debian 4.4.4-7)4.4.4”
         GNU-stack,“”,@ progbits

你可以看到foo_i()foo_u()更有效率。 这是因为无符号算术溢出由标准定义为“环绕”,所以(a + 69u)很可能小于a如果a很大,因此这种情况下必须有代码。 另一方面,带符号的算术溢出是不确定的,所以GCC将继续,并且假设带符号的算术溢出,所以(a + 69) 不能小于a 。 因此,不加区别地select无符号types可能会不必要地影响性能。

C ++的创build者Bjarne Stroustrup在他的书The C ++编程语言中警告使用无符号types:

无符号整数types非常适合将存储器视为位数组的用途。 使用unsigned而不是int来获得更多的位来表示正整数几乎不是一个好主意。 尝试通过声明variables无符号来确保某些值是正值将通常被隐式转换规则所破坏。

答案是肯定的 C和C ++的“unsigned”inttypes不是“总是正整数”,不pipetypes的名称是什么样子。 如果您尝试将types读为“非负”,那么C / C ++ unsigned int的行为是没有意义的…例如:

  • 两个无符号的差别是一个无符号的数字(如果你把它看作是“两个非负数之间的差别是非负的”)是没有意义的)
  • int和unsigned int的添加是无符号的
  • 有一个从int到unsigned int的隐式转换(如果你读取unsigned为“非负”,这是相反的转换,这是有道理的)
  • 如果你声明一个函数接受一个无符号参数,当有人传递一个负的int时,你可以简单地把它转换成一个巨大的正值; 换句话说,使用无符号参数types不会帮助您在编译时或运行时find错误。

事实上,无符号数对于某些情况是非常有用的,因为它们是环“整数模N”的元素,N是2的幂。 当你想使用模n算术或位掩码时,无符号整数很有用; 他们没有用作数量。

不幸的是,在C和C ++中,unsigned也被用来表示非负数量,以便能够使用所有的16位,当那些小的整数能够使用32k或64k被认为是一个很大的区别。 我把它归类为一个历史性的事故……你不应该试图去读它的逻辑,因为没有逻辑。

顺便说一句,我认为这是一个错误…如果32K是不够的,那么很快64K也是不够的; 滥用模数只是因为在我看来,额外的一点是成本太高支付。 当然,如果存在或者定义了一个适当的非负types的话,那么这样做是合理的……但是无符号语义在使用它的时候是非负的。

有时候你可能会发现谁说unsigned是好的,因为它“logging”你只想要非负值…但是这个文档只对那些实际上不知道C或C ++的未签名工作的人有什么价值。 对于我来说,看到一个用于非负值的无符号types仅仅意味着编写代码的人不理解该部分的语言。

如果你真的理解并希望 unsigned int的“包装”行为,那么他们是正确的select(例如,当我处理字节时,我几乎总是使用“unsigned char”)。 如果你不打算使用包装行为(而且这种行为只会在你显示的差异的情况下成为一个问题),那么这是一个明确的指示,无符号types是一个糟糕的select,你应该坚持简单的整数。

这是否意味着C ++ std::vector<>::size()返回types是不好的select? 是的…这是一个错误。 但是,如果你这么说,那么准备好被称为坏名字谁不明白,“无符号”的名字只是一个名字…它所关心的是行为,这是一个“模n”的行为人们会认为一个容器大小的“模n”型是一个明智的select)。

我似乎与这里的大多数人意见不一致,但我觉得unsignedtypes是相当有用的,但不是以他们原始的历史forms。

如果你坚持一个types为你expression的语义,那么应该没有问题:使用size_t (无符号)数组索引,数据偏移等off_t (有符号)的文件偏移量。 使用ptrdiff_t (有符号)指针的差异。 对于小的无符号整数使用uint8_t对于有符号的整数使用int8_t 。 而且你至less避免了80%的饮用水问题。

不要使用intlongunsignedchar如果你不行的话。 他们属于历史书籍。 (有时你必须,错误返回,位字段,例如)

并回到你的例子:

bitsAvailable – mandatoryDataSize >= optionalDataSize

可以很容易地改写为

bitsAvailable >= optionalDataSize + mandatoryDataSize

这并不能避免潜在的溢出问题( assert是你的朋友),但是让你更接近你想要testing的想法,我想。

 if (bitsAvailable >= optionalDataSize + mandatoryDataSize) { // Optional data fits, so add it to the header. } 

无Bug,只要mandatoryDataSize + optionalDataSize不能溢出无符号整数types – 这些variables的命名使我相信这可能是这种情况。

你不能完全避免可移植代码中的无符号types,因为标准库中的许多typedef是无符号的(最显着的是size_t ),许多函数返回这些types(例如std::vector<>::size() )。

也就是说,我一般都喜欢坚持签名types,因为你列出的原因。 这不仅仅是你提出的情况 – 在混合有符号/无符号算术的情况下,被签名的参数被悄悄地提升为无符号的。

来自Eric Lipperts博客文章(见这里 )的评论:

Jeffrey L. Whitledge

我曾经开发过一个系统,其中负值作为一个参数是没有意义的,所以不是validation参数值是否定的,我认为用uint代替它是个好主意。 我很快发现,无论什么时候我使用这些值(如调用BCL方法),它们都被转换为有符号整数。 这意味着我必须validation值不超过最高端的有符号整数范围,所以我什么都没有得到。 而且,每次调用代码时,所使用的整数(通常从BCL函数中接收)都必须转换为提示。 没过多久,我就把所有这些东西都换成了整数,把所有不必要的东西都扔了出去。 我仍然必须validation数字不是负数,但代码更清洁!

Eric Lippert

我自己不能说得更好。 你几乎不需要uint的范围,他们不符合CLS。 表示一个小整数的标准方法是使用“int”,即使那里有超出范围的值。 一个很好的经验法则:只有在与非托pipe代码进行互操作的情况下才使用“uint”,这些代码需要提示,或者所讨论的整数明确用作一组位,而不是数字。 总是尽量避免在公共界面。 – 埃里克

当types是无符号的时, (bitsAvailable – mandatoryDataSize)产生一个'意外的'结果,而bitsAvailable < mandatoryDataSize是一个有时候使用有符号types的原因,即使数据不会是负数。

我认为没有硬性的规则 – 我通常默认使用无符号types的数据,没有理由是负面的,但是你必须确保算术包装不会暴露错误。

再次,如果您使用签名types,您仍然必须考虑溢出:

 MAX_INT + 1 

关键是你在执行这些types的错误algorithm时要小心。

不,您应该使用适合您的应用程序的types。 没有黄金法则。 有时在小型微控制器上,例如,尽可能使用8位或16位variables,因此速度更快,内存效率更高,因为这通常是本地数据path大小,但这是一个非常特殊的情况。 我也build议尽可能使用stdint.h。 如果你正在使用visual studio,你可以findBSD许可的版本。

如果存在溢出的可能性,则在计算过程中将值分配给下一个最高数据types,即:

 void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) { signed __int64 available = bitsAvailable; signed __int64 mandatory = mandatoryDataSize; signed __int64 optional = optionalDataSize; if ( (mandatory + optional) <= available ) { // Optional data fits, so add it to the header. } } 

否则,只需单独检查值而不是计算:

 void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) { if ( bitsAvailable < mandatoryDataSize ) { return; } bitsAvailable -= mandatoryDataSize; if ( bitsAvailable < optionalDataSize ) { return; } bitsAvailable -= optionalDataSize; // Optional data fits, so add it to the header. } 

您需要查看您对variables执行的操作的结果,以检查是否可以上/下溢 – 在您的情况下,结果可能为负。 在这种情况下,最好使用已签名的等价物。

我不知道它是否可能在c中,但在这种情况下,我只是将XY的东西投给int。

如果你的数字永远不会小于零,但有机会<0,所有的手段都要使用带符号的整数,然后撒布断言或其他运行时检查。 如果你实际上使用32位(或64或16,取决于你的目标体系结构)的值,其中最重要的位表示除了“ – ”以外的其他值,你应该只使用无符号variables来保存它们。 检测整数溢出是比较容易的,因为一个总是正数的数字比零时为负数,所以如果你不需要这个数字,就去签名数字。

假设你需要从1到50000进行计数。你可以用两个字节的无符号整数来实现,但不能用一个两个字节的有符号整数(如果空间很重要的话)。