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

前端 未结 8 912
遇见更好的自我
遇见更好的自我 2020-12-24 11:00

The following code crashes C++ with a runtime error:

#include 

using namespace std;

int main() {
    string s = \"aa\";
    for (int i = 0; i         


        
相关标签:
8条回答
  • 2020-12-24 11:23

    Assuming you left out important code in the for loop

    Most people here seem unable to reproduce the crash—myself included—and it looks like the other answers here are based on the assumption that you left out some important code in the body of the for loop, and that the missing code is what is causing your crash.

    If you are using i to access memory (presumably characters in the string) in the body of the for loop, and you left that code out of your question in an attempt to provide a minimal example, then the crash is easily explained by the fact that s.length() - 3 has the value SIZE_MAX due to modular arithmetic on unsigned integer types. SIZE_MAX is a very big number, so i will keep getting bigger until it is used to access an address that triggers a segfault.

    However, your code could theoretically crash as-is, even if the body of the for loop is empty. I am unaware of any implementations that would crash, but maybe your compiler and CPU are exotic.

    The following explanation does not assume that you left out code in your question. It takes on faith that the code you posted in your question crashes as-is; that it isn't an abbreviated stand-in for some other code that crashes.

    Why your first program crashes

    Your first program crashes because that is its reaction to undefined behavior in your code. (When I try running your code, it terminates without crashing because that is my implementation's reaction to the undefined behavior.)

    The undefined behavior comes from overflowing an int. The C++11 standard says (in [expr] clause 5 paragraph 4):

    If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.

    In your example program, s.length() returns a size_t with value 2. Subtracting 3 from that would yield negative 1, except size_t is an unsigned integer type. The C++11 standard says (in [basic.fundamental] clause 3.9.1 paragraph 4):

    Unsigned integers, declared unsigned, shall obey the laws of arithmetic modulo 2n where n is the number of bits in the value representation of that particular size of integer.46

    46) This implies that unsigned arithmetic does not overflow because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting unsigned integer type.

    This means that the result of s.length() - 3 is a size_t with value SIZE_MAX. This is a very big number, bigger than INT_MAX (the largest value representable by int).

    Because s.length() - 3 is so big, execution spins in the loop until i gets to INT_MAX. On the very next iteration, when it tries to increment i, the result would be INT_MAX + 1 but that is not in the range of representable values for int. Thus, the behavior is undefined. In your case, the behavior is to crash.

    On my system, my implementation's behavior when i is incremented past INT_MAX is to wrap (set i to INT_MIN) and keep going. Once i reaches -1, the usual arithmetic conversions (C++ [expr] clause 5 paragraph 9) cause i to equal SIZE_MAX so the loop terminates.

    Either reaction is appropriate. That is the problem with undefined behavior—it might work as you intend, it might crash, it might format your hard drive, or it might cancel Firefly. You never know.

    How your second program avoids the crash

    As with the first program, s.length() - 3 is a size_t type with value SIZE_MAX. However, this time the value is being assigned to an int. The C++11 standard says (in [conv.integral] clause 4.7 paragraph 3):

    If the destination type is signed, the value is unchanged if it can be represented in the destination type (and bit-field width); otherwise, the value is implementation-defined.

    The value SIZE_MAX is too big to be representable by an int, so len gets an implementation-defined value (probably -1, but maybe not). The condition i < len will eventually be true regardless of the value assigned to len, so your program will terminate without encountering any undefined behavior.

    0 讨论(0)
  • 2020-12-24 11:24

    First of all: why does it crash? Let's step through your program like a debugger would.

    Note: I'll assume that your loop body isn't empty, but accesses the string. If this isn't the case, the cause of the crash is undefined behaviour through integer overflow. See Richard Hansens answer for that.

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

    We would expect the check i < s.length() - 3 to fail right away, since the length of s is two (we only every given it a length at the beginning and never changed it) and 2 - 3 is -1, 0 < -1 is false. However we do get an "OK" here.

    This is because s.length() isn't 2. It's 2u. std::string::length() has return type size_t which is an unsigned integer. So going back to the loop condition, we first get the value of s.length(), so 2u, now subtract 3. 3 is an integer literal and interpreted by the compiler as type int. So the compiler has to calculate 2u - 3, two values of different types. Operations on primitive types only work for same types, so one has to be converted into the other. There are some strict rules, in this case, unsigned "wins", so 3 get's converted to 3u. In unsigned integers, 2u - 3u can't be -1u as such a number does not exists (well, because it has a sign of course!). Instead it calculates every operation modulo 2^(n_bits), where n_bits is the number of bits in this type (usually 8, 16, 32 or 64). So instead of -1 we get 4294967295u (assuming 32bit).

    So now the compiler is done with s.length() - 3 (of course it's much much faster than me ;-) ), now let's go for the comparison: i < s.length() - 3. Putting in the values: 0 < 4294967295u. Again, different types, 0 becomes 0u, the comparison 0u < 4294967295u is obviously true, the loop condition is positively checked, we can now execute the loop body.

    After incrementing, the only thing that changes in the above is the value of i. The value of i will again be converted into an unsigned int, as the comparison needs it.

    So we have

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

    Here's the problem: What do you do in the loop body? Presumably you access the i^th character of your string, don't you? Even though it wasn't your intention, you didn't only accessed the zeroth and first, but also the second! The second doesn't exists (as your string only has two characters, the zeroth and first), you access memory you shouldn't, the program does whatever it wants (undefined behaviour). Note that the program isn't required to crash immediately. It can seem to work fine for another half an hour, so these mistakes are hard to catch. But it's always dangerous to access memory beyond the bounds, this is where most crashes come from.

    So in summary, you get a different value from s.length() - 3 from that what you'd expect, this results in a positive loop condition check, that leads to repetitive execution of the loop body, which in itself accesses memory it shouldn't.

    Now let's see how to avoid that, i.e. how to tell the compiler what you actually meant in your loop condition.


    Lengths of strings and sizes of containers are inherently unsigned so you should use an unsigned integer in for loops.

    Since unsigned int is fairly long and therefore undesirable to write over and over again in loops, just use size_t. This is the type every container in the STL uses for storing length or size. You may need to include cstddef to assert platform independence.

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

    Since a < b - 3 is mathematically equivalent to a + 3 < b, we can interchange them. However, a + 3 < b prevents b - 3 to be a huge value. Recall that s.length() returns an unsigned integer and unsigned integers perform operations module 2^(bits) where bits is the number of bits in the type (usually 8, 16, 32 or 64). Therefore with s.length() == 2, s.length() - 3 == -1 == 2^(bits) - 1.


    Alternatively, if you want to use i < s.length() - 3 for personal preference, you have to add a condition:

    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!
    
    0 讨论(0)
提交回复
热议问题