C++在带有否定表达式的'for'循环中崩溃

use*_*633 55 c++

以下代码使运行时错误导致C++崩溃:

#include <string>

using namespace std;

int main() {
    string s = "aa";
    for (int i = 0; i < s.length() - 3; i++) {

    }
}
Run Code Online (Sandbox Code Playgroud)

虽然此代码不会崩溃:

#include <string>

using namespace std;

int main() {
    string s = "aa";
    int len = s.length() - 3;
    for (int i = 0; i < len; i++) {

    }
}
Run Code Online (Sandbox Code Playgroud)

我只是不知道如何解释它.这种行为可能是什么原因?

Ant*_*nio 85

s.length()是无符号整数类型.当你减去3时,你将它减为负数.对于一个unsigned,它意味着非常大.

解决方法(有效期为字符串长达INT_MAX)将是这样的:

#include <string>

using namespace std;

int main() {

    string s = "aa";

    for (int i = 0; i < static_cast<int> (s.length() ) - 3; i++) {

    }
}
Run Code Online (Sandbox Code Playgroud)

哪个永远不会进入循环.

一个非常重要的细节是您可能收到了"比较有符号和无符号值"的警告.问题是,如果忽略这些警告,则进入隐式 "整数转换" (*)的非常危险的字段,该字段具有已定义的行为,但很难遵循:最好的是永远不要忽略那些编译器警告.


(*)您可能也有兴趣了解"整数提升".

  • 在这种特殊情况下,这是一种解决方法,但通常情况下很差.如果字符串的长度大于有符号整数可表示的最大值,则您的转换会冒未定义的行为. (8认同)
  • 对于使用强制转换而不仅仅使用正确类型的解决方案,哇57赞成(目前为止)?没有冒犯,这将有效,但imho这不是正确的做法.我对这个答案看起来多么受欢迎印象深刻. (7认同)
  • @Antonio此行为不称为"整数提升",而是"整数转换".当`char`,`short`和bitfields被隐式提升为更宽的类型`int`时,就会发生"整体推文". (6认同)
  • "在这种特殊情况下,这是一种解决方法,但通常是一种糟糕的解决方案",我不同意这种情况一般都很差.您希望有多少次使用长度超过20亿个字符的字符串?当然,*要注意*你可能会被截断,但是这种解决方案的替代方案往往更复杂,而且通常情况下这很好. (4认同)
  • @SChepurin你到底是什么意思? (3认同)
  • 是的我也不同意这个答案.将字符串长度(固有的无符号)转换为有符号整数听起来像是一个等待爆炸的设计缺陷.如果你需要你的字符串长度至少为三个字符,这意味着你需要它遵循某种格式,这意味着你应该在操作它之前验证它. (2认同)

ste*_*fan 28

首先:为什么会崩溃?让我们像调试器一样逐步执行您的程序.

注意:我假设你的循环体不是空的,但访问字符串.如果不是这种情况,则崩溃的原因是通过整数溢出的未定义行为.请参阅Richard Hansens的答案.

