为什么 long long n = 2000*2000*2000*2000; 会溢出?

156
long long int n = 2000*2000*2000*2000;    // overflow

long long int n = pow(2000,4);            // works
long long int n = 16000000000000;         // works

为什么第一个会溢出(将整数字面常量相乘后赋值给long long)?

与第二个或第三个有何不同之处?


53
pow(2000,4)使用的是 double,而 2000*2000*2000*2000 使用的是 int - Jarod42
12
第一个计算使用 int。2000 是一个 int,不是 long long int。 - drescherjm
13
因为最大的32位整数值是2^31 - 1,即2,147,483,647,小于200020002000*2000,并且所有的2000都是整型,所以计算结果会被视为整型而不是长整型。 - drescherjm
50
定期提醒:您对操作结果的处理方式不会影响计算该结果的方式。 - David Schwartz
29
这似乎是我找到的关于整数字面量溢出表达式的最佳规范问答,因此我已经将其他问题的重复标记或编辑为指向该问题。 - Peter Cordes
显示剩余5条评论
6个回答

143

因为2000是一个通常为32位的int。只需使用2000LL

建议使用LL后缀而不是ll,这是由@AdrianMole在现已删除的评论中提出的,请查看他的答案

默认情况下,整数字面量的类型是可以容纳其值但不小于int的最小类型。2000可以很容易地存储在int中,因为标准保证它实际上至少是一个16位类型。

算术运算符总是使用存在的类型中较大的类型,但不小于int

  • char*char将被提升为operator*(int,int)->int
  • char*int调用operator*(int,int)->int
  • long*int调用operator*(long,long)->long
  • int*int仍然调用operator*(int,int)->int

关键是,该类型不取决于结果是否可以存储在推断的类型中。这正是发生在您的例子中的问题-乘法使用int,但结果溢出仍然存储为int

C ++不支持像Haskell那样基于目标推断类型,因此赋值是无关紧要的。


评论不是用来进行长时间讨论的;此对话已经移动到聊天室 - Samuel Liew

94

你第一行代码右侧的常量(字面值)是 int 值(不是 long long int)。因此,乘法使用 int 算术进行,会发生溢出。

要解决这个问题,请使用 LL 后缀将常量更改为 long long

long long int n = 2000LL * 2000LL * 2000LL * 2000LL;

cppreference

事实上,正如Peter Cordes所述的评论中指出的那样,在第一个(最左边)或第二个常量上只实际上需要使用LL后缀。这是因为当两种不同ranks类型相乘时,级别较低的操作数会被提升到较高级别的类型,如此处所述:Implicit type conversion rules in C++ operators。此外,由于*(乘法)运算符具有left-to-right associativity,第一次乘法的“提升”结果将该提升传播到第二个和第三个乘法。

因此,以下任何一行也可在不溢出的情况下工作:

long long int n1 = 2000LL * 2000 * 2000 * 2000;
long long int n2 = 2000 * 2000LL * 2000 * 2000;

注意:虽然小写后缀(如2000ll)在C++中是有效的,且对于编译器来说完全没有歧义,但普遍共识是应该避免在longlong long整数字面量中使用小写字母“ell”,因为它很容易被人类读者误认为数字1。因此,您会注意到在这里呈现的答案中使用了大写后缀2000LL

46
*从左往右结合,因此只有最左边的“2000LL”实际上需要一个“LL”后缀。随着其他两个*运算符的计算,它们将被隐式提升为“long long”。在所有这些数字上都使用LL并没有什么不好的地方;对于阅读代码的人来说可以减少疑惑,但这只是作为今后参考的一点说明。C++运算符中的隐式类型转换规则 - Peter Cordes
1
@PeterCordes 我已经把你的评论加入了我的答案 - 希望你不介意! 起初我有点犹豫,因为这个问题在其他答案中已经(部分地)得到了解决(尤其是 Werner 的回答)。然而,希望我能更详细地解释这个问题。 - Adrian Mole
3
当人们通过评论找到方法来改进文章时,这通常是一件好事,包括借用某些措辞,特别是在像这样的规范问答中,希望许多未来的读者最终会看到。改进文章正是评论的目的,所以干杯。 :) 是的,我在评论后才注意到 Werner 的回答。解释这一点肯定是有益的;在寻找重复内容时(我最终将其关闭为此问题的重复,因为它有很好的答案),我发现有些重复错误地陈述使任何数字都能工作的情况。 - Peter Cordes
1
如果LL在第三个常量上,这也能行得通吗?前两个在int算术中相乘,但这没问题,因为2000*2000适合一个int - Federico Poloni
@FedericoPoloni 是的,在这四个特定值的情况下,那样做是可行的。然而,我的回答旨在解决更一般的情况,在这些情况下,必须从一开始就应用提升。但我也认为,在所有四个操作数上使用后缀是最好/最清晰的编码风格:如果你想要 long long 算术运算,就使用 long long 操作数。 - Adrian Mole
2
@FedericoPoloni 还要注意(也许更重要的是),如果“int”宽度为16位,则2000 * 2000溢出。我记得,C++标准允许使用16位的“int”,32位的“long”和64位的“long long”。 - Adrian Mole

