你是否总是对C中的数字使用'int',即使它们是非负数?

时间:2010-07-15 19:51:18

标签: c coding-style

我总是使用 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 来表示数字,即使它们是 不能否定?

16 个答案:

答案 0 :(得分:91)

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

你可能天真地说,“很明显,长度需要size_t或检查为>= 0,我永远不会犯这个错误”。除此之外,我保证,如果你曾经写过任何非平凡的东西,你就有。 WindowsLinuxBSDSolarisFirefoxOpenSSLSafari,{{3}的作者也是如此}},MS PaintInternet ExplorerGoogle PicasaOperaFlashOpen OfficeSubversionApachePythonPHPPidgin,... on and on and on ... - 这些都是明亮的 工作知道安全的人。

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

男,Gimp

答案 1 :(得分:25)

  

我应该一直......

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

但是,这是一个非常主观的问题,很容易弄乱无符号的人:

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

导致无限循环。

这就是为什么包括Google's C++ Style Guide在内的某些样式指南会阻止unsigned数据类型。

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

答案 2 :(得分:11)

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

  • 您需要将数据视为纯二进制表示。
  • 你需要用无符号数得到的模运算的语义。
  • 您必须与使用无符号类型的代码(例如,接受/返回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进行编译时,您会得到:

        .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可能小于a,因此必须有代码对于这种情况。另一方面,有符号的算术溢出是未定义的,因此GCC将继续并假设已签名的算术不会溢出,因此(a + 69) 不能小于a。因此,不加选择地选择无符号类型会对性能产生不必要的影响。

答案 3 :(得分:10)

C ++的创建者Bjarne Stroustrup在他的“C ++编程语言”一书中警告过使用无符号类型:

  

无符号整数类型是理想的   用于将存储视为一点的用途   阵列。使用无符号而不是   int再获得一位代表   正整数几乎不是一个整数   好主意。试图确保这一点   通过声明,一些值是积极的   无符号变量通常是   被隐含的转换打败了   规则。

答案 4 :(得分:9)

答案是肯定的。无论类型的名称是什么,C和C ++的“unsigned”int类型不是“始终为正整数”。如果你试图将类型读作“非负”,那么C / C ++无符号整数的行为是没有意义的......例如:

  • 两个无符号的差别是一个无符号数(如果你把它读成“两个非负数之间的差异是非负数”就没有意义了)
  • 添加int和unsigned int是无符号的
  • 从int到unsigned int的隐式转换(如果你将unsigned作为“非负”读取,则相反的转换是有意义的)
  • 如果你声明一个函数接受一个无符号参数,当有人传递一个负数int时,你只需将它隐式转换为一个巨大的正值;换句话说,使用无符号参数类型无法帮助您在编译时和运行时都找不到错误。

实际上,无符号数对某些情况非常有用,因为它们是环“整数 - 模N”的元素,N是2的幂。当您想要使用模数运算或位掩码时,无符号整数非常有用;它们不适用于数量。

不幸的是,在C和C ++中,无符号也被用来表示非负数量,当能够使用32k或64k的小整数被认为是一个很大的区别时,能够使用全部16位。我把它基本上归类为历史事故......你不应该试着读它里面的逻辑,因为没有逻辑。

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

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

如果您真的理解并希望无符号整数的“包装”行为,那么它们是正确的选择(例如,当我处理字节时,我几乎总是使用“unsigned char”);如果你不打算使用包装行为(并且这种行为对于你来说就像你所显示的差异一样是个问题)那么这是一个明确的指示,即无符号类型是一个糟糕的选择而你应该坚持平原。

这是否意味着C ++ std::vector<>::size()返回类型是一个糟糕的选择?是的......这是一个错误。但是,如果你这样说准备好被称为坏名字的人不明白“无符号”名称只是一个名字......重要的是行为,这是一种“模数”行为(并且没有人们会认为容器大小的“modulo-n”类型是明智的选择。

答案 5 :(得分:7)

我似乎与大多数人不同意,但我发现unsigned类型非常有用,但不是 raw 历史形式。

如果您坚持使用类型代表的语义,则应该没有问题:对数组索引,数据偏移等使用size_t(无符号)等。off_t(已签名)文件偏移量。使用ptrdiff_t(已签名)表示指针的差异。对于小的无符号整数,请使用uint8_t;对于已签名的整数,请使用int8_t。并且你避免了至少80%的可移植性问题。

如果不是,请不要使用intlongunsignedchar。它们属于历史书籍。 (有时你必须,错误返回,比特字段,例如)

回到你的榜样:

bitsAvailable – mandatoryDataSize >= optionalDataSize

可以轻松改写为

bitsAvailable >= optionalDataSize + mandatoryDataSize

这不能避免可能出现溢出的问题(assert是你的朋友),但我认为让你更接近想要测试的想法。

答案 6 :(得分:6)

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

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

答案 7 :(得分:6)

您无法完全避免可移植代码中的无符号类型,因为标准库中的许多typedef是无符号的(最值得注意的是size_t),并且许多函数返回这些类型(例如std::vector<>::size())。

尽管如此,由于您概述的原因,我通常更愿意尽可能坚持签名类型。这不仅仅是你提出的情况 - 在混合签名/无符号算术的情况下,signed参数被悄悄地提升为unsigned。

答案 8 :(得分:3)

来自Eric Lipperts博客帖子之一的评论(见here):

Jeffrey L. Whitledge

  

我曾经开发过一个系统   负值没有任何意义   参数,所以而不是验证   参数值是   非负面的,我认为这将是一个   很好的想法,只需使用uint。一世   每当我发现的时候很快发现   将这些值用于任何事物(比如   调用BCL方法),他们已经   转换为有符号整数。这个   意味着我必须验证   价值没有超过签字   最高端的整数范围,所以我   一无所获。还有,每一次   代码被调用,这是一个整体   正在使用(通常从BCL收到   函数)必须转换为   的uint。没过多久我才开始   将所有这些改变回到整数   并采取了所有不必要的铸造   出。我仍然需要验证   数字不是负数,而是代码   更干净!

Eric Lippert

  

我自己不能说得更好。   你几乎从不需要a的范围   uint,它们不符合CLS。   代表小的标准方式   整数是“int”,即使有   是那里的价值   范围。一个好的经验法则:只使用   对于你所处的情况,“uint”   与非托管代码互操作   期待uint,或者在哪里   有问题的整数显然用作   一组位,而不是数字。总是   尽量避免在公共接口中使用它。    - 埃里克

答案 9 :(得分:2)

(bitsAvailable – mandatoryDataSize)在类型未签名时产生'意外'结果的情况,bitsAvailable < mandatoryDataSize是有时签名类型的原因,即使数据预计永远不会是负数。

我认为没有硬性和快速的规则 - 我通常“默认”使用无符号类型的数据,而这些数据没有理由为负数,但是你必须确保算术包装不会暴露错误。

然后,如果您使用签名类型,您仍然必须考虑溢出:

MAX_INT + 1

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

答案 10 :(得分:2)

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

答案 11 :(得分:1)

如果存在溢出的可能性,则在计算期间将值分配给下一个最高数据类型,即:

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. 
} 

答案 12 :(得分:0)

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

答案 13 :(得分:0)

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

答案 14 :(得分:0)

如果你的数字应该永远不会小于零,但是有机会成为&lt; 0,无论如何都要使用有符号整数并在其周围进行断言或其他运行时检查。如果您实际使用32位(或64或16,取决于您的目标体系结构)值,其中最重要的位意味着“ - ”以外的其他值,您应该只使用无符号变量来保存它们。检测整数溢出更容易,其中一个应该总是正数的数字非常负,而不是零时,所以如果你不需要那个位,那就去签名的那个。

答案 15 :(得分:0)

假设您需要从1到50000计数。您可以使用双字节无符号整数,但不能使用双字节有符号整数(如果空间很重要)。