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
是-1
, 0 < -1
是错误的。 但是我们在这里得到了一个“OK”。
这是因为s.length()
不是2
。 这是2u
。 std::string::length()
返回types是size_t
,它是一个无符号整数。 因此,回到循环条件,我们首先得到s.length()
的值,所以2u
,现在减去3
。 3
是一个整数字面值,由编译器将其解释为int
types。 所以编译器必须计算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() == 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!
实际上,在第一个版本中,你循环了很长一段时间,因为你把i
和一个包含非常大数字的无符号整数进行比较。 string的大小(实际上)与size_t
是一个无符号整数。 当你从这个数值中减去3
时,它会下溢并成为一个很大的值。
在代码的第二个版本中,将这个无符号的值赋给一个有符号的variables,这样就可以得到正确的值。
事实上并不是导致崩溃的条件或价值,它很可能是您将string超出边界索引,这是一个未定义的行为。
假设你在for
循环中遗漏了重要的代码
这里的大多数人似乎无法重现包括我自己在内的崩溃 – 看起来这里的其他答案是基于这样的假设,即在for
循环体中省略了一些重要的代码,并且缺less的代码是造成你的崩溃。
如果您正在使用i
来访问for
循环体内的内存(可能是string中的字符),并且为了提供一个最简单的示例而将代码留在您的问题之外,那么崩溃很容易被事实由于无符号整数types的模运算, s.length() - 3
的值为SIZE_MAX
。 SIZE_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是该特定整数大小的值表示中的位数。 4646)这意味着无符号算术不会溢出,因为无法用结果无符号整数types表示的结果被减less的模数大于可由无符号整数types表示的最大值的数。
这意味着s.length() - 3
的结果是值为SIZE_MAX
的size_t
。 这是一个非常大的数字,大于INT_MAX
( int
表示的最大值)。
因为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() - 3
是size_t
types,其值为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,它也给错误了。