50

2000*2000*2000*2000是4个int值的乘积,其结果为一个int值。当你将这个int值赋给long long int n时,溢出已经发生了(如果int是32位,则结果值将无法容纳)。

你需要确保不会发生溢出,因此当你写下

long long int n = static_cast<long long int>(2000)*2000*2000*2000;

请确保进行 long long int 乘法运算(long long intint 相乘会返回一个 long long int, 因此在您的情况下不会发生溢出)。

一种更简洁且更好的方法是,使用 2000LL2000ll 替代 static_cast。这样可以将整数字面值指定为正确的类型。对于适合于 int 的 2000,不需要这样做,但如果有超出 int 范围的更高值,则需要这样做。

long long int n = 2000LL*2000*2000*2000;
long long int n = 2000LL*2000LL*2000LL*2000LL;

5
Clang提示:警告:使用旧式强制类型转换[-Wold-style-cast]! - Adrian Mole
5
您可以使用C++类型转换的语法,即static_cast<long long int>(2000)来避免这个问题(尽管我通常会省略隐含的“int”部分)。但在这种情况下,使用2000LL更为简单。 - ShadowRanger
1
@HolyBlackCat 我使用带有 /Wall 的 Visual Studio 中的 clang-cl,并且确实会出现警告。另外,为什么要使用可以做任何事情的 C 风格转换,而更柔和的 static_cast 就足够了呢? - Adrian Mole
@AdrianMole 因为这样打字更少。 :) - HolyBlackCat
1
在编写代码时,任何C风格的强制类型转换都是自动代码审查问题,因此保留它会浪费每次再次查看它时的时间和注意力。函数式转换的字符数相同,因此没有必要使用C风格转换。 - JDługosz
显示剩余2条评论

30
截至目前为止,其他答案似乎没有明确回答所提出的问题。我会尝试填补这个空白。
“为什么第一个表达式会溢出(将整数字面常量相乘以分配给long long)?”
该表达式
long long int n = 2000*2000*2000*2000;

被解释为下列内容:

long long int n = ((2000*2000)*2000)*2000;

以下是步骤(假设为32位int):

  1. (2000*2000) 是两个int值的乘积,得到4000000,另一个int值。
  2. ((2000*2000)*2000) 是上述产生的int值4000000与一个int值2000相乘。如果该值可以适合int中,则会得到8000000000。但是我们假设的32位int只能存储最大值231-1 = 2147483647。因此我们在这一点上就会发生溢出。
  3. 下一个乘法将会发生,如果上面没有溢出。
  4. 结果int的赋值将会发生(如果没有溢出),将其赋给long long变量,它将保留该值。

由于我们确实发生了溢出,所以该语句具有未定义行为,因此无法保证步骤3和4。

与第二个或第三个有什么不同?

  • long long int n = pow(2000,4);

pow(2000,4)20004转换为double(请参阅some docs on pow),然后函数实现尽最大努力产生结果的良好近似值,作为double。然后赋值将此double值转换为long long

  • long long int n = 16000000000000;

字面量16000000000000太大,无法适应int,因此其类型是可以容纳该值的下一个有符号类型。它可能是longlong long,具体取决于平台。有关详细信息,请参见Integer literal#The type of the literal。然后赋值将此值转换为long long(如果字面量的类型已经是long long,则只需写入即可)。


感谢您详细的回答! - Yves Gurcan

19

首先是使用整数(通常为32位)进行乘法运算,因为这些整数无法存储2000^4,所以会发生溢出。然后将结果转换为long long int

其次是调用pow函数,该函数将第一个参数转换为double并返回一个double。然后将结果转换为long long int。在这种情况下不存在溢出,因为计算是在double值上进行的。


9
int 可以非常狭窄,最窄为16位,在一些现代的嵌入式微控制器上(例如AVR或MSP430)会出现,因此如果最终值大于32767,则需要考虑可移植性。 (您不太可能找到具有64位“int”的C实现,尽管我记得有极少数情况。并且从历史上看,“int”可能不完全是32位。)难以精确地进行描述而不使答案膨胀,但是可以说“使用int(通常为32位)”。 - Peter Cordes

5

如果您想理解这个问题,可以尝试使用以下C++代码:

#include<iostream>
#include<cxxabi.h>

using namespace std;
using namespace abi;

int main () {
    int status;
    cout << __cxa_demangle(typeid(2000*2000*2000*2000).name(),0,0,&status);
}

如您所见,类型为int

在C语言中,您可以使用(由此提供):

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>

#define typename(x) _Generic((x),        /* Get the name of a type */             \
                                                                                  \
        _Bool: "_Bool",                  unsigned char: "unsigned char",          \
         char: "char",                     signed char: "signed char",            \
    short int: "short int",         unsigned short int: "unsigned short int",     \
          int: "int",                     unsigned int: "unsigned int",           \
     long int: "long int",           unsigned long int: "unsigned long int",      \
