C++在具有负表达式的'for'循环中崩溃

C++ crashes in a 'for' loop with a negative expression

本文关键字:for 循环 崩溃 表达式 C++      更新时间:2023-10-16

以下代码因运行时错误导致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()是无符号整数类型。当你减去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++) {
    }
}

它永远不会进入循环。

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


(*)您可能也有兴趣了解"整数促销"

首先:为什么会崩溃?让我们像调试程序一样逐步完成您的程序。

注意:我假设循环体不是空的,而是访问字符串。如果不是这种情况,则崩溃的原因是整数溢出导致的未定义行为。请看理查德·汉森对此的回答。

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 - 3-10 < -1是false。然而,我们在这里确实得到了"OK"。

这是因为s.length()不是2。是2ustd::string::length()的返回类型为size_t,它是一个无符号整数。回到循环条件,我们首先得到s.length()的值,所以2u,现在减去33是一个整数字面值,编译器将其解释为类型int。因此编译器必须计算2u - 3,这两个不同类型的值。基元类型上的操作只适用于相同的类型,所以必须将其中一个转换为另一个。有一些严格的规则,在这种情况下,unsigned"获胜",因此3将转换为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的值将再次转换为无符号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!

问题是:你在循环体中做什么?假设您访问了字符串的i^th字符,不是吗?尽管这不是你的本意,但你不仅访问了第零个和第一个,还访问了第二个!第二个不存在(因为你的字符串只有两个字符,第零个和第一个),你访问了不应该访问的内存,程序可以随心所欲(未定义的行为)。请注意,程序不需要立即崩溃。它似乎还能再工作半个小时,所以这些错误很难被发现。但是,超范围访问内存总是很危险的,这就是大多数崩溃的原因。

总之,您从s.length() - 3中获得的值与您预期的值不同,这会导致正循环条件检查,从而导致循环体的重复执行,而循环体本身访问不应该访问的内存。

现在让我们看看如何避免这种情况,即如何告诉编译器您在循环条件中的实际含义。


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

由于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++) {
    //    ^^^^^^         ^^^^
    }
}

由于a < b - 3在数学上等同于a + 3 < b,我们可以交换它们。然而,a + 3 < b阻止了b - 3成为一个巨大的值。回想一下,s.length()返回一个无符号整数,无符号整数执行运算模块2^(bits),其中bits是类型中的位数(通常为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与包含一个很大数字的无符号整数进行比较时,您会循环很长时间。字符串的大小(实际上)与size_t相同,后者是一个无符号整数。当您从该值减去3时,它下溢并继续成为一个大值。

在代码的第二个版本中,将这个无符号值分配给一个有符号的变量,从而获得正确的值。

实际上,导致崩溃的并不是条件或值,很可能是索引超出了范围,这是一种未定义行为。

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

这里的大多数人似乎无法重现这场崩盘;包括我自己;这里的其他答案似乎是基于这样的假设,即您在for循环的主体中遗漏了一些重要的代码,而丢失的代码正是导致崩溃的原因。

如果您正在使用i访问for循环主体中的内存(可能是字符串中的字符),并且为了提供一个最小的示例,您将该代码排除在问题之外,那么由于无符号整数类型的模运算,s.length() - 3的值为SIZE_MAX,这很容易解释崩溃。SIZE_MAX是一个非常大的数字,所以i将不断变大,直到它被用来访问触发segfault的地址。

然而,理论上,即使for循环的主体是空的,代码也可能按原样崩溃。我不知道任何会崩溃的实现,但也许你的编译器和CPU是奇异的。

以下解释并不假设您在问题中遗漏了代码。它相信你在问题中发布的代码会按原样崩溃;它不是其他崩溃代码的缩写替身。

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

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

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

如果在表达式的求值过程中,结果没有在数学上定义,或者不在其类型的可表示值范围内,则行为是未定义的。

在示例程序中,s.length()返回值为2的size_t。除size_t是无符号整数类型外,从中减去3将得到负1。C++11标准规定(在[基本.基本]条款3.9.1第4段中):

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

46)这意味着无符号算术不会溢出,因为不能由结果的无符号整数类型表示的结果被模减为比所得到的无符号整型可以表示的最大值大一的数字。

这意味着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,因此循环终止。

任何一种反应都是合适的。这就是未定义行为的问题—它可能会按你的意愿工作,可能会崩溃,可能会格式化你的硬盘,也可能会取消萤火虫。你永远不会知道。

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

与第一个程序一样,s.length() - 3是值为SIZE_MAXsize_t类型。然而,这一次该值被分配给int。C++11标准说(在[conv.integral]第4.7条第3段中):

如果目的地类型是有符号的,那么如果可以用目的地类型(和位字段宽度)表示,则该值不变;否则,该值由实现定义。

SIZE_MAX太大,无法用int表示,因此len得到一个实现定义的值(可能是-1,但可能不是)。无论分配给len的值如何,条件i < len最终都将为true,因此您的程序将终止,而不会遇到任何未定义的行为。

s.length()的类型是值为2的size_t,因此s.length)-3也是一个无符号类型size_t,它的值是实现定义的SIZE_MAX(如果其大小为64位,则为18446744073709551615)。它至少是32位类型(在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()是无符号类型的量,当您执行s.length)-3时,它变为负值,负值存储为大正值(由于无符号转换规范),循环变为无限,因此崩溃。

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

static_cast<int>(s.length())

您遇到的问题源于以下语句:

i < s.length() - 3

s.length()的结果为unsignedsize_t类型。如果你想象两个的二进制表示:

0…010

然后你从中替换三个,你有效地起飞了三次1,那就是:

0…001

0…000

但是,当它试图从左边获取另一个数字时,你会遇到一个问题,删除它下溢的第三个数字:

1…111

无论您是使用无符号还是有符号符号的类型使用最高有效位(或MSB)来表示数字是否为负数。当发生undeflow时,它只是表示符号类型的负数。

另一方面,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) {
    }
}

需要注意的重要一点是,替换被省略了,相反,加法在其他地方使用了相同的逻辑求值。第一个和最后一个都改变了CCD_ 102循环内可用的CCD_。

我很想提供一个代码示例:

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

经过一番思考,我意识到这是个坏主意。读者的练习就是找出原因!

原因与int a=1000000000;长-长b=a*1000000;会出错。当编译器将这些数字相乘时,它会将其计算为int,因为a和文字1000000000都是int,而且10^18比int的上限大得多,所以它会出错。在您的情况下,我们有s.length()-3,因为s.length)是无符号int,它不能是负的,而且由于s.length()-3被计算为无符号int并且它的值是-1,所以这里也会出现错误。