55

以下代码使 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++) {

    }
}

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

4

8 回答 8

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++) {

    }
}

永远不会进入循环。

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


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

于 2013-07-01T07:05:39.510 回答
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!
.
.

我们预计检查i < s.length() - 3会立即失败,因为 的长度s是 2(我们只在开始时给它一个长度并且从未改变它)并且2 - 3is -1,0 < -1是假的。但是,我们确实在这里得到了“OK”。

这是因为s.length()不是2。是2ustd::string::length()返回类型size_t为无符号整数。所以回到循环条件,我们首先得到 的值s.length(),所以2u,现在减去33是一个整数文字,编译器将其解释为 type 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将再次转换为无符号整数,因为比较需要它。

所以我们有

(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你所期望的不同,这会导致一个积极的循环条件检查,这会导致循环体的重复执行,它本身会访问它不应该访问的内存。

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


字符串的长度和容器的大小本质上是无符号的,因此您应该在 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++) {
    //    ^^^^^^         ^^^^
    }
}

由于a < b - 3在数学上等价于a + 3 < b,我们可以互换它们。但是,a + 3 < b防止b - 3成为一个巨大的价值。回想一下,s.length()返回一个无符号整数和无符号整数执行操作模块2^(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!
于 2013-07-01T08:31:24.437 回答
12

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

在代码的第二个版本中,您将此无符号值分配给一个有符号变量,因此您得到了正确的值。

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

于 2013-07-01T07:04:33.340 回答
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 的 a。从中减去 3 将产生负 1,但size_t无符号整数类型除外。C++11 标准说(在 [basic.fundamental] 条款 3.9.1 第 4 段中):

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

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

这意味着 的结果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具有 value 的类型SIZE_MAX。但是,这次将值分配给int. C++11 标准说(在 [conv.integral] 条款 4.7 第 3 段中):

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

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

于 2013-07-02T23:03:16.233 回答
3

s.length() 的类型size_t值为 2,因此 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 因为它是 asigned integer并且它不进入循环。

说到崩溃,这个“无限”循环并不是导致崩溃的直接原因。如果您在循环中共享代码,您可以获得进一步的解释。

于 2013-07-01T07:03:55.477 回答
1

由于 s.length() 是无符号类型的数量,当您执行 s.length()-3 时,它变为负数,负值存储为大的正值(由于无符号转换规范)并且循环变为无限,因此它崩溃.

要使其工作,您必须将 s.length() 类型转换为:

static_cast < int > (s.length())

于 2013-07-01T12:00:58.333 回答
1

您遇到的问题来自以下语句:

i < s.length() - 3

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

0...010

然后你用这个替换三个,你有效地起飞了 1 次,即:

0...001

0...000

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

1...111

无论您有无符号或有符号类型,都会发生这种情况,但不同之处在于有符号类型使用最高有效位(或 MSB)来表示数字是否为负。当发生逆流时,它仅代表有符号类型的负数。

另一方面, size_t 是unsigned。当它下溢时,它现在将代表 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) {

    }
}

需要注意的重要一点是,替换已被省略,而是在其他地方使用了相同的逻辑评估。第一个和最后一个都改变了循环i内可用的值,for而第二个将保持不变。

我很想提供这个作为代码示例:

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

    }
}

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

于 2013-07-01T13:22:07.390 回答
0

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

于 2015-03-22T20:48:17.290 回答