long long int: "long long int", unsigned long long int: "unsigned long long int", \
        float: "float",                         double: "double",                 \
  long double: "long double",                   char *: "pointer to char",        \
       void *: "pointer to void",                int *: "pointer to int",         \
       char(*)[]: "pointer to char array",      default: "other")


unsigned int a = 3;
int main() {
    printf("%s", typename(a-10));
    return 0;
}

在这里,表达式的类型是无符号整数,因为类型不匹配会隐式地将类型升级为无符号整数和整数之间最大的类型,即无符号整数。 无符号整数将下溢为大正数,当分配给或解释为整数时,将成为预期的负数。计算结果始终为无符号整数,而与所涉及的值无关。

C

整型字面量的最小默认类型是int,但仅当字面量超过此限制时,它的类型才变为unsigned int; 如果大于此限制,则将其赋予long int类型,因此2000都是int。但是,在使用一元或二元运算符对表达式执行操作时,使用隐式类型层次结构来决定类型,而不是结果的值(与字面量本身不同,后者在确定类型时使用字面量的长度),这是因为C使用类型强制转换而不是类型合成。为了解决这个问题,您需要在2000上使用长后缀ul来显式指定字面量的类型。
类似地,十进制字面量的默认类型是double,但是可以通过f后缀进行更改。前缀不会改变十进制或整数字面量的类型。
字符串字面值的类型是char [],虽然实际上它是一个const char [],只是指向在.rodata中表示该字符串字面值的第一个字符的地址,并且可以像使用任何数组一样使用一元符号&"string"获取地址,这个地址与"string"相同(即地址相同),只是类型不同(char (*)[7]char[7]"string"char[]在编译器级别上不仅仅是指向数组的指针,它本身就是数组,而一元符号提取的只是指向数组的指针)。前缀u将其更改为char16_t数组,它是一个unsigned short int;前缀U将其更改为char32_t数组,它是一个unsigned int;前缀L将其更改为wchar_t数组,它是一个intu8是一个char,未带前缀的字符串使用实现特定的编码,通常与u8相同,即UTF-8,其中ASCII是一个子集。只有对字符串字面值可用的原始(R)前缀(仅在GNU C上可用(std=gnu99以后))可以添加前缀,即uRu8R,但这不会影响类型。
字符字面值的类型为int,除非带有前缀uu'a'unsigned short int)或UU'a'unsigned int)。在字符字面值上使用u8L时,都是int。字符串或字符字面值中的转义序列不影响编码和类型,只是将要编码的字符实际呈现给编译器的一种方式。
复数字面值10i+110j+1的类型为complex int,其中实部和虚部都可以有后缀,如10Li+1,在这种情况下,虚部变成了长整型,整个类型为complex long int,升级了实部和虚部的类型,因此无论后缀放在哪里或者是否放在两个位置上,都不会产生影响。如果不匹配,则总是使用两个后缀中最大的作为整体类型。
使用显式转换而不是字面量后缀总是会产生正确的行为,如果您使用它正确并了解语义差异(对于signed进行符号扩展;对于unsigned进行零扩展 - 这基于要转换的文字或表达式的类型,而不是被转换成的类型,因此signed int扩展为unsigned long int),将文字截断/扩展为该类型的表达式,而不是文字固有地具有该类型。

enter image description here

再次提醒,最小的默认类型是 int,对于最小的文字基础。文字基础即文字的实际值和后缀会根据下表影响最终的文字类型,在每个后缀的框中,根据实际文字基础的大小,列出了最终类型的顺序,从小到大排列。对于每个后缀,文字的最终类型只能等于或大于后缀类型,并且基于文字基础的大小。C 表现出相同的行为。当大于 long long int 时,根据编译器的不同,会使用 __int128。我认为您还可以创建自己的文字后缀运算符 i128 并返回该类型的值。
十进制文字的默认类型与 C 相同。
字符串字面值的类型是char []&"string"的类型是const char (*) [7]+"string"的类型是const char *(在C中,只能使用"string"+0进行衰减)。 C ++不同之处在于后两种形式获得了const,但在C中它们没有。字符串前缀的行为与C相同。
字符和复数字面值的行为与C相同。

1
@MaksimKuzmin 这个问题看起来很简单,但它隐藏了CPU级别的数字底层系统表示以及C/C++语言如何处理它。事实上,这不是一个简单的问题,因此这个非常详细的答案对于这个问题是有意义和有用的。 - Zilog80
@Zilog80 是的,我只是想要一个关于字面量的指南以便日后参考。 - Lewis Kelsey
@LewisKelsey 你的意思是最好放链接到字面量文档,而不是嵌入它,我完全同意。我主要是指出在 CPU 级别上数字表示值得一些阐述[但确实不需要嵌入文档摘录]。 - Zilog80

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