BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / cpp / #96111同步于 2017/8/26
该镜像源已超过 30 天没有更新,可能在源站已被删除。
CPP机器人发帖

[C语言也惊喜]这个常量的值是什么?(答案公布)

nuanyangyang
2017/8/26镜像同步50 回复
long long a = -0x8000000000000000LL; println("%lld\n", a); 上述C++程序的输出结果是什么?(假设使用C++14标准。这个平台上long long的取值范围是[-0x8000000000000000, 0x7fffffffffffffff]) (1)-9223372036854775808 (2)9223372036854775808 (3)0 (4)稍等,我去拿灭火器…… 更新:话说怎么都选1?有没有选别的的?那个灭火器多好玩呀,不好奇灭火器是干什么用的吗,怎么没人选呢? 更新:答案:(4) C/C++里面其实没有负整数的字面值(literal)。所以,-0x8000000000000000LL这个表达式,在编译器看来,相当于:-(0x8000000000000000LL),也就是正的0x8000000000000000LL然后取负。 C/C++的literal: 0x8000000000000000LL由于标注了LL,而且是十六进制的,所以可能的类型是long long或者unsigned long long,取最小的能够装下这个数的类型。题目规定了long long最大只到0x7fffffffffffffff,装不下,所以,能承载这个数值的只有unsigned long long。所以0x8000000000000000LL是unsigned long long型的。对这个值取负,仍然是unsigned long long,同时按规定,结果取除以2的64次方的模,所以(-0x8000000000000000LL)的值还是正的0x8000000000000000LL本身。 然后就出问题了:下一步,把这个unsigned long long值往long long型的变量a里赋值,而题目假设了long long的范围是[-0x8000000000000000, 0x7fffffffffffffff],那个0x8000000000000000超出范围了。将无符号整数转为有符号整数,超出有符号整数能表示的范围,结果是未定义行为,即undefined behaviour,也就是任何事都可以发生,从什么都不发生到机器冒烟都有可能。所以运行程序之前准备一个灭火器也是合理的做法。 那么,-9223372036854775808LL这个表达式又怎么样呢?9223372036854775808LL是十进制的,加了LL,所以,和十六进制不一样,C语言这时候只会把它当作long long,而不会当作unsigned long long。而这个数9223372036854775808的数学值是十六进制的0x8000000000000000,已经超出long long的范围了,所以,当写出9223372036854775808LL的时候就已经是undefined behaviour了,连负号“-”都来不及求值。 这可以说是C/C++语言设计上的一个很微妙的细节吧。在C/C++诞生之初,并不是所有的机器都使用补码(2's complement)表示法,使用原码、反码的也都有,所以C语言并没有规定有符号数要用补码表示法。表示法没有规定,那么一个类型能表示的数的范围也就无法确定。比如,如果使用了反码(1's complenent)表示法,那么64位有符号整数的范围就是[-0x7FFFFFFFFFFFFFFF, 0x7FFFFFFFFFFFFFFF],且分正0和负0。C语言有符号和无符号数转换的时候保持的是数学数值,而不是按比特的表示法。所以,当一个无符号数无法被有符号数表示的时候,就真的没有符合逻辑的结果了。更况且有的硬件会在整数溢出时抛出硬件异常(如MIPS,或者ARM在某些配置的情况下)。这样,从语言规范的角度,甚至连规定“溢出时结果是不可预料的值”都不行,只能“溢出时什么都可能发生,从什么都不发生到机器冒烟都可以”,即“undefined behaviour”了。 这种设计可以反映出C语言的理念:尽可能避免插入运行时的安全检查,以提高执行效率,而让程序员进行安全检查。同时还可以允许各种疯狂的优化,比如既然有符号整数溢出是undefined behaviour,那么编译器就可以忽略溢出,而假设“x+1 > x”这个表达式对任何x都是真。可以对比,Java不是这样的,Java规定了long的范围是[-0x8000000000000000, 0x7FFFFFFFFFFFFFFF],而且溢出以后会回绕到另一端。这就使得这种行为非常适合用补码实现,而不使用补码的机器也必须模拟这种行为。由于补码表示法的优势,如今大多数机器都使用补码来表示负数,所以Java的设计也是合理的。 我一点也不喜欢C/C++的设计,但毕竟标准定成这样了,这等于给了编译器一个“杀人的许可证”:只要程序里出现了9223372036854775808LL,编译器就认为这是undefined behaviour,就可以做任何事(让机器冒烟也可以)。所以程序员不得不尽一切努力避免出现9223372036854775808LL。 还是来欣赏一下GCC的limits.h吧: /* If we are not using GNU CC we have to define all the symbols ourself. Otherwise use gcc's definitions (see below). */ // 注:GCC有内置常量,如__LLONG_LONG_MAX__等。如果是新版本的GCC,就不用这里的定义了, // 而是直接将标准的常量,如LLONG_MAX,定义为编译器内置常量,如__LONG_LONG_MAX__。 #if !defined __GNUC__ || __GNUC__ < 2 // ... // 这里省略了很多定义 /* Minimum and maximum values a `signed short int' can hold. */ # define SHRT_MIN (-32768) # define SHRT_MAX 32767 /* Maximum value an `unsigned short int' can hold. (Minimum is 0.) */ # define USHRT_MAX 65535 // 接下来,就是见证奇迹的时刻: /* Minimum and maximum values a `signed int' can hold. */ # define INT_MIN (-INT_MAX - 1) # define INT_MAX 2147483647 /* Maximum value an `unsigned int' can hold. (Minimum is 0.) */ # define UINT_MAX 4294967295U /* Minimum and maximum values a `signed long int' can hold. */ # if __WORDSIZE == 64 # define LONG_MAX 9223372036854775807L # else # define LONG_MAX 2147483647L # endif # define LONG_MIN (-LONG_MAX - 1L) /* Maximum value an `unsigned long int' can hold. (Minimum is 0.) */ # if __WORDSIZE == 64 # define ULONG_MAX 18446744073709551615UL # else # define ULONG_MAX 4294967295UL # endif # ifdef __USE_ISOC99 /* Minimum and maximum values a `signed long long int' can hold. */ # define LLONG_MAX 9223372036854775807LL # define LLONG_MIN (-LLONG_MAX - 1LL) /* Maximum value an `unsigned long long int' can hold. (Minimum is 0.) */ # define ULLONG_MAX 18446744073709551615ULL # endif /* ISO C99 */ # endif /* limits.h */ #endif /* GCC 2. */ 恭喜 @abcd12345678 @prison 回答正确! [ema28]
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
glswy机器人#1 · 2017/8/27
暖神,还活着吗?所以结果是什么?
malo机器人#2 · 2017/8/27
溢出了,选1? 发自「贵邮」
nvyoujiaren机器人#3 · 2017/8/28
第一次见在16进制面前使用符号的,只有十进制才有符号这一概念,难道标准又改了,我数学学得不好不要骗我~~~~~~~~
firekisser机器人#4 · 2017/8/28
没用C++14,看起来是(1) ※@※:~/POJ/test$ more test_overflow.cc #include <iostream> #include <cstdio> using namespace std; int main(int argc, char ** argv) { long long a = -0x8000000000000000LL; printf("%lld\n", a); return 0; } ※@※:~/POJ/test$ ./test_overflow -9223372036854775808 ※@※:~/POJ/test$
qyz0123321机器人#5 · 2017/8/28
转化成汇编: movabs $0x8000000000000000,%rax 符号已经被丢弃了。 @nvyoujiaren long long的取值范围是[-0x8000000000000000, 0x7fffffffffffffff]?这个不应该是[-2^63, 2^63-1]么?
nvyoujiaren机器人#6 · 2017/8/28
一看你就是刚学的学生,记得好清楚,莫非是学霸??? 【 在 qyz0123321 的大作中提到: 】 : 转化成汇编: : movabs $0x8000000000000000,%rax : 符号已经被丢弃了。 : ...................
qyz0123321机器人#7 · 2017/8/28
没,只是对这个知识点比较印象深刻罢了。。。 【 在 nvyoujiaren 的大作中提到: 】 : 一看你就是刚学的学生,记得好清楚,莫非是学霸???
lance6716机器人#8 · 2017/8/28
[-0x8000000000000000, 0x7fffffffffffffff]就是[-2^63, 2^63-1]啊,质疑暖神之前要double check 【 在 qyz0123321 的大作中提到: 】 : 转化成汇编: : movabs $0x8000000000000000,%rax : 符号已经被丢弃了。 : ...................
lance6716机器人#9 · 2017/8/28
不熟悉C++的规范,那我只能瞎猜了 那个负号会触发类型转换?