您是不是应该始终对 C 中的数字使用“int”,即使它们是非负数?

Posted

技术标签:

【中文标题】您是不是应该始终对 C 中的数字使用“int”,即使它们是非负数?【英文标题】:Should you always use 'int' for numbers in C, even if they are non-negative?您是否应该始终对 C 中的数字使用“int”,即使它们是非负数? 【发布时间】:2011-03-16 14:56:54 【问题描述】:

我总是将 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,即使它们 不能是负数吗?

【问题讨论】:

出了什么问题:if (bitsAvailable >= optionalDataSize +mandatoryDataSize) ... ? 仅供参考,Java 不支持无符号类型,因此如果您打算让代码与 Java 互操作,则应避免使用这些类型,除非您确实需要特定值的类型范围。我不认为仅出于表明不支持/不允许负值的目的而使用 unsigned 是合适的。 另一个仅供参考:这类错误是好的静态代码分析器会为您找到的错误。 Coverity 会发现像这个一样的问题,没有使用任何其他的足够多的说,但我相信他们中的大多数人都会明白这一点。以下是可用工具列表:en.wikipedia.org/wiki/List_of_tools_for_static_code_analysis 另见:***.com/questions/1951519/when-to-use-stdsize-t 用于 C++,但答案仍然主要适用 @Russell,它也不完美。加法可能会导致溢出并换行 unsigned。 【参考方案1】:

没有提到的一件事是交换有符号/无符号数字会导致安全漏洞。这是一个问题,因为标准 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 将验证&lt;= 512 测试,但会导致memcpy 将超过512 个字节复制到缓冲区 - 这允许攻击者覆盖堆栈上的函数返回地址并且(经过一些工作) 接管你的电脑!

你可能会天真地说,“很明显,长度需要为size_t 或检查为&gt;= 0,我永远不会犯这个错误”。除了,我保证如果你曾经写过任何重要的东西,你有。 Windows、Linux、BSD、Solaris、Firefox、OpenSSL、Safari、MS Paint、Internet Explorer、Google Picasa1、@93 987654332@, Open Office, Subversion, Apache, Python, php, Pidgin, Gimp, ... 一直在继续 ... -这些都是聪明的人,他们的工作是了解安全性。

简而言之,始终使用size_t 作为尺寸。

伙计,programming is hard。

【讨论】:

否,忘记边界检查会导致安全漏洞。如果你在另一个方向上弄错了,unsigned 不会帮助你,你的函数会很高兴地写信给myArray[0xFFFFFFFF] @dan04: 不,根本原因是在你应该使用无符号整数时使用有符号整数,例如size_t (或者,更准确地说,这是隐含的有符号/无符号数之间的转换)。当然,忘记检查边界也是一个问题。我已经更改了示例以使其更清楚 - 谢谢。 这个问题让我很痛苦,现在我只是将所有内容都转换为有符号整数(是的,有符号)。 2 GB 的地址空间不值得麻烦 :) 我仍然不明白为什么省略边界检查length 的下限不是这里的根本问题。当然,您可以使用像size_t 这样的无符号类型,但是您甚至无法检查下限是否为非负数。由于隐式转换规则,这只会​​导致 不同 错误。这是如何改进的? @dan04:不,它不会写。如果使用unsigned int length = 0xFFFFFFFF,则if (length &lt;= 512) 将评估为false【参考方案2】:

我应该一直...

“我应该总是……”的答案几乎肯定是“不”,有很多因素决定您是否应该使用数据类型 - 一致性很重要。

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

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

导致无限循环。

这就是为什么包括Google's C++ Style Guide 在内的一些样式指南不鼓励unsigned 数据类型的原因。

在我个人看来,我没有遇到很多由这些无符号数据类型问题引起的错误——我会说使用断言来检查你的代码并明智地使用它们(在你执行算术时更少)。

【讨论】:

恕我直言,unsigned 有助于在编译阶段而不是运行时捕获错误。序数值(例如数量)应为 unsigned int 而不是 signed int 未检测到的下溢和上溢是 C 系列的基本陷阱——使用有符号与无符号会改变错误情况,但不会消除任何错误情况。当然,在零附近出现错误情况可能是特别坏事,但正如你所说,这取决于你在做什么。在上面的循环中,您可以检查 != ~0 作为您的结束条件 - 这是一个有用的无符号无效/结束值。这是一个轻微的作弊(0 是 int,所以 ~0-1)但在正常的机器上,隐式转换只是工作,并且在视觉上它比没有签名的 -1 更奇怪。 @Thomas :感谢您的反馈,但我不完全确定我是否同意。 c(和 c++)提供了 signedunsigned 类型之间的隐式转换,这可能会产生沉默和令人惊讶的结果。两者之间没有太多可以触发编译失败的语法约束(除非您传递额外的编译器警告标志)。 unsigned 类型的好处主要是语义上的,除非您专门使用无符号类型来避免符号位的操作(例如在位掩码中)。 @Steve314 :是的,当然有办法避免这种情况——但它们不像&gt;=0那样直观易读...这就是它成为“陷阱”的原因:)跨度> Bad Things™ 发生在您使用有符号数字表示尺寸参数时。看我的帖子。【参考方案3】:

您应该使用无符号整数类型的一些情况是:

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

但对于一般算术,问题是,当您说某事“不能为负”时,这并不一定意味着您应该使用无符号类型。因为你可以在一个无符号数中放一个负值,只是当你把它取出来时它会变成一个非常大的值。因此,如果您的意思是禁止使用负值,例如对于基本平方根函数,那么您就是在陈述该函数的前提条件,并且您应该断言。你不能断言不能是的,是;您需要一种方法来保存带外值,以便您可以测试它们(这与 getchar() 背后的逻辑相同,返回 int 而不是 char。)

此外,有符号与无符号的选择也会对性能产生实际影响。看看下面的(人为的)代码:

#include <stdbool.h>

bool 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 编译时,您会得到:

.文件“try.c” 。文本 .p2align 4,,15 .globl foo_i .type foo_i, @function foo_i: 移动 $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 刚毛%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 非常大,(a + 69u) 很可能小于a,因此必须有针对这种情况的代码。另一方面,有符号算术溢出是未定义的,所以 GCC 会继续假设有符号算术不会溢出,所以(a + 69)不能永远不会更少比a。因此,不加选择地选择无符号类型会不必要地影响性能。

【讨论】:

【参考方案4】:

C++ 的创造者 Bjarne Stroustrup 在他的《C++ 编程语言》一书中警告不要使用无符号类型:

无符号整数类型是理想的 用于将存储视为一点的用途 大批。使用无符号而不是 int 多获得一位来表示 正整数几乎从来不是 好主意。试图确保 通过声明一些值是正的 无符号变量通常是 被隐式转换打败 规则。

【讨论】:

然而标准库使用无符号类型来表示容器大小(C++ 程序中的主要错误来源)... @6502 我会使用迭代器与标准容器交互,除了最琐碎或一次性的 sn-ps 之外,几乎所有任务都使用迭代器。 更明确地说:他确实警告一般!他只警告尝试通过使用无符号而不是有符号来扩展值范围!【参考方案5】:

答案是肯定的。 C 和 C++ 的“无符号”int 类型不是“始终为正整数”,无论该类型的名称是什么样的。如果您尝试将类型读取为“非负”,则 C/C++ 无符号整数的行为毫无意义......例如:

两个无符号之差是一个无符号数(如果您将其读作“两个非负数之差是非负数”,则毫无意义) int 和 unsigned int 相加是无符号的 存在从 int 到 unsigned int 的隐式转换(如果您将 unsigned 解读为“非负数”,那么相反的转换才有意义) 如果在有人传递负整数时声明一个接受无符号参数的函数,您只需将其隐式转换为一个巨大的正值;换句话说,使用无符号参数类型并不能帮助您在编译时和运行时发现错误。

