使用位移位计算 C 中有符号的长最大值

Compute signed long max value in C using bit shift

昨天刚开始学习 C,这让我在元旦快要发疯了...尝试使用移位操作打印不同的 int 范围。除了带符号的 long max/min 值外,一切正常。想不通为什么 (1 << 63) - 1 returns -1?但是 (1 << 64) -1 对于 unsigned long long 工作正常...

#include <limits.h>
#include <stdio.h>

void print_range() {
    signed char scmax = (1 << 7) - 1;
    char c = scmax; // char means signed char!
    unsigned char uscmax = (1 << 8) - 1;
    char cmin = -(1 << 7);
    unsigned char ucmin = 0;
    printf("signed char max: %d = %d, unsigned char max: %d = %d\n", scmax, SCHAR_MAX, uscmax, UCHAR_MAX);
    printf("signed char min: %d = %d, unsigned char min: %d\n", cmin, CHAR_MIN, ucmin);

    // signed int
    int imax = (1 << 31) - 1; //(2 << 30) - 1;
    unsigned int uimax = (1 << 32) - 1;
    int imin = -(1 << 31);
    //NOTE: %d is for signed char/short/int, %u is for the unsigned formatter.
    printf("signed int max: %d = %d, unsigned int max: %u = %u\n", imax, INT_MAX, uimax, UINT_MAX);
    printf("signed int min: %d = %d, unsigned int min = %d\n", imin, INT_MIN, 0);

    long long lmax = (1 << 63) - 1L; // WHY DOES THIS NOT WORK???
    unsigned long long ulmax = (1 << 64) - 1;
    long long lmin = -(1 << 63); // NEITHER DOES THIS???
    printf("signed long max: %lld = %lld, unsigned long max: %llu = %llu\n", lmax, LLONG_MAX, ulmax, ULLONG_MAX);
    printf("signed long min: %lld = %lld, unsigned long min: %d\n", lmin, LLONG_MIN, 0);
}

记住,编程语言是规范,在一些技术报告中用英文写成。它不是一个软件。对于 C11, see n1570

1 这样的文字常量不是 long,而是 int

要编写文字常量 long 1,您需要编写 1L(或者您可以编写 (long)1...)。要编写文字常量 unsigned long long 1,您应该编写 1ULL(或代码 (unsigned long long)1,这是一个常量表达式)。

文字常量符合 "smallest" 整数类型,大到足以表示它。所以 1 是一个 int,在 64 位计算机上(实际上,C 的实现就像我的 Linux/x86-64 一样)10000000000(即 1010 ) 是一个 long(因为它不适合 int),因为在这样的计算机上 int-s 有 32 位而 long-s有 64 位。

请注意,int 的大小或范围并未由 C99 或 C11 标准精确定义,并且可能因一种实现而异。您可能希望包含 <stdint.h> standard header 并使用 int32_t ...

等类型

所以 1 << 63 是一个 (int)1 左移 63 位(因为左操作数是一个 int 移位在 int-s 上运行)。在我的 Linux/x86-64 上,一个 int 只有 32 位,所以这个操作是一个 undefined behavior.

您应该非常害怕未定义的行为,请参阅 this answer 中的参考资料。可悲的是,偶尔,未定义的行为可能会像您希望的那样工作(但它仍然是 UB)。

查看代码的另一种方式是关注 software portability


顺便说一句,养成使用所有警告和调试信息进行编译的习惯,例如如果使用 GCC,则使用 gcc -Wall -Wextra -g。有时,编译器会很聪明地在这种情况下向您发出警告。然后,改进您的代码以删除所有警告。稍后,使用调试器 (gdb) 逐步 运行 您的代码并了解(通过在调试器中查询程序的状态)发生了什么。


在你的代码中你有:

     char c = scmax; // char means signed char!

遗憾的是,这要棘手得多。你有几种 C 的变体或方言。在某些变体中,char 是有符号的,而在其他变体中,char 是无符号的。您拥有的变体是特定于实现的(编译器作者选择最容易在某些目标体系结构上实现的变体)。对于 GCC,请参阅 C 方言选项 ,例如 -fsigned-char-funsigned-char(您几乎不应该使用它们,并且当您使用它们时,非常注意后果;您可能需要重新编译整个 C 标准库)。

表达式被计算为 int,因为两个操作数都是 int。你需要让他们 long longs:

((1LL << 63) - 1)
(((long long)1 << 63) -1)

