在C语言中声明64位变量

32

我有一个问题。

uint64_t var = 1; // this is 000000...00001 right?

在我的代码中,这个可以工作:

var ^ (1 << 43)

但是它如何知道1应该在64位中呢?我不应该写成这样吗?

var ^ ( (uint64_t) 1 << 43 )

4
我想你的编译器很友好,让你逃过了这个错误。! - Baldrick
@Baldrick 或者这是一个编译器扩展,其中一个溢出的常量计算例如 1<<43 会自动提升为 long long - ratchet freak
5个回答

35

正如您所想,1是普通的带符号int(在您的平台上可能是32位宽度的2的补码算术),43也是如此,因此如果偶然出现1<<43,结果会导致溢出:事实上,如果两个参数都是int类型,则运算符规则指定结果也将是一个int

然而,在C中,带符号整型溢出是未定义行为,因此原则上任何事情都可能发生。在您的情况下,编译器可能会发出代码,在64位寄存器中执行该移位操作,所以通过幸运,它似乎可以工作;为了获得保证正确的结果,您应该使用您写的第二种形式,或者,作为替代方案,使用ull后缀将1指定为unsigned long long字面量(unsigned long long至少保证为64位)。

var ^ ( 1ULL << 43 )

11

我推荐 OP 的方法,将常量 ( (uint64_t) 1 << 43 ) 转换。

对于 OP 的小例子,下面的两种方法很可能表现相同。

uint64_t var = 1; 
// OP solution)
var ^ ( (uint64_t) 1 << 43 )
// Others suggested answer
var ^ ( 1ULL << 43 )        

以上结果具有相同的值,但不同的类型。这两种类型在C语言中的存在可能导致潜在差异:uint64_tunsigned long long以及后续可能发生的情况。

uint64_t具有确切的范围0到264-1。
unsigned long long的范围为0至至少264-1。

如果unsigned long long始终是64位,就像许多现代计算机那样,那么就没有问题了。但是,让我们展望未来,假设这段代码在一个unsigned long long为16字节(0到至少2128-1)的机器上运行。

下面是一个人为制造的例子:第一个^的结果是一个uint64_t,当乘以3时,积仍将是一个uint64_t,执行模264,如果溢出可能发生,则将结果分配给d1。在下一个情况中,^的结果是一个unsigned long long,当乘以3时,积可能大于264,然后将其分配给d2。因此,d1d2具有不同的答案。

double d1, d2;
d1 = 3*(var ^ ( (uint64_t) 1 << 43 ));
d2 = 3*(var ^ ( 1ULL << 43 ));
如果想要使用 unit64_t 进行工作,请保持一致性。不要假设 unit64_tunsigned long long 是相同的。如果你的答案可以是 unsigned long long,那就没问题了。但我的经验告诉我,如果开始使用像 uint64_t 这样的固定大小类型,那么就不想让变长类型捣乱影响计算结果。

你评论中的额外“-1”是否重要?可能应该是“...可能比2的64次方还大-1...”,但我没有修复它。 - Keith Thompson
我在上面两行评论中多打了一个“-1”,是个笔误,已经更正。你对“可能更大”的敏锐观察是正确的,稍后会进行修复,还有一些其他小问题也会一并处理。 - chux - Reinstate Monica

4

一个便携式的获得 unit64_t 常量的方法是使用 stdint.h 中的 UINT64_C 宏:

UINT64_C(1) << 43

很可能UINT64_C(c)被定义为类似于c##ULL的东西。

从C标准来看:

INTN_C(value)应扩展为与类型int_leastN_t相对应的整数常量表达式。宏UINTN_C(value)应扩展为与类型uint_leastN_t相对应的整数常量表达式。例如,如果uint_least64_tunsigned long long int类型的名称,则UINT64_C(0x123)可能会扩展为整数常量0x123ULL


4

var ^ ( 1ULL << 43 ) should do it.


3
您的编译器不知道应该使用64位进行移位操作。但是,在特定版本的编译器、特定配置和特定代码下,两件错误的事情碰巧变成了正确的结果。但是不要抱有侥幸心理。
假设您的平台上的int类型是32位(这是非常可能的),那么在1 << 43中的两个错误是:
1. 如果移位量大于或等于左操作数的类型的宽度,则其行为未定义。这意味着如果x是int或unsigned int类型,则x << 43的行为是未定义的,x << 32或任何其他x << n(其中n≥32)也是如此。例如,1u << 43也会产生未定义的行为。
2. 如果左操作数具有带符号类型,并且操作的结果超出了该类型,则行为未定义。例如,0x12345 << 16的行为未定义,因为左操作数的类型是带符号类型int,但结果值不适合int。另一方面,0x12345u << 16是被明确定义的,其值为0x23450000u。
“未定义行为”意味着编译器可以生成会崩溃或返回错误结果的代码。在这种情况下,恰好得到了所需的结果,这是不被禁止的,但是墨菲定律规定,总有一天生成的代码将无法如您所愿。
为了保证操作在64位类型上进行,您需要确保左操作数是一个64位类型——分配结果的变量的类型并不重要。这与 float x = 1/2 导致 x 包含0而不是0.5 的问题相同:只有操作数的类型才能确定算术运算符的行为。任何一个 (uint64)1 << 43(long long)1 << 43(unsigned long long)1 << 431ll << 431ull << 43 都可以。如果使用有符号类型,则仅在没有溢出时定义行为,因此如果您期望截断溢出,请务必使用无符号类型。即使不应该发生溢出,通常也建议使用无符号类型,因为行为是可重现的——如果使用有符号类型,则仅打印调试目的的值就可能改变行为(因为编译器喜欢利用未定义的行为生成在微观层面上最有效的代码,这可能非常敏感,例如寄存器分配压力)。
由于您的意图是将结果设置为 uint64_t 类型,因此最好使用该类型执行所有计算。因此:
uint64_t var = 1;
… var ^ ((uint64_t)1 << 43) …

有趣的是,0x12345 是一个 有符号的 整数而不是一个 无符号的 整数。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接