为什么我们需要带上类型后缀?U/UL/ULL/L/LL

类型后缀

给变量赋值的时候分两个阶段。
第一步,如果没有指定字面量的类型,C99会找到一个能容纳字面量值的最小的带符号类型(int / long int / long long int)。
第二步,转换成左边变量类型并赋值。

long i = 0xffff; //   0xffffffffffff
long j = 0xffffU; //  0x00000000ffff
long k = 0xffffUL; // 0x00000000ffff

在下面几种场景里,我们需要显式指定字面量的类型来避免问题。

  • 未定义的行为
printf("%lld", 1LL); // correct, because 1LL has type long long
printf("%lld", 1);   // undefined behavior, because 1 has type int
  • 防止溢出
    在这里插入图片描述
long x = 10000000L * 4096L;
  • 移位操作
unsigned long long y = 1ULL << 36;

如前所述,赋值操作在后,数值计算在前,因此只把等号左边的变量声明成较长的数据类型是不足矣防止计算溢出的。

在这里插入图片描述
可以看到只有正确类型的类型声明,才能避免移位结果溢出。
在这里插入图片描述

  • 数值比较
    在数值比较时,我们应当指定数据类型。
int x = -1;
printf("%d\n", x < 12); // prints 1 because it's true that -1 < 12

int x = -1;
printf("%d\n", x < 12U); // prints 0 because (unsigned int)-1 is large

编译过程中编译器会给出大量的告警提示信息来帮助我们避免一些未定义的行为和后期难以发现的 bug。

我们要对这些行为做到心中有数,因为有些潜藏的错误编译器无法识别出来。我们来看下面这个错误,预期高32bits全是0,实际上生成的代码做了符号扩展,最终结果高32bits全为1,这明显和我们的意图是不一致的。

在这里插入图片描述

关于移位操作,可以参考下 Linux 是如何用相关的宏来防止类似的问题的。

// ./include/uapi/linux/const.h
...
#ifdef __ASSEMBLY__
#define _AC(X,Y)	X
#define _AT(T,X)	X
#else
#define __AC(X,Y)	(X##Y)
#define _AC(X,Y)	__AC(X,Y)
#define _AT(T,X)	((T)(X))
#endif

#define _UL(x)		(_AC(x, UL))
#define _ULL(x)		(_AC(x, ULL))

#define _BITUL(x)	(_UL(1) << (x))
#define _BITULL(x)	(_ULL(1) << (x))
...

// ./include/vdso/const.h
#include <uapi/linux/const.h>

#define UL(x)		(_UL(x))
#define ULL(x)		(_ULL(x))

// 类似的,也有位域的定义方式
...
#define BIT(nr)			(UL(1) << (nr))

读者可能会问,为什么不直接在数值上带上后缀而要引入这些宏呢?原因是汇编代码不识别后缀,这些宏是为了能让相关的代码能够在C代码和汇编代码中都使用而引入的。

猜你喜欢

转载自blog.csdn.net/FJDJFKDJFKDJFKD/article/details/126428339
今日推荐