C ++在负expression式的“for”循环中崩溃

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

#include <string> using namespace std; int main() { string s = "aa"; for (int i = 0; i < s.length() - 3; i++) { } } 

虽然这段代码不会崩溃:

 #include <string> using namespace std; int main() { string s = "aa"; int len = s.length() - 3; for (int i = 0; i < len; i++) { } } 

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

s.length()是无符号整数types。 当你减去3,你做它的否定。 对于unsigned ,它意味着非常大

一个解决方法(只要string长到INT_MAX就有效)就是这样的:

 #include <string> using namespace std; int main() { string s = "aa"; for (int i = 0; i < static_cast<int> (s.length() ) - 3; i++) { } } 

哪个永远不会进入循环。

一个非常重要的细节是,你可能收到了一个“比较有符号和无符号值”的警告。 问题是,如果你忽略这些警告,你进入隐含的 “整数转换” (*)的非常危险的领域,它有一个定义的行为,但很难遵循:最好是永远不会忽略这些编译器警告。


(*)您可能也有兴趣了解“整数推广” 。

首先: 为什么它会崩溃? 让我们像debugging器一样通过你的程序。

注意:我假设你的循环体不是空的,但是可以访问这个string。 如果情况并非如此,那么崩溃的原因是通过整数溢出未定义的行为 。 请参阅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! . . 

我们希望检查i < s.length() - 3立即失败,因为s的长度是2(我们只有每个给定的长度在开头,从不改变它), 2 - 3-10 < -1是错误的。 但是我们在这里得到了一个“OK”。

这是因为s.length()不是2 。 这是2ustd::string::length()返回types是size_t ,它是一个无符号整数。 因此,回到循环条件,我们首先得到s.length()的值,所以2u ,现在减去33是一个整数字面值,由编译器将其解释为inttypes。 所以编译器必须计算2u - 3 ,不同types的两个值。 原始types的操作只对同一types有效,所以必须将其转换为另一种types。 有一些严格的规则,在这种情况下, unsigned “胜利”,所以3得到的转换为3u 。 在无符号整数中, 2u - 3u不能是-1u ,因为这个数字不存在(当然,因为它有一个符号)。 相反,它会计算每个以modulo 2^(n_bits)运算,其中n_bits是这种types的位数(通常是n_bits或64)。 所以,而不是-1我们得到4294967295u (假设32位)。

所以现在编译器是用s.length() - 3 (当然它比我快得多s.length() - 3 )),现在我们来进行比较: i < s.length() - 3 。 把值: 0 < 4294967295u 。 再次,不同的types, 0变成了0u ,比较0u < 4294967295u显然是真的,循环条件被肯定的检查,现在我们可以执行循环体了。

递增后,上面唯一改变的是i的值。 i的价值将再次被转换成一个无符号整数,因为比较需要它。

所以我们有

 (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! 

这是问题:你在循环体中做什么? 大概你可以访问你的string的i^th字符,不是吗? 即使这不是你的意图,你不仅访问了第零和第一,但也是第二! 第二个不存在(因为你的string只有两个字符,第零个和第一个),你访问内存你不应该,程序做任何想要的(未定义的行为)。 请注意,该程序不需要立即崩溃。 看来再过半个小时都能正常工作,所以这些错误很难赶上。 但是,超越界限访问内存总是很危险的,这是大多数崩溃来自的地方。

总而言之,从s.length() - 3得到与你所期望的不同的值,这导致了一个积极的循环条件检查,导致了循环体的重复执行,循环体本身就会访问它不应该。

现在让我们来看看如何避免这种情况,也就是说如何告诉编译器你的循环条件是什么意思。


string的长度和容器的大小本质上是无符号的,所以你应该在for循环中使用一个无符号的整数。

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

 #include <cstddef> #include <string> using namespace std; int main() { string s = "aa"; for ( size_t i = 0; i + 3 < s.length(); i++) { // ^^^^^^ ^^^^ } } 

由于a < b - 3在math上等于a + 3 < b ,因此我们可以互换它们。 然而, a + 3 < b防止b - 3是一个巨大的价值。 回想一下, s.length()返回一个无符号整数,无符号整数执行操作模块2^(bits) ,其中bits是types中的位数(通常为8,16,32或64)。 因此用s.length() == 2s.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! 

实际上,在第一个版本中,你循环了很长一段时间,因为你把i和一个包含非常大数字的无符号整数进行比较。 string的大小(实际上)与size_t是一个无符号整数。 当你从这个数值中减去3时,它会下溢并成为一个很大的值。

在代码的第二个版本中,将这个无符号的值赋给一个有符号的variables,这样就可以得到正确的值。

事实上并不是导致崩溃的条件或价值,它很可能是您将string超出边界索引,这是一个未定义的行为。

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

这里的大多数人似乎无法重现包括我自己在内的崩溃 – 看起来这里的其他答案是基于这样的假设,即在for循环体中省略了一些重要的代码,并且缺less的代码是造成你的崩溃。

如果您正在使用i来访问for循环体内的内存(可能是string中的字符),并且为了提供一个最简单的示例而将代码留在您的问题之外,那么崩溃很容易被事实由于无符号整数types的模运算, s.length() - 3的值为SIZE_MAXSIZE_MAX是一个非常大的数字,所以i会继续变大,直到它被用来访问触发段错误的地址。

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

下面的解释并不假定你在你的问题中遗漏了代码。 它相信你在你的问题上发布的代码崩溃了, 对于其他崩溃的代码,这不是一个简化的替代scheme。

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

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

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

如果在expression式评估过程中,结果不是math定义的,或者不在其types的可表示值范围内,则行为是未定义的。

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

无符号整数( unsigned符号整数)应遵循算术模2 n的定律,其中n是该特定整数大小的值表示中的位数。 46

46)这意味着无符号算术不会溢出,因为无法用结果无符号整数types表示的结果被减less的模数大于可由无符号整数types表示的最大值的数。