此外,许多体系结构将最多移动类型 -1 的大小,因此对于 63 仅移动 31 位,对于 32 或 64 移动 0。

(1<<64)-1) 的工作方式与预期不同:(1<<64)0,原因如前一段所述。 0-1-1 转换为 long long,它仍然是 -1LL 并转换为 unsigned long long 它导致最大 unsigned long long (由于 2 -常见架构中 signedunsigned 数字的补充表示)

您的代码在多个方面调用了未定义的行为:

  • 左移 1,一个 int 值,比类型中的位数减一会调用未定义的行为;
  • 将有符号整数左移任意数量,使得结果值超出类型的范围会调用未定义的行为,就像所有其他有符号算术溢出一样。

用移位计算这些最大值很不方便。如果您可以假设 2s 补码并且没有填充位,则可以使用按位补码获得最大无符号值并将其右移一次以获得最大有符号值,然后取反并减一以获得最小有符号值。

这是更正后的代码:

#include <limits.h>
#include <stdio.h>

int main(void) {
    // char
    unsigned char ucmin = 0;
    unsigned char ucmax = ~ucmin;
    signed char scmax = ucmax >> 1;
    signed char scmin = -scmax - 1;
    char cmax = ((char)(-1)) < 0 ? scmax : ucmax;
    char cmin = ((char)(-1)) < 0 ? scmin : ucmin;
    printf("signed char min: %d = %d, signed char max: %d = %d\n",
           scmin, SCHAR_MIN, scmax, SCHAR_MAX);
    printf("unsigned char min: %d, unsigned char max: %u = %u\n",
           ucmin, ucmax, UCHAR_MAX);
    printf("char min: %d = %d, char max: %d = %d\n",
           cmin, CHAR_MIN, cmax, CHAR_MAX);

    // short
    unsigned short usmin = 0;
    unsigned short usmax = ~usmin;
    signed short smax = usmax >> 1;
    signed short smin = -smax - 1;
    printf("short min: %d = %d, short max: %d = %d\n",
           smin, SHRT_MIN, smax, SHRT_MAX);
    printf("unsigned short min: %d, unsigned sort max: %u = %u\n",
           usmin, usmax, USHRT_MAX);

    // int
    unsigned int umin = 0;
    unsigned int umax = ~umin;
    signed int imax = umax >> 1;
    signed int imin = -imax - 1;
    printf("int min: %d = %d, int max: %d = %d\n",
           imin, INT_MIN, imax, INT_MAX);
    printf("unsigned int min: %u, unsigned int max: %u = %u\n",
           umin, umax, UINT_MAX);

    // long int
    unsigned long ulmin = 0;
    unsigned long ulmax = ~ulmin;
    signed long lmax = ulmax >> 1;
    signed long lmin = -lmax - 1;
    printf("long int min: %ld = %ld, long int max: %ld = %ld\n",
           lmin, LONG_MIN, lmax, LONG_MAX);
    printf("unsigned long int min: %lu, unsigned long int max: %lu = %lu\n",
           ulmin, ulmax, ULONG_MAX);

    // long long int
    unsigned long long ullmin = 0;
    unsigned long long ullmax = ~ullmin;
    signed long long llmax = ullmax >> 1;
    signed long long llmin = -llmax - 1;
    printf("long long int min: %lld = %lld, long long int max: %lld = %lld\n",
           llmin, LLONG_MIN, llmax, LLONG_MAX);
    printf("unsigned long long int min: %llu, unsigned long long int max: %llu = %llu\n",
           ullmin, ullmax, ULLONG_MAX);

    return 0;
}

这一行:

long long lmax = (1 << 63) - 1L; // WHY DOES THIS NOT WORK???

不起作用,因为数字是 int,除非另有声明。

AN int 是(通常)32 位 (您的编译器应该通过以下消息告诉您:

warning: integer overflow in exression [-Woverflow]

与您遇到问题的其他表达方式类似。

你的编译器没有告诉你这个问题意味着你的编译没有启用警告。

对于 gcc,至少使用:

-Wall -Wextra -pedantic  

我还发现这些参数非常有用:

-Wconversion -std=gnu99

(1LL << 63) - 1LL 应该可以工作,但如果您使用检测整数溢出的标志(如 -fsanitize=address -fsanitize=undefined)进行编译,则会出现错误。在这种情况下,最好这样做:

((1LL << 62) - 1) | (1LL << 62)