std::string s = "aa";//assign the two-character string "aa" to variable s of type std::string
for ( int i = 0; // create a variable i of type int with initial value 0 
i < s.length() - 3 // call s.length(), subtract 3, compare the result with i. OK!
{...} // execute loop body
i++ // do the incrementing part of the loop, i now holds value 1!
i < s.length() - 3 // call s.length(), subtract 3, compare the result with i. OK!
{...} // execute loop body
i++ // do the incrementing part of the loop, i now holds value 2!
i < s.length() - 3 // call s.length(), subtract 3, compare the result with i. OK!
{...} // execute loop body
i++ // do the incrementing part of the loop, i now holds value 3!
.
.
Run Code Online (Sandbox Code Playgroud)

我们预计支票i < s.length() - 3马上失败,因为长度s为两(我们只每一个给定它在开始的长度,并没有改变它),2 - 3-1,0 < -1是假的.但是我们在这里得到了"OK".

这是因为s.length()不是2.是的2u.std::string::length()具有size_t无符号整数的返回类型.所以回到循环条件,我们首先得到的值s.length(),所以2u,现在减去3.3是一个整数文字,由编译器解释为类型int.所以编译器必须计算2u - 3两种不同类型的值.对原始类型的操作仅适用于相同类型,因此必须将其转换为另一种类型.有一些严格的规则,在这种情况下,unsigned"胜利",所以3get转换为3u.在无符号整数中,2u - 3u不能-1u像这样的数字不存在(好吧,因为它有一个标志当然!).相反,它计算每个操作modulo 2^(n_bits),在哪里n_bits是此类型的位数(通常为8,16,32或64).所以而不是-1我们得到4294967295u(假设32位).

所以现在编译器完成了s.length() - 3(当然它比我快得多;-)),现在让我们进行比较:i < s.length() - 3.投入价值观:0 < 4294967295u.再次,不同的类型0变成0u,比较0u < 4294967295u显然是正确的,循环条件被正面检查,我们现在可以执行循环体.

递增后,上面唯一改变的是值i.将值i再次转换为unsigned int,因为比较需要它.

所以我们有

(0u < 4294967295u) == true, let's do the loop body!
(1u < 4294967295u) == true, let's do the loop body!
(2u < 4294967295u) == true, let's do the loop body!
Run Code Online (Sandbox Code Playgroud)

这就是问题:你在循环体中做了什么?想必您访问i^th您的字符串的字符,不是吗?即使这不是你的意图,你不仅访问了第一个,而且第二个!第二个不存在(因为你的字符串只有两个字符,第0个和第一个),你访问内存你不应该,程序做任何它想要的(未定义的行为).请注意,程序不需要立即崩溃.它似乎可以再工作半小时,所以这些错误很难捕捉到.但是访问超出边界的内存总是很危险的,这是大多数崩溃的来源.

总而言之,你s.length() - 3从你期望的那里得到一个不同的值,这会产生一个正循环条件检查,导致循环体的重复执行,循环体本身不应该访问内存.

现在让我们看看如何避免这种情况,即如何告诉编译器你在循环条件中实际意味着什么.


字符串的长度和容器的大小本质上是无符号的,因此您应该在for循环中使用无符号整数.

由于unsigned int相当长,因此不希望在循环中反复写入,只需使用size_t.这是STL中用于存储长度或大小的每个容器的类型.您可能需要包含cstddef以断言平台独立性.

#include <cstddef>
#include <string>

using namespace std;

int main() {

    string s = "aa";

    for ( size_t i = 0; i + 3 < s.length(); i++) {
    //    ^^^^^^         ^^^^
    }
}
Run Code Online (Sandbox Code Playgroud)

由于a < b - 3在数学上等同于a + 3 < b,我们可以互换它们.但是,a + 3 < b防止b - 3成为巨大的价值.回想一下,s.length()返回无符号整数和无符号整数执行操作模块2^(bits),其中bits是类型中的位数(通常为8,16,32或64).因此s.length() == 2,s.length() - 3 == -1 == 2^(bits) - 1.


或者,如果您想i < s.length() - 3用于个人偏好,则必须添加条件:

for ( size_t i = 0; (s.length() > 3) && (i < s.length() - 3); ++i )
//    ^             ^                    ^- your actual condition
//    ^             ^- check if the string is long enough
//    ^- still prefer unsigned types!
Run Code Online (Sandbox Code Playgroud)


Som*_*ude 12

实际上,在第一个版本中,循环很长一段时间,与 包含非常大数字i无符号整数进行比较.字符串的大小(实际上)与size_t无符号整数相同.当你3从该值中减去它时,它会下溢并继续成为一个很大的值.

在代码的第二个版本中,将此无符号值分配给有符号变量,这样您就可以得到正确的值.