确实,无符号数在某些情况下非常有用,因为它们是“整数-模-N”环的元素,其中 N 是 2 的幂。无符号整数在您想使用模 n 算术或用作位掩码时很有用;它们不能用作数量。

不幸的是,在 C 和 C++ 中,无符号数也被用来表示非负数,以便能够使用所有 16 位,而当时能够使用 32k 或 64k 的整数被认为是一个很大的区别。 .我基本上将其归类为历史事故......你不应该试图阅读其中的逻辑,因为没有逻辑。

顺便说一句,我认为这是一个错误……如果 32k 还不够,那么很快 64k 也不够了;在我看来,仅仅因为一个额外的位就滥用模整数是一项太高的成本。当然,如果存在或定义了适当的非负类型,这样做是合理的……但无符号语义将其用作非负是错误的。

有时您可能会发现谁说 unsigned 很好,因为它“记录”了您只需要非负值...但是该文档仅对实际上不知道 unsigned 如何适用于 C 的人具有任何价值或 C++。对我来说,看到用于非负值的无符号类型仅仅意味着编写代码的人不理解那部分的语言。

如果你真的理解并且想要无符号整数的“包装”行为,那么它们就是正确的选择(例如,当我处理字节时,我几乎总是使用“无符号字符”);如果您不打算使用包装行为(并且在您显示的差异的情况下,这种行为对您来说只是一个问题),那么这清楚地表明无符号类型是一个糟糕的选择,您应该坚持使用纯整数。

这是否意味着 C++ std::vector&lt;&gt;::size() 返回类型是一个糟糕的选择?是的……这是一个错误。但是,如果您这么说,请准备好被谁不理解“未签名”名称只是一个名称而被称为坏名称……重要的是行为,那是“模-n”行为(并且没有人们会认为容器大小的“模n”类型是一个明智的选择)。

【讨论】:

-1。呃,我的意思是 +4294967295 :) unsigned 的语义不合逻辑。 @dan04: 无符号整数的问题在于它们被用于两种不同的目的,每一种都可能有一套合理的规则,但是 C 有来自这两个目的的杂乱无章的规则.环绕的数字类型对于某些事情非常有用。例如,在处理 TCP 数据包时,能够说出 tcp-&gt;stuffed - tcp-&gt;acked 并知道有多少字节已填充到缓冲区中但即使序列号已经回绕,也没有得到确认,这是非常有用的。问题是无符号值没有一致的包装语义...... ...因为它们通常用于保存永远不会为负的值,但太大而无法放入相同大小的无符号类型。无符号类型的包装行为并没有被设计成它们,因为它们在早期系统中自然发生并且很有用。 在许多具有 16 位 int 类型的系统上,具有大于 32K 的单个对象是很常见的,但有效处理大于 64K 的对象需要更大的 int 类型. unsigned int 的问题在于,正如您正确指出的那样,它用于服务两个不相交的角色(数字与代数环)。我希望 C 会为高达 2^2^n-1 的自然数添加新的单独类型 [例如65535],自然数高达 2^(2^n-1)-1 [例如32767],代数环 mod 2^2^n [例如65536],在每种情况下都具有更好的语义。【参考方案6】:

我似乎与这里的大多数人意见不一,但我发现 unsigned 类型非常有用,但不是以它们的原始历史形式。

如果您因此坚持类型为您表示的语义,那么应该没有问题:使用size_t(无符号)表示数组索引、数据偏移等。off_t(有符号)表示文件偏移。使用ptrdiff_t(有符号)表示指针的差异。对小的无符号整数使用uint8_t,对有符号整数使用int8_t。并且您避免了至少 80% 的可移植性问题。

如果不能,请不要使用intlongunsignedchar。它们属于历史书籍。 (有时你必须,错误返回,位域,例如)