这意味着s.length() - 3的结果是值为SIZE_MAXsize_t 。 这是一个非常大的数字,大于INT_MAXint表示的最大值)。

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

在我的系统上,当i通过INT_MAX增量时,我的实现的行为是将(将i设置为INT_MIN )并继续。 一旦i达到-1,通常的算术转换(C ++ [expr]子句5段落9)导致i等于SIZE_MAX因此循环终止。

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

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

和第一个程序一样, s.length() - 3size_ttypes,其值为SIZE_MAX 。 但是,这次值被分配给一个int 。 C ++ 11标准规定(在[conv.integral]第4.7条第3款中):

如果目标types是有符号的,如果目标types可以用目标types(和位域宽度)表示,则该值不变。 否则,该值是实现定义的。

SIZE_MAX的值太大,无法用int来表示,所以len得到一个实现定义的值(可能是-1,但也许不是)。 无论分配给len的值是什么,条件i < len最终都是真的,所以你的程序将终止而不会遇到任何未定义的行为。

s.length()的types是size_t ,其值为2,因此s.length() – 3也是一个无符号typessize_t ,它的SIZE_MAX值是实现定义的(如果它的大小是64,则为18446744073709551615位)。 它至less是32位types(在64位平台中可以是64位),这个高数字意味着一个无限循环。 为了防止这个问题,你可以简单地将s.length()int

 for (int i = 0; i < (int)s.length() - 3; i++) { //..some code causing crash } 

在第二种情况下, len是-1,因为它是一个有signed integer ,它不会进入循环。

说到崩溃,这个“无限”的循环不是崩溃的直接原因。 如果你在循环中共享代码,你可以得到更多的解释。

由于s.length()是无符号types的数量,当你做s.length() – 3时,它变成负值,负值被存储为大的正值(由于无符号的转换规格),并且循环变得无限,因此它崩溃。

要使其工作,您必须将s.length()types化为:

static_cast <int>(s.length())

您遇到的问题来自以下声明:

 i < s.length() - 3 

s.length()的结果是无符号的 size_ttypes。 如果你想象两个二进制表示:

0 … 010

然后你从这里取代三个,你有三个有效的起飞,那就是:

0 … 001

0 … 000

但是,你有一个问题,删除下溢的第三个数字,因为它试图从左边获得另一个数字:

1 … 111

这就是发生了什么,无论你有一个无符号或有符号的types,但不同之处在于有符号types使用最高有效位(或最高有效位)来表示该数字是否为负数。 发生未定义stream时,它仅表示签名types的负值。

另一方面,size_t是无符号的 。 当下溢时,它将代表size_t可能表示的最大数字。 因此,循环实际上是无限的(取决于您的计算机,因为这会影响size_t的最大值)。

为了解决这个问题,你可以用几种不同的方式来操纵你的代码:

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

要么

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

甚至:

 int main() { string s = "aa"; for(size_t i = s.length(); i > 3; --i) { } } 

重要的是要注意的是替代已经被省略,相反,在其他地方,加法也被用于相同的逻辑评估。 第一个和最后一个都改变了在for循环中可用的i的值,而第二个将保持它相同。

我试图提供这个作为代码的例子:

 int main() { string s = "aa"; for(size_t i = s.length(); --i > 2;) { } } 

经过一番思考,我意识到这是一个坏主意。 读者的锻炼是为了解决这个问题!

原因是int a = 1000000000; 长长的b = a * 100000000; 会给出错误。 当编译器将这些数字相乘时,将它作为整数进行求值,因为a和literal是1000000000 int,并且由于10 ^ 18比int的上界大得多,所以会给出错误。 在你的情况,我们有s.length() – 3,因为s.length()是无符号整型,它不能为负,因为s.length() – 3被评为无符号整型,它的值是-1,它也给错误了。