并且它实际上不是导致崩溃的条件或值,它最有可能是将字符串索引出界限,这是一种未定义的行为.

  • *unsigned*类型的上/下溢实际上是明确定义的.请参阅:http://stackoverflow.com/q/988588/1030702,http://stackoverflow.com/a/2760612/1030702,C/C++标准.虽然流行的编译器(VC++,GCC)现在也做同样的事情但它仍然不好依赖它.*signed*类型尚未定义. (5认同)
  • 你确定第一个循环只是很长,而不是无限的吗?int是否可以达到std :: numeric_limits <unsigned int> :: max()?当`i`到达边界时会发生什么?回答我的自我,可能它会变成负面的并且在整数提升中它将真正变得更大/表达的右边部分的相同值.但是很难的情况!:) (2认同)

Ric*_*sen 5

假设你在for循环中遗漏了重要的代码

这里的大多数人似乎无法重现崩溃 - 我自己包含 - 并且看起来这里的其他答案是基于你在for循环体中遗漏了一些重要代码的假设,并且丢失的代码是导致你的崩溃.

如果您使用i的主体访问内存(字符串中可能人物)for循环,你离开的代码你的问题,试图提供一个最小的例子,然后崩溃很容易被这样的事实解释s.length() - 3SIZE_MAX由于无符号整数类型的模运算所致的值. SIZE_MAX是一个非常大的数字,所以i将继续变大,直到它用于访问触发段错误的地址.

但是,即使for循环体是空的,理论上你的代码也可以原样崩溃.我不知道任何会崩溃的实现,但也许你的编译器和CPU是异国情调.

以下说明并未假设您在问题中遗漏了代码.我们相信您在问题中发布的代码会按原样崩溃; 它不是一个崩溃的其他代码的缩写替代品.

为什么你的第一个程序崩溃了

您的第一个程序崩溃,因为这是对代码中未定义行为的反应.(当我尝试运行您的代码时,它会终止而不会崩溃,因为这是我的实现对未定义行为的反应.)

未定义的行为来自于溢出int.C++ 11标准说(在[expr]第5条第4款中):

如果在评估表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义.

在您的示例程序中,s.length()返回size_t值为2.从中减去3将产生负1,除了size_t是无符号整数类型.C++ 11标准说(在[basic.fundamental]第3.9.1条第4款中):

声明的无符号整数unsigned应遵守算术模2 n的定律,其中n是该特定整数大小的值表示中的位数.46

46)这意味着无符号算术不会溢出,因为无法用结果无符号整数类型表示的结果是以比可以由结果无符号整数类型表示的最大值大1的数量减少的模数.

这意味着结果s.length() - 3size_t有价值的SIZE_MAX.这是一个非常大的数字,大于INT_MAX(可表示的最大值int).

因为s.length() - 3这么大,执行会在循环中旋转直到i达到INT_MAX.在下一次迭代中,当它尝试递增时i,结果将为INT_MAX+ 1但不在可表示值的范围内int.因此,行为是不确定的.在您的情况下,行为是崩溃.

在我的系统上,我的实现在i过去增加时的行为INT_MAX是换行(设置iINT_MIN)并继续.一旦i达到-1,通常的算术转换(C++ [expr]第5段第9段)会导致i相等,SIZE_MAX因此循环终止.

这两种反应都是合适的.这是未定义行为的问题 - 它可能会按照您的意图工作,它可能会崩溃,它可能会格式化您的硬盘驱动器,或者它可能会取消Firefly.你永远都不会知道.

你的第二个程序如何避免崩溃

与第一个程序一样,s.length() - 3是一个size_t有价值的类型SIZE_MAX.但是,这次将值分配给int.C++ 11标准说(在[conv.integral]第4.7条第3款中):

如果目标类型已签名,则如果可以在目标类型(和位字段宽度)中表示该值,则该值不会更改; 否则,该值是实现定义的.

该值SIZE_MAX太大而无法通过a表示int,因此len获取实现定义的值(可能为-1,但可能不是).i < len无论分配给哪个值,条件最终都将成立len,因此您的程序将终止而不会遇到任何未定义的行为.