回到你的例子:

bitsAvailable – mandatoryDataSize &gt;= optionalDataSize

可以很容易地改写为

bitsAvailable &gt;= optionalDataSize + mandatoryDataSize

这并不能避免潜在溢出的问题(assert 是你的朋友),但我认为它会让你更接近你想要测试的内容。

【讨论】:

我喜欢这样:如果您使用无符号类型,最好避免减法。 在 32 位系统上,给定 uint16_t x = 0xFFFF; uint16_t y=x*x; 标准对 y 的值有何看法? @supercat,我看不出你的目标是什么,但规则很简单。 RHS 以 32 位 int 计算。乘法的结果似乎是0xFFFE0001,因此乘法溢出并且行为未定义。这是一个很好的例子,为什么永远不应该使用窄类型进行算术运算。使用size_t 时不会出现此问题。 @JensGustedt:直到最近,大多数嵌入式系统都使用 16 位 int。如果在这样的系统上编写代码以在将重复值写入流后更新 16 位二进制补码或补码校验和,则将两个 uint16_t 相乘将是自然的方法。此外,直到最近,用于 32 位系统的 99.9% 的 C 编译器都可以毫无困难地产生完全相同的计算。虽然有些人会认为将表达式写成1u*x*x 会更好,但我认为需要后一种形式是语言规范中的缺陷。 @JensGustedt:顺便说一句,我刚刚浏览了你关于 C 缺陷的博客。我认为我要说的最重要的事情是 C 缺少的是一种标准方法,程序可以通过它对编译器说“这是我对我的实现的要求;你应该给我我需要的东西,或者拒绝编译”。目前,许多编译器提供命令行开关来控制char 是有符号还是无符号,整数溢出之类的行为是否完全可预测,有些可预测,或否定时间和因果律等,但没有标准方法程序来指定要求。【参考方案7】:
if (bitsAvailable >= optionalDataSize + mandatoryDataSize) 
    // Optional data fits, so add it to the header.

没有错误,只要mandatoryDataSize + optionalDataSize 不能溢出无符号整数类型——这些变量的命名让我相信很可能是这种情况。

【讨论】:

【参考方案8】:

在可移植代码中你不能完全避免无符号类型,因为标准库中的许多 typedef 都是无符号的(最明显的是 size_t),而且许多函数会返回这些类型(例如 std::vector&lt;&gt;::size())。

也就是说,出于您所概述的原因,我通常更喜欢尽可能坚持使用签名类型。这不仅仅是您提出的情况 - 在混合有符号/无符号算术的情况下,有符号参数被悄悄提升为无符号。

【讨论】:

【参考方案9】:

来自 Eric Lipperts 的一篇博客文章中的 cmets(参见 here):

杰弗里·L·惠特利奇

我曾经开发过一个系统,其中 负值毫无意义 参数,所以而不是验证 参数值是 非负数,我认为这将是一个 改用 uint 是个好主意。一世 很快发现,每当我 将这些值用于任何事物(例如 调用 BCL 方法),它们是 转换为有符号整数。这 意味着我必须验证 值没有超过签名 整数范围在顶端,所以我 一无所获。另外,每次 代码被调用,整数是 正在使用(通常从 BCL 收到 函数)必须转换为 单位。没过多久我 将所有这些 uint 改回 ints 并采取了所有不必要的演员 出去。我仍然需要验证 数字不是负数,而是代码 干净多了!

埃里克·利珀特

我自己说得再好不过了。 你几乎从不需要 a 的范围 uint,并且它们不符合 CLS。 表示小尺寸的标准方法 整数与“int”,即使有 里面的值是不是 范围。一个好的经验法则:只使用 "uint" 适用于您所处的情况 与非托管代码互操作 期望 uints,或者 有问题的整数显然被用作 一组位,而不是一个数字。总是 尽量避免在公共接口中使用它。 - 埃里克

【讨论】:

这是关于 C#,而不是 C @BlueRaja:具体的例子是 C# 特有的,但是 cmets 提出的一般观点仍然是正确的。 正如我在帖子中提到的,您应该对需要大小参数的 API 使用无符号数据类型(使用 size_t)。在 .Net 中情况并非如此,缓冲区溢出不是问题。 @BlueRaja:引用明确指出在调用需要 unsigned int 的代码时应该使用 unsigned 数据类型。 我的意思是你应该为你自己的 API 使用无符号数据类型,这需要一个大小参数(在 C 中),不管你调用什么。【参考方案10】:

(bitsAvailable – mandatoryDataSize) 在类型为无符号时产生“意外”结果的情况,bitsAvailable &lt; mandatoryDataSize 是有时使用有符号类型的原因,即使预期数据永远不会为负。

我认为没有硬性规定 - 对于没有理由为负的数据,我通常“默认”使用无符号类型,但随后您必须确保算术包装不会暴露错误。

再一次,如果你使用有符号类型,你有时还是得考虑溢出:

MAX_INT + 1

关键是在对这些类型的错误进行算术运算时必须小心。

【讨论】:

“包装”是无符号整数唯一有趣的特性(对于常规整数,您只有未定义的行为)。如果包装将成为一个问题(或者如果您必须小心避免它),那么这清楚地表明“未签名”是错误的选择。使用无符号并且包装有问题(这是无符号类型的最显着特征)是无稽之谈......当你使用无符号时你想要包装......你应该选择无符号因为包装行为...... @6502:你说得非常好,老实说,我认为我有时会使用无符号类型,而有符号类型可能是更好的选择。但我认为也有例外;例如,在处理文件大小时,您可能需要能够处理全部范围的size_t(甚至是一些更大的无符号类型),但您可能仍需要处理包装错误。【参考方案11】:

不,您应该使用适合您的应用程序的类型。没有黄金法则。有时在小型微控制器上,例如,尽可能使用 8 位或 16 位变量会更快,内存效率更高,因为这通常是本机数据路径的大小,但这是一种非常特殊的情况。我还建议尽可能使用 stdint.h。如果您使用的是 Visual Studio,则可以找到 BSD 许可版本。

【讨论】:

【参考方案12】:

如果有溢出的可能,则在计算过程中将值赋给下一个最高的数据类型,即:

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. 
 

【讨论】:

【参考方案13】:

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

【讨论】:

【参考方案14】:

我不知道它在 c 中是否可行,但在这种情况下,我只会将 X-Y 转换为 int。

【讨论】:

签名溢出的效果(这是您在此解决方案中所依赖的)是 U.B.在标准 C 中。 @Pavel Minaev 我不得不承认我不知道缩写 U.B.【参考方案15】:

如果你的数字应该永远不会小于零,但有可能小于 0,那么一定要使用带符号的整数并散布断言或其他运行时检查。如果您实际使用 32 位(或 64 位或 16 位,取决于您的目标架构)值,其中最高位表示“-”以外的其他值,则应仅使用无符号变量来保存它们。在应该始终为正的数字比为零时更容易检测整数溢出,因此如果您不需要该位,请使用带符号的位。

【讨论】:

【参考方案16】:

假设您需要从 1 数到 50000。您可以使用两字节无符号整数来做到这一点,但不能使用两字节有符号整数(如果空间很重要的话)。

【讨论】:

你为什么不能?您的意思是 2 字节(16 位)值吗? 我不能做的就是数数。固定的。 ;)

以上是关于您是不是应该始终对 C 中的数字使用“int”,即使它们是非负数?的主要内容,如果未能解决你的问题,请参考以下文章

C ++获取int中的每个数字

C中的int总是32位吗?

.zip 文件中的文件是不是始终被压缩?

使用空指针是不是意味着某些内存始终未被使用?

目标c:检查整数/整数/数字

package.json 中的版本是不是应该始终遵循 semver?