如果你总是用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<= 512
testing,但会导致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)。
我似乎与这里的大多数人意见不一致,但我觉得unsigned
types是相当有用的,但不是以他们原始的历史forms。
如果你坚持一个types为你expression的语义,那么应该没有问题:使用size_t
(无符号)数组索引,数据偏移等off_t
(有符号)的文件偏移量。 使用ptrdiff_t
(有符号)指针的差异。 对于小的无符号整数使用uint8_t
对于有符号的整数使用int8_t
。 而且你至less避免了80%的饮用水问题。
不要使用int
, long
, unsigned
, char
如果你不行的话。 他们属于历史书籍。 (有时你必须,错误返回,位字段,例如)
并回到你的例子:
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进行计数。你可以用两个字节的无符号整数来实现,但不能用一个两个字节的有符号整数(如果空间很重要的话)。