我总是使用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来表示数字,即使它们不能为负数吗?
有一点没有提到,交换有符号/无符号数字可能会导致安全漏洞.这是一个很大的问题,因为标准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
已签名,但在传递给时会转换为unsigned memcpy
.因此设置长度SHRT_MIN
将验证<= 512
测试,但导致memcpy
复制超过512个字节到缓冲区 - 这允许攻击者覆盖堆栈上的函数返回地址和(经过一些工作)接管您的计算机!
你可能天真地说,"很明显,长度需要被size_t
检查或检查>= 0
,我永远不会犯这个错误".除此之外,我保证,如果你曾经写过任何非平凡的东西,你就有.因此,有作者的Windows,Linux的,BSD,的Solaris,火狐,OpenSSL的,Safari浏览器,MS画图,互联网浏览器,谷歌的Picasa,歌剧,闪存,开放式办公,颠覆,阿帕奇,Python的,PHP,洋泾浜,瘸子,... 在和和 ...... -这些都是光明的人,他们的工作是知道的安全性.
简而言之,始终size_t
用于尺寸.
男人,编程很难.
我应该总是......
"我应该总是......"的答案几乎肯定是"不",有很多因素决定了你是否应该使用数据类型 - 一致性很重要.
但是,这是一个非常主观的问题,它很容易搞乱无符号:
for (unsigned int i = 10; i >= 0; i--);
导致无限循环.
这就是为什么包括谷歌的C++风格指南在内的一些风格指南不鼓励unsigned
数据类型.
在我个人看来,我没有遇到由无符号数据类型的这些问题引起的许多错误 - 我会说使用断言检查你的代码并明智地使用它们(当你执行算术时更少).
您应该使用无符号整数类型的一些情况是:
您需要将数据视为纯二进制表示.
你需要使用无符号数的模运算的语义.
您必须与使用无符号类型的代码(例如,接受/返回size_t
值的标准库例程)进行交互.
但对于一般算术,当你说某些东西"不能为负"时,这并不一定意味着你应该使用无符号类型.因为你可以在无符号中加一个负值,所以当你去掉它时它会变成一个非常大的值.所以,如果你的意思是禁止使用负值,比如基本的平方根函数,那么你就是在说明函数的前提条件,你应该断言.你不能断言不可能的是; 你需要一种方法来支持带外值,因此您可以测试他们(这是同一种逻辑背后的getchar()
返回一个int
而不是char
.)
此外,签名与未签名的选择也会对性能产生实际影响.看看下面的(人为的)代码:
#includebool foo_i(int a) { return (a + 69) > a; } bool foo_u(unsigned int a) { return (a + 69u) > a; }
foo
除了参数类型之外,两者都是相同的.但是,在编译时c99 -fomit-frame-pointer -O2 -S
,您会得到:
.file "try.c" .text .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" .section .note.GNU-stack,"",@progbits
你可以看到它foo_i()
比效率更高foo_u()
.这是因为无标符算术溢出由标准定义为"环绕",因此(a + 69u)
很可能小于a
if a
非常大,因此必须有这种情况的代码.另一方面,有符号算术溢出是未定义的,因此GCC将继续并假设有符号算术不会溢出,因此(a + 69)
不能小于a
.因此,不加选择地选择无符号类型会不必要地影响性能.
C++的创建者Bjarne Stroustrup在他的"C++编程语言"一书中警告过使用无符号类型:
无符号整数类型非常适合将存储视为位数组的用途.使用无符号而不是int来再获得一位来表示正整数几乎不是一个好主意.通过声明无符号变量来确保某些值为正的尝试通常会被隐式转换规则所取代.
答案是肯定的.无论类型的名称是什么,C和C++的"unsigned"int类型不是"始终为正整数".如果你试图将类型读作"非负",那么C/C++无符号整数的行为是没有意义的......例如:
两个无符号的差异是一个无符号数(如果你把它读成"两个非负数之间的差异是非负的",则没有意义)
添加int和unsigned int是无符号的
有一个从int到unsigned int的隐式转换(如果你将unsigned作为"非负"读取,那么它是相反的转换,这是有意义的)
如果你声明一个函数接受一个无符号参数,当有人传递一个负数int时,你只需将其隐式转换为一个巨大的正值; 换句话说,使用unsigned参数类型无法帮助您在编译时和运行时都找不到错误.
实际上,无符号数对于某些情况非常有用,因为它们是环"整数 - 模N"的元素,其中N是2的幂.当您想要使用模数运算或位掩码时,无符号整数非常有用; 它们不适用于数量.
不幸的是,在C和C++中,无符号也被用来表示非负数量,当能够使用32k或64k的小整数被认为是一个很大的区别时,能够使用全部16位.我把它基本上归类为历史事故......你不应该试着读它里面的逻辑,因为没有逻辑.
顺便说一句,在我看来这是一个错误...如果32k还不够,那么很快64k也不够; 因为在我看来一个额外的位,滥用模数整数是一个太高的代价.当然,如果存在或定义了适当的非负类型,那将是合理的......但是无符号语义对于将其用作非负数而言是错误的.
有时您可能会发现谁说无符号是好的,因为它"文档"您只需要非负值...但是该文档仅适用于那些实际上并不知道无符号适用于C或C++的人.对于我来说,看到用于非负值的无符号类型只是意味着编写代码的人不理解该部分的语言.
如果你真的理解并想要无符号整数的"包装"行为,那么它们就是正确的选择(例如,当我处理字节时,我几乎总是使用"unsigned char"); 如果你不打算使用包装行为(并且这种行为对你来说只是一个问题,就像你所显示的差异一样)那么这是一个明确的指示,即无符号类型是一个糟糕的选择而你应该坚持平原.
这是否意味着C++ std::vector<>::size()
返回类型是一个糟糕的选择?是的......这是一个错误.但是,如果你这样说准备好被称为坏名字的人不明白"无符号"名称只是一个名字......重要的是行为,这是一种"模数"行为(并且没有人们会认为容器大小的"modulo-n"类型是明智的选择.
我似乎与这里的大多数人不一致,但我发现unsigned
类型非常有用,但不是原始的历史形式.
如果您因此坚持类型为您表示的语义,则应该没有问题:对文件偏移使用size_t
(无符号)数组索引,数据偏移等off_t
(签名).使用ptrdiff_t
(签名)指针的差异.使用uint8_t
的小型无符号整数,并int8_t
为签署的.并且您避免了至少80%的可移植性问题.
不要使用int
,long
,unsigned
,char
如果你不得.它们属于历史书籍.(有时你必须,错误返回,比特字段,例如)
并回到你的例子:
bitsAvailable – mandatoryDataSize >= optionalDataSize
可以很容易地重写为
bitsAvailable >= optionalDataSize + mandatoryDataSize
这不能避免潜在溢出的问题(assert
是你的朋友),但我认为你会更接近你想要测试的想法.
if (bitsAvailable >= optionalDataSize + mandatoryDataSize) { // Optional data fits, so add it to the header. }
没有错误,只要mandatoryDataSize + optionalDataSize不能溢出无符号整数类型 - 这些变量的命名使我相信这可能就是这种情况.
您无法完全避免可移植代码中的无符号类型,因为标准库中的许多typedef是无符号的(最值得注意的size_t
),并且许多函数返回那些(例如std::vector<>::size()
).
也就是说,由于您概述的原因,我通常更愿意尽可能坚持签名类型.这不仅仅是你提出的情况 - 在混合签名/无符号算术的情况下,signed参数被悄悄地提升为